Skip to content
Closed
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
2 changes: 1 addition & 1 deletion apps/server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ app.use('/api', authMiddleware);
app.use('/api/fs', createFsRoutes(events));
app.use('/api/agent', createAgentRoutes(agentService, events));
app.use('/api/sessions', createSessionsRoutes(agentService));
app.use('/api/features', createFeaturesRoutes(featureLoader));
app.use('/api/features', createFeaturesRoutes(featureLoader, agentService));
app.use('/api/auto-mode', createAutoModeRoutes(autoModeService));
app.use('/api/enhance-prompt', createEnhancePromptRoutes());
app.use('/api/worktree', createWorktreeRoutes());
Expand Down
12 changes: 11 additions & 1 deletion apps/server/src/routes/features/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,13 @@ import { createUpdateHandler } from './routes/update.js';
import { createDeleteHandler } from './routes/delete.js';
import { createAgentOutputHandler } from './routes/agent-output.js';
import { createGenerateTitleHandler } from './routes/generate-title.js';
import { createValidateFeatureHandler } from './routes/validate-feature.js';
import { AgentService } from '../../services/agent-service.js';

export function createFeaturesRoutes(featureLoader: FeatureLoader): Router {
export function createFeaturesRoutes(
featureLoader: FeatureLoader,
agentService: AgentService
): Router {
const router = Router();

router.post('/list', validatePathParams('projectPath'), createListHandler(featureLoader));
Expand All @@ -23,6 +28,11 @@ export function createFeaturesRoutes(featureLoader: FeatureLoader): Router {
router.post('/delete', validatePathParams('projectPath'), createDeleteHandler(featureLoader));
router.post('/agent-output', createAgentOutputHandler(featureLoader));
router.post('/generate-title', createGenerateTitleHandler());
router.post(
'/validate-feature',
validatePathParams('projectPath'),
createValidateFeatureHandler(featureLoader, agentService)
);

return router;
}
195 changes: 195 additions & 0 deletions apps/server/src/routes/features/routes/validate-feature.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
/**
* Validate feature route - Uses AI agent to check if a feature is already implemented
*/

import type { Request, Response } from 'express';
import { FeatureLoader } from '../../../services/feature-loader.js';
import { AgentService } from '../../../services/agent-service.js';
import { getErrorMessage, logError } from '../common.js';
import type { Feature } from '@automaker/types';

interface ValidateFeatureRequest {
projectPath: string;
featureId: string;
}

export function createValidateFeatureHandler(
featureLoader: FeatureLoader,
agentService: AgentService
) {
return async (req: Request, res: Response) => {
let sessionId: string | undefined;

try {
const { projectPath, featureId }: ValidateFeatureRequest = req.body;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The type ValidateFeatureRequest is used here but is not defined or imported. This will cause a TypeScript error. You should define it within this file or in a shared types file. For example:

interface ValidateFeatureRequest {
  projectPath: string;
  featureId: string;
}


// Load the feature
const feature = await featureLoader.get(projectPath, featureId);
if (!feature) {
return res.status(404).json({
success: false,
error: 'Feature not found',
});
}

// Create a validation prompt
const validationPrompt = `Your task is to review this feature and the existing codebase and determine whether or not it has been fully/partially/not implemented.

Feature Details:
- Title: ${feature.title}
- Category: ${feature.category}
- Description: ${feature.description}

Please analyze the codebase and provide your assessment in the following format (plain text, no markdown):

ASSESSMENT: [FULLY_IMPLEMENTED|PARTIALLY_IMPLEMENTED|NOT_IMPLEMENTED]
REASONING: [Brief explanation of your decision]
EVIDENCE: [Specific code/files that support your assessment]

Be thorough in your analysis. Check for:
- Related components, functions, or classes
- Test files
- Configuration changes
- Documentation updates
- Any other relevant implementation details

If the feature is FULLY_IMPLEMENTED, it should be complete and ready for approval.
If PARTIALLY_IMPLEMENTED, explain what's missing.
If NOT_IMPLEMENTED, explain why you believe this feature hasn't been addressed.`;

// Create a temporary session for validation
let session;
try {
// First create the session metadata
session = await agentService.createSession(
`Feature Validation: ${feature.title}`,
projectPath,
projectPath
);

// Track session ID for cleanup
sessionId = session.id;

// Then initialize the conversation session in memory
await agentService.startConversation({
sessionId: session.id,
workingDirectory: projectPath,
});
} catch (sessionError) {
logError(sessionError, 'Failed to create agent session');
return res.status(500).json({
success: false,
error: getErrorMessage(sessionError) || 'Failed to create agent session',
});
}

// Send the validation prompt to the agent
let result;
try {
result = await agentService.sendMessage({
sessionId: session.id,
message: validationPrompt,
workingDirectory: projectPath,
});
} catch (messageError) {
logError(messageError, 'Failed to send message to agent');

// Clean up the session if it exists
if (sessionId) {
try {
await agentService.deleteSession(sessionId);
} catch (cleanupError) {
logError(cleanupError, 'Failed to cleanup session after message error');
}
}

return res.status(500).json({
success: false,
error: getErrorMessage(messageError) || 'Failed to send message to agent',
});
}

if (!result.success) {
// Clean up the session
if (sessionId) {
try {
await agentService.deleteSession(sessionId);
} catch (cleanupError) {
logError(cleanupError, 'Failed to cleanup session after failed result');
}
}

return res.status(500).json({
success: false,
error: 'Failed to validate feature',
});
}

// Parse the agent response with improved regex
const response = result.message?.content || '';
console.log('[ValidateFeature] Raw AI Response:', response);

// Improved regex patterns to handle edge cases
const assessmentMatch = response.match(
/ASSESSMENT:\s*\*{0,2}(FULLY_IMPLEMENTED|PARTIALLY_IMPLEMENTED|NOT_IMPLEMENTED)\*{0,2}/im
);
const reasoningMatch = response.match(
/REASONING:\s*\*{0,2}([^\n*]+(?:\n[^\n*]+)*?)\*(?=\n[A-Z]+:|$)/im
);
const evidenceMatch = response.match(/EVIDENCE:\s*\*{0,2}([\s\S]*?)(?=\n\n[A-Z]+:|$)/im);

console.log('[ValidateFeature] Regex matches:');
console.log(' - Assessment match:', assessmentMatch);
console.log(' - Reasoning match:', reasoningMatch);
console.log(' - Evidence match:', evidenceMatch);

// Extract values with better fallbacks
const assessment = assessmentMatch?.[1]?.trim() || 'NOT_IMPLEMENTED';
const reasoning = reasoningMatch?.[1]?.trim() || 'Unable to determine reasoning';
const evidence = evidenceMatch?.[1]?.trim() || 'No specific evidence provided';

console.log('[ValidateFeature] Extracted values:');
console.log(' - Assessment:', assessment);
console.log(' - Reasoning:', reasoning);
console.log(' - Evidence:', evidence?.substring(0, 200) + '...');

// Clean up the session
if (sessionId) {
try {
await agentService.deleteSession(sessionId);
} catch (cleanupError) {
logError(cleanupError, 'Failed to cleanup session after successful validation');
}
}

return res.json({
success: true,
validation: {
assessment: assessment as
| 'FULLY_IMPLEMENTED'
| 'PARTIALLY_IMPLEMENTED'
| 'NOT_IMPLEMENTED',
reasoning,
evidence,
fullResponse: response,
},
});
} catch (error) {
logError(error, 'Unexpected error in validate feature handler');

// Ensure session cleanup on any thrown exception
if (sessionId) {
try {
await agentService.deleteSession(sessionId);
} catch (cleanupError) {
logError(cleanupError, 'Failed to cleanup session in outer catch');
}
}

return res.status(500).json({
success: false,
error: getErrorMessage(error),
});
}
};
}
25 changes: 15 additions & 10 deletions apps/ui/src/components/views/board-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,16 @@ export function BoardView() {
const selectedWorktreeBranch =
currentWorktreeBranch || worktrees.find((w) => w.isMain)?.branch || 'main';

// Use column features hook - must be before useBoardActions
const { getColumnFeatures, completedFeatures } = useBoardColumnFeatures({
features: hookFeatures,
runningAutoTasks,
searchQuery,
currentWorktreePath,
currentWorktreeBranch,
projectPath: currentProject?.path || null,
});

// Extract all action handlers into a hook
const {
handleAddFeature,
Expand All @@ -387,6 +397,8 @@ export function BoardView() {
handleForceStopFeature,
handleStartNextFeatures,
handleArchiveAllVerified,
handleValidateFeature,
handleValidateAllBacklog,
} = useBoardActions({
currentProject,
features: hookFeatures,
Expand Down Expand Up @@ -432,6 +444,7 @@ export function BoardView() {
setCurrentWorktree(currentProject.path, newWorktree.path, newWorktree.branch);
},
currentWorktreeBranch,
getColumnFeatures,
});

// Handler for addressing PR comments - creates a feature and starts it automatically
Expand Down Expand Up @@ -804,16 +817,6 @@ export function BoardView() {
handleStartImplementation,
});

// Use column features hook
const { getColumnFeatures, completedFeatures } = useBoardColumnFeatures({
features: hookFeatures,
runningAutoTasks,
searchQuery,
currentWorktreePath,
currentWorktreeBranch,
projectPath: currentProject?.path || null,
});

// Use background hook
const { backgroundSettings, backgroundImageStyle } = useBoardBackground({
currentProject,
Expand Down Expand Up @@ -1087,6 +1090,8 @@ export function BoardView() {
setSpawnParentFeature(feature);
setShowAddDialog(true);
}}
onValidate={handleValidateFeature}
onValidateAllBacklog={handleValidateAllBacklog}
featuresWithContext={featuresWithContext}
runningAutoTasks={runningAutoTasks}
shortcuts={shortcuts}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,15 @@ import {
Eye,
Wand2,
Archive,
Check,
Loader2,
} from 'lucide-react';

interface CardActionsProps {
feature: Feature;
isCurrentAutoTask: boolean;
hasContext?: boolean;
isValidatingAll?: boolean;
shortcutKey?: string;
onEdit: () => void;
onViewOutput?: () => void;
Expand All @@ -28,12 +31,14 @@ interface CardActionsProps {
onComplete?: () => void;
onViewPlan?: () => void;
onApprovePlan?: () => void;
onValidate?: () => void;
}

export function CardActions({
feature,
isCurrentAutoTask,
hasContext,
isValidatingAll,
shortcutKey,
onEdit,
onViewOutput,
Expand All @@ -46,6 +51,7 @@ export function CardActions({
onComplete,
onViewPlan,
onApprovePlan,
onValidate,
}: CardActionsProps) {
return (
<div className="flex flex-wrap gap-1.5 -mx-3 -mb-3 px-3 pb-3">
Expand Down Expand Up @@ -298,6 +304,29 @@ export function CardActions({
<Edit className="w-3 h-3 mr-1" />
Edit
</Button>
{onValidate && (
<Button
variant="outline"
size="sm"
className="h-7 text-xs px-2"
onClick={(e) => {
e.stopPropagation();
onValidate();
}}
onPointerDown={(e) => e.stopPropagation()}
data-testid={`validate-${feature.id}`}
title="Check if feature is already implemented"
disabled={
feature.metadata?.[`validating-${feature.id}`] === true || isValidatingAll === true
}
>
{feature.metadata?.[`validating-${feature.id}`] === true ? (
<Loader2 className="w-3 h-3 animate-spin" />
) : (
<Check className="w-3 h-3" />
)}
</Button>
)}
{feature.planSpec?.content && onViewPlan && (
<Button
variant="outline"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ interface KanbanCardProps {
onViewPlan?: () => void;
onApprovePlan?: () => void;
onSpawnTask?: () => void;
onValidate?: () => void;
hasContext?: boolean;
isCurrentAutoTask?: boolean;
isValidatingAll?: boolean;
shortcutKey?: string;
contextContent?: string;
summary?: string;
Expand All @@ -53,8 +55,10 @@ export const KanbanCard = memo(function KanbanCard({
onViewPlan,
onApprovePlan,
onSpawnTask,
onValidate,
hasContext,
isCurrentAutoTask,
isValidatingAll,
shortcutKey,
contextContent,
summary,
Expand Down Expand Up @@ -168,6 +172,7 @@ export const KanbanCard = memo(function KanbanCard({
feature={feature}
isCurrentAutoTask={!!isCurrentAutoTask}
hasContext={hasContext}
isValidatingAll={isValidatingAll}
shortcutKey={shortcutKey}
onEdit={onEdit}
onViewOutput={onViewOutput}
Expand All @@ -180,6 +185,7 @@ export const KanbanCard = memo(function KanbanCard({
onComplete={onComplete}
onViewPlan={onViewPlan}
onApprovePlan={onApprovePlan}
onValidate={onValidate}
/>
</CardContent>
</Card>
Expand Down
Loading