feat: Implement GitHub issue validation endpoint and UI integration

- Added a new endpoint for validating GitHub issues using the Claude SDK.
- Introduced validation schema and logic to handle issue validation requests.
- Updated GitHub routes to include the new validation route.
- Enhanced the UI with a validation dialog and button to trigger issue validation.
- Mapped issue complexity to feature priority for better task management.
- Integrated validation results display in the UI, allowing users to convert validated issues into tasks.
This commit is contained in:
Kacper
2025-12-23 15:50:10 +01:00
parent d4d4b8fb3d
commit a881d175bc
9 changed files with 835 additions and 5 deletions

View File

@@ -3,16 +3,19 @@
*/
import { Router } from 'express';
import { validatePathParams } from '../../middleware/validate-paths.js';
import { createCheckGitHubRemoteHandler } from './routes/check-github-remote.js';
import { createListIssuesHandler } from './routes/list-issues.js';
import { createListPRsHandler } from './routes/list-prs.js';
import { createValidateIssueHandler } from './routes/validate-issue.js';
export function createGitHubRoutes(): Router {
const router = Router();
router.post('/check-remote', createCheckGitHubRemoteHandler());
router.post('/issues', createListIssuesHandler());
router.post('/prs', createListPRsHandler());
router.post('/check-remote', validatePathParams('projectPath'), createCheckGitHubRemoteHandler());
router.post('/issues', validatePathParams('projectPath'), createListIssuesHandler());
router.post('/prs', validatePathParams('projectPath'), createListPRsHandler());
router.post('/validate-issue', validatePathParams('projectPath'), createValidateIssueHandler());
return router;
}

View File

@@ -0,0 +1,164 @@
/**
* POST /validate-issue endpoint - Validate a GitHub issue using Claude SDK
*
* Scans the codebase to determine if an issue is valid, invalid, or needs clarification.
*/
import type { Request, Response } from 'express';
import { query } from '@anthropic-ai/claude-agent-sdk';
import { createLogger } from '@automaker/utils';
import type { IssueValidationResult } from '@automaker/types';
import { createSuggestionsOptions } from '../../../lib/sdk-options.js';
import {
issueValidationSchema,
ISSUE_VALIDATION_SYSTEM_PROMPT,
buildValidationPrompt,
} from './validation-schema.js';
import { getErrorMessage, logError } from './common.js';
const logger = createLogger('IssueValidation');
/**
* Request body for issue validation
*/
interface ValidateIssueRequestBody {
projectPath: string;
issueNumber: number;
issueTitle: string;
issueBody: string;
issueLabels?: string[];
}
/**
* Creates the handler for validating GitHub issues against the codebase.
*
* Uses Claude SDK with:
* - Read-only tools (Read, Glob, Grep) for codebase analysis
* - JSON schema structured output for reliable parsing
* - System prompt guiding the validation process
*/
export function createValidateIssueHandler() {
return async (req: Request, res: Response): Promise<void> => {
// Declare timeoutId outside try block for proper cleanup
let timeoutId: ReturnType<typeof setTimeout> | undefined;
try {
const { projectPath, issueNumber, issueTitle, issueBody, issueLabels } =
req.body as ValidateIssueRequestBody;
// Validate required fields
if (!projectPath) {
res.status(400).json({ success: false, error: 'projectPath is required' });
return;
}
if (!issueNumber || typeof issueNumber !== 'number') {
res
.status(400)
.json({ success: false, error: 'issueNumber is required and must be a number' });
return;
}
if (!issueTitle || typeof issueTitle !== 'string') {
res.status(400).json({ success: false, error: 'issueTitle is required' });
return;
}
if (typeof issueBody !== 'string') {
res.status(400).json({ success: false, error: 'issueBody must be a string' });
return;
}
logger.info(`Validating issue #${issueNumber}: ${issueTitle}`);
// Build the prompt
const prompt = buildValidationPrompt(issueNumber, issueTitle, issueBody, issueLabels);
// Create abort controller with 2 minute timeout for validation
const abortController = new AbortController();
const VALIDATION_TIMEOUT_MS = 120000; // 2 minutes
timeoutId = setTimeout(() => {
logger.warn(`Validation timeout reached after ${VALIDATION_TIMEOUT_MS}ms`);
abortController.abort();
}, VALIDATION_TIMEOUT_MS);
// Create SDK options with structured output and abort controller
const options = createSuggestionsOptions({
cwd: projectPath,
systemPrompt: ISSUE_VALIDATION_SYSTEM_PROMPT,
abortController,
outputFormat: {
type: 'json_schema',
schema: issueValidationSchema as Record<string, unknown>,
},
});
// Execute the query
const stream = query({ prompt, options });
let validationResult: IssueValidationResult | null = null;
let responseText = '';
for await (const msg of stream) {
// Collect assistant text for debugging/fallback
if (msg.type === 'assistant' && msg.message?.content) {
for (const block of msg.message.content) {
if (block.type === 'text') {
responseText += block.text;
}
}
}
// Extract structured output on success
if (msg.type === 'result' && msg.subtype === 'success') {
const resultMsg = msg as { structured_output?: IssueValidationResult };
if (resultMsg.structured_output) {
validationResult = resultMsg.structured_output;
logger.debug('Received structured output:', validationResult);
}
}
// Handle errors
if (msg.type === 'result') {
const resultMsg = msg as { subtype?: string };
if (resultMsg.subtype === 'error_max_structured_output_retries') {
logger.error('Failed to produce valid structured output after retries');
throw new Error('Could not produce valid validation output');
}
}
}
// Require structured output - no fragile fallback parsing
if (!validationResult) {
logger.error('No structured output received from Claude SDK');
logger.debug('Raw response text:', responseText);
throw new Error('Validation failed: no structured output received');
}
// Clear the timeout since we completed successfully
clearTimeout(timeoutId);
logger.info(`Issue #${issueNumber} validation complete: ${validationResult.verdict}`);
res.json({
success: true,
issueNumber,
validation: validationResult,
});
} catch (error) {
// Clear timeout on error as well (if it was set)
if (timeoutId) {
clearTimeout(timeoutId);
}
logError(error, `Issue validation failed`);
logger.error('Issue validation error:', error);
// Check if response already sent
if (!res.headersSent) {
res.status(500).json({
success: false,
error: getErrorMessage(error),
});
}
}
};
}

View File

@@ -0,0 +1,138 @@
/**
* Issue Validation Schema and System Prompt
*
* Defines the JSON schema for Claude's structured output and
* the system prompt that guides the validation process.
*/
/**
* JSON Schema for issue validation structured output.
* Used with Claude SDK's outputFormat option to ensure reliable parsing.
*/
export const issueValidationSchema = {
type: 'object',
properties: {
verdict: {
type: 'string',
enum: ['valid', 'invalid', 'needs_clarification'],
description: 'The validation verdict for the issue',
},
confidence: {
type: 'string',
enum: ['high', 'medium', 'low'],
description: 'How confident the AI is in its assessment',
},
reasoning: {
type: 'string',
description: 'Detailed explanation of the verdict',
},
bugConfirmed: {
type: 'boolean',
description: 'For bug reports: whether the bug was confirmed in the codebase',
},
relatedFiles: {
type: 'array',
items: { type: 'string' },
description: 'Files related to the issue found during analysis',
},
suggestedFix: {
type: 'string',
description: 'Suggested approach to fix or implement the issue',
},
missingInfo: {
type: 'array',
items: { type: 'string' },
description: 'Information needed when verdict is needs_clarification',
},
estimatedComplexity: {
type: 'string',
enum: ['trivial', 'simple', 'moderate', 'complex', 'very_complex'],
description: 'Estimated effort to address the issue',
},
},
required: ['verdict', 'confidence', 'reasoning'],
additionalProperties: false,
} as const;
/**
* System prompt that guides Claude in validating GitHub issues.
* Instructs the model to use read-only tools to analyze the codebase.
*/
export const ISSUE_VALIDATION_SYSTEM_PROMPT = `You are an expert code analyst validating GitHub issues against a codebase.
Your task is to analyze a GitHub issue and determine if it's valid by scanning the codebase.
## Validation Process
1. **Read the issue carefully** - Understand what is being reported or requested
2. **Search the codebase** - Use Glob to find relevant files by pattern, Grep to search for keywords
3. **Examine the code** - Use Read to look at the actual implementation in relevant files
4. **Form your verdict** - Based on your analysis, determine if the issue is valid
## Verdicts
- **valid**: The issue describes a real problem that exists in the codebase, or a clear feature request that can be implemented. The referenced files/components exist and the issue is actionable.
- **invalid**: The issue describes behavior that doesn't exist, references non-existent files or components, is based on a misunderstanding of the code, or the described "bug" is actually expected behavior.
- **needs_clarification**: The issue lacks sufficient detail to verify. Specify what additional information is needed in the missingInfo field.
## For Bug Reports, Check:
- Do the referenced files/components exist?
- Does the code match what the issue describes?
- Is the described behavior actually a bug or expected?
- Can you locate the code that would cause the reported issue?
## For Feature Requests, Check:
- Does the feature already exist?
- Is the implementation location clear?
- Is the request technically feasible given the codebase structure?
## Response Guidelines
- **Always include relatedFiles** when you find relevant code
- **Set bugConfirmed to true** only if you can definitively confirm a bug exists in the code
- **Provide a suggestedFix** when you have a clear idea of how to address the issue
- **Use missingInfo** when the verdict is needs_clarification to list what's needed
- **Set estimatedComplexity** to help prioritize:
- trivial: Simple text changes, one-line fixes
- simple: Small changes to one file
- moderate: Changes to multiple files or moderate logic changes
- complex: Significant refactoring or new feature implementation
- very_complex: Major architectural changes or cross-cutting concerns
Be thorough in your analysis but focus on files that are directly relevant to the issue.`;
/**
* Build the user prompt for issue validation.
*
* Creates a structured prompt that includes the issue details for Claude
* to analyze against the codebase.
*
* @param issueNumber - The GitHub issue number
* @param issueTitle - The issue title
* @param issueBody - The issue body/description
* @param issueLabels - Optional array of label names
* @returns Formatted prompt string for the validation request
*/
export function buildValidationPrompt(
issueNumber: number,
issueTitle: string,
issueBody: string,
issueLabels?: string[]
): string {
const labelsSection = issueLabels?.length ? `\n\n**Labels:** ${issueLabels.join(', ')}` : '';
return `Please validate the following GitHub issue by analyzing the codebase:
## Issue #${issueNumber}: ${issueTitle}
${labelsSection}
### Description
${issueBody || '(No description provided)'}
---
Scan the codebase to verify this issue. Look for the files, components, or functionality mentioned. Determine if this issue is valid, invalid, or needs clarification.`;
}

View File

@@ -1,10 +1,43 @@
import { useState, useEffect, useCallback } from 'react';
import { CircleDot, Loader2, RefreshCw, ExternalLink, CheckCircle2, Circle, X } from 'lucide-react';
import { getElectronAPI, GitHubIssue } from '@/lib/electron';
import {
CircleDot,
Loader2,
RefreshCw,
ExternalLink,
CheckCircle2,
Circle,
X,
Wand2,
} from 'lucide-react';
import {
getElectronAPI,
GitHubIssue,
IssueValidationResult,
IssueComplexity,
} from '@/lib/electron';
/**
* Map issue complexity to feature priority.
* Lower complexity issues get higher priority (1 = high, 2 = medium).
*/
function getFeaturePriority(complexity: IssueComplexity | undefined): number {
switch (complexity) {
case 'trivial':
case 'simple':
return 1; // High priority for easy wins
case 'moderate':
case 'complex':
case 'very_complex':
default:
return 2; // Medium priority for larger efforts
}
}
import { useAppStore } from '@/store/app-store';
import { Button } from '@/components/ui/button';
import { Markdown } from '@/components/ui/markdown';
import { cn } from '@/lib/utils';
import { toast } from 'sonner';
import { ValidationDialog } from './github-issues-view/validation-dialog';
export function GitHubIssuesView() {
const [openIssues, setOpenIssues] = useState<GitHubIssue[]>([]);
@@ -13,6 +46,9 @@ export function GitHubIssuesView() {
const [refreshing, setRefreshing] = useState(false);
const [error, setError] = useState<string | null>(null);
const [selectedIssue, setSelectedIssue] = useState<GitHubIssue | null>(null);
const [validating, setValidating] = useState(false);
const [validationResult, setValidationResult] = useState<IssueValidationResult | null>(null);
const [showValidationDialog, setShowValidationDialog] = useState(false);
const { currentProject } = useAppStore();
const fetchIssues = useCallback(async () => {
@@ -57,6 +93,103 @@ export function GitHubIssuesView() {
api.openExternalLink(url);
}, []);
const handleValidateIssue = useCallback(
async (issue: GitHubIssue) => {
if (!currentProject?.path) {
toast.error('No project selected');
return;
}
setValidating(true);
setValidationResult(null);
setShowValidationDialog(true);
try {
const api = getElectronAPI();
if (api.github?.validateIssue) {
const result = await api.github.validateIssue(currentProject.path, {
issueNumber: issue.number,
issueTitle: issue.title,
issueBody: issue.body || '',
issueLabels: issue.labels.map((l) => l.name),
});
if (result.success) {
setValidationResult(result.validation);
} else {
toast.error(result.error || 'Failed to validate issue');
setShowValidationDialog(false);
}
}
} catch (err) {
console.error('[GitHubIssuesView] Validation error:', err);
toast.error(err instanceof Error ? err.message : 'Failed to validate issue');
setShowValidationDialog(false);
} finally {
setValidating(false);
}
},
[currentProject?.path]
);
const handleConvertToTask = useCallback(
async (issue: GitHubIssue, validation: IssueValidationResult) => {
if (!currentProject?.path) {
toast.error('No project selected');
return;
}
try {
const api = getElectronAPI();
if (api.features?.create) {
// Build description from issue body + validation info
const description = [
`**From GitHub Issue #${issue.number}**`,
'',
issue.body || 'No description provided.',
'',
'---',
'',
'**AI Validation Analysis:**',
validation.reasoning,
validation.suggestedFix ? `\n**Suggested Approach:**\n${validation.suggestedFix}` : '',
validation.relatedFiles?.length
? `\n**Related Files:**\n${validation.relatedFiles.map((f) => `- \`${f}\``).join('\n')}`
: '',
]
.filter(Boolean)
.join('\n');
const feature = {
id: `issue-${issue.number}-${crypto.randomUUID()}`,
title: issue.title,
description,
category: 'From GitHub',
status: 'backlog' as const,
passes: false,
priority: getFeaturePriority(validation.estimatedComplexity),
model: 'opus' as const,
thinkingLevel: 'none' as const,
branchName: '',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
const result = await api.features.create(currentProject.path, feature);
if (result.success) {
toast.success(`Created task: ${issue.title}`);
} else {
toast.error(result.error || 'Failed to create task');
}
}
} catch (err) {
console.error('[GitHubIssuesView] Convert to task error:', err);
toast.error(err instanceof Error ? err.message : 'Failed to create task');
}
},
[currentProject?.path]
);
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
@@ -184,6 +317,19 @@ export function GitHubIssuesView() {
</span>
</div>
<div className="flex items-center gap-2 flex-shrink-0">
<Button
variant="default"
size="sm"
onClick={() => handleValidateIssue(selectedIssue)}
disabled={validating}
>
{validating ? (
<Loader2 className="h-4 w-4 mr-1 animate-spin" />
) : (
<Wand2 className="h-4 w-4 mr-1" />
)}
Validate with AI
</Button>
<Button
variant="outline"
size="sm"
@@ -260,6 +406,16 @@ export function GitHubIssuesView() {
</div>
</div>
)}
{/* Validation Dialog */}
<ValidationDialog
open={showValidationDialog}
onOpenChange={setShowValidationDialog}
issue={selectedIssue}
validationResult={validationResult}
isValidating={validating}
onConvertToTask={handleConvertToTask}
/>
</div>
);
}

View File

@@ -0,0 +1,238 @@
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import {
CheckCircle2,
XCircle,
AlertCircle,
FileCode,
Lightbulb,
AlertTriangle,
Loader2,
Plus,
} from 'lucide-react';
import { cn } from '@/lib/utils';
import type {
IssueValidationResult,
IssueValidationVerdict,
IssueValidationConfidence,
IssueComplexity,
GitHubIssue,
} from '@/lib/electron';
interface ValidationDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
issue: GitHubIssue | null;
validationResult: IssueValidationResult | null;
isValidating: boolean;
onConvertToTask?: (issue: GitHubIssue, validation: IssueValidationResult) => void;
}
const verdictConfig: Record<
IssueValidationVerdict,
{ label: string; color: string; bgColor: string; icon: typeof CheckCircle2 }
> = {
valid: {
label: 'Valid',
color: 'text-green-500',
bgColor: 'bg-green-500/10',
icon: CheckCircle2,
},
invalid: {
label: 'Invalid',
color: 'text-red-500',
bgColor: 'bg-red-500/10',
icon: XCircle,
},
needs_clarification: {
label: 'Needs Clarification',
color: 'text-yellow-500',
bgColor: 'bg-yellow-500/10',
icon: AlertCircle,
},
};
const confidenceConfig: Record<IssueValidationConfidence, { label: string; color: string }> = {
high: { label: 'High Confidence', color: 'text-green-500' },
medium: { label: 'Medium Confidence', color: 'text-yellow-500' },
low: { label: 'Low Confidence', color: 'text-orange-500' },
};
const complexityConfig: Record<IssueComplexity, { label: string; color: string }> = {
trivial: { label: 'Trivial', color: 'text-green-500' },
simple: { label: 'Simple', color: 'text-blue-500' },
moderate: { label: 'Moderate', color: 'text-yellow-500' },
complex: { label: 'Complex', color: 'text-orange-500' },
very_complex: { label: 'Very Complex', color: 'text-red-500' },
};
export function ValidationDialog({
open,
onOpenChange,
issue,
validationResult,
isValidating,
onConvertToTask,
}: ValidationDialogProps) {
if (!issue) return null;
const handleConvertToTask = () => {
if (validationResult && onConvertToTask) {
onConvertToTask(issue, validationResult);
onOpenChange(false);
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">Issue Validation Result</DialogTitle>
<DialogDescription>
#{issue.number}: {issue.title}
</DialogDescription>
</DialogHeader>
{isValidating ? (
<div className="flex flex-col items-center justify-center py-12 space-y-4">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
<p className="text-sm text-muted-foreground">Analyzing codebase to validate issue...</p>
</div>
) : validationResult ? (
<div className="space-y-6 py-4">
{/* Verdict Badge */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
{(() => {
const config = verdictConfig[validationResult.verdict];
const Icon = config.icon;
return (
<>
<div className={cn('p-2 rounded-lg', config.bgColor)}>
<Icon className={cn('h-6 w-6', config.color)} />
</div>
<div>
<p className={cn('text-lg font-semibold', config.color)}>{config.label}</p>
<p
className={cn(
'text-sm',
confidenceConfig[validationResult.confidence].color
)}
>
{confidenceConfig[validationResult.confidence].label}
</p>
</div>
</>
);
})()}
</div>
{validationResult.estimatedComplexity && (
<div className="text-right">
<p className="text-xs text-muted-foreground">Estimated Complexity</p>
<p
className={cn(
'text-sm font-medium',
complexityConfig[validationResult.estimatedComplexity].color
)}
>
{complexityConfig[validationResult.estimatedComplexity].label}
</p>
</div>
)}
</div>
{/* Bug Confirmed Badge */}
{validationResult.bugConfirmed && (
<div className="flex items-center gap-2 p-3 rounded-lg bg-red-500/10 border border-red-500/20">
<AlertTriangle className="h-5 w-5 text-red-500 flex-shrink-0" />
<span className="text-sm font-medium text-red-500">Bug Confirmed in Codebase</span>
</div>
)}
{/* Reasoning */}
<div className="space-y-2">
<h4 className="text-sm font-medium flex items-center gap-2">
<Lightbulb className="h-4 w-4 text-muted-foreground" />
Analysis
</h4>
<p className="text-sm text-muted-foreground leading-relaxed whitespace-pre-wrap">
{validationResult.reasoning}
</p>
</div>
{/* Related Files */}
{validationResult.relatedFiles && validationResult.relatedFiles.length > 0 && (
<div className="space-y-2">
<h4 className="text-sm font-medium flex items-center gap-2">
<FileCode className="h-4 w-4 text-muted-foreground" />
Related Files
</h4>
<div className="space-y-1">
{validationResult.relatedFiles.map((file, index) => (
<div
key={index}
className="text-sm font-mono bg-muted/50 px-2 py-1 rounded text-muted-foreground"
>
{file}
</div>
))}
</div>
</div>
)}
{/* Suggested Fix */}
{validationResult.suggestedFix && (
<div className="space-y-2">
<h4 className="text-sm font-medium">Suggested Approach</h4>
<p className="text-sm text-muted-foreground leading-relaxed whitespace-pre-wrap bg-muted/30 p-3 rounded-lg border border-border">
{validationResult.suggestedFix}
</p>
</div>
)}
{/* Missing Info (for needs_clarification) */}
{validationResult.missingInfo && validationResult.missingInfo.length > 0 && (
<div className="space-y-2">
<h4 className="text-sm font-medium flex items-center gap-2">
<AlertCircle className="h-4 w-4 text-yellow-500" />
Missing Information
</h4>
<ul className="space-y-1 list-disc list-inside">
{validationResult.missingInfo.map((info, index) => (
<li key={index} className="text-sm text-muted-foreground">
{info}
</li>
))}
</ul>
</div>
)}
</div>
) : (
<div className="flex flex-col items-center justify-center py-12 text-center">
<AlertCircle className="h-8 w-8 text-muted-foreground mb-4" />
<p className="text-sm text-muted-foreground">No validation result available.</p>
</div>
)}
<DialogFooter>
<Button variant="ghost" onClick={() => onOpenChange(false)}>
Close
</Button>
{validationResult?.verdict === 'valid' && onConvertToTask && (
<Button onClick={handleConvertToTask}>
<Plus className="h-4 w-4 mr-2" />
Convert to Task
</Button>
)}
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -1,8 +1,26 @@
// Type definitions for Electron IPC API
import type { SessionListItem, Message } from '@/types/electron';
import type { ClaudeUsageResponse } from '@/store/app-store';
import type {
IssueValidationVerdict,
IssueValidationConfidence,
IssueComplexity,
IssueValidationInput,
IssueValidationResult,
IssueValidationResponse,
} from '@automaker/types';
import { getJSON, setJSON, removeItem } from './storage';
// Re-export issue validation types for use in components
export type {
IssueValidationVerdict,
IssueValidationConfidence,
IssueComplexity,
IssueValidationInput,
IssueValidationResult,
IssueValidationResponse,
};
export interface FileEntry {
name: string;
isDirectory: boolean;
@@ -156,6 +174,10 @@ export interface GitHubAPI {
mergedPRs?: GitHubPR[];
error?: string;
}>;
validateIssue: (
projectPath: string,
issue: IssueValidationInput
) => Promise<IssueValidationResponse | { success: false; error: string }>;
}
// Feature Suggestions types
@@ -2631,6 +2653,22 @@ function createMockGitHubAPI(): GitHubAPI {
mergedPRs: [],
};
},
validateIssue: async (projectPath: string, issue: IssueValidationInput) => {
console.log('[Mock] Validating GitHub issue:', { projectPath, issue });
// Return a mock validation result
return {
success: true as const,
issueNumber: issue.issueNumber,
validation: {
verdict: 'valid' as const,
confidence: 'medium' as const,
reasoning:
'This is a mock validation. In production, Claude SDK would analyze the codebase to validate this issue.',
relatedFiles: ['src/components/example.tsx'],
estimatedComplexity: 'moderate' as const,
},
};
},
};
}

View File

@@ -24,6 +24,7 @@ import type {
GitHubAPI,
GitHubIssue,
GitHubPR,
IssueValidationInput,
} from './electron';
import type { Message, SessionListItem } from '@/types/electron';
import type { Feature, ClaudeUsageResponse } from '@/store/app-store';
@@ -751,6 +752,8 @@ export class HttpApiClient implements ElectronAPI {
checkRemote: (projectPath: string) => this.post('/api/github/check-remote', { projectPath }),
listIssues: (projectPath: string) => this.post('/api/github/issues', { projectPath }),
listPRs: (projectPath: string) => this.post('/api/github/prs', { projectPath }),
validateIssue: (projectPath: string, issue: IssueValidationInput) =>
this.post('/api/github/validate-issue', { projectPath, ...issue }),
};
// Workspace API

View File

@@ -81,3 +81,15 @@ export {
THINKING_LEVEL_LABELS,
getModelDisplayName,
} from './model-display.js';
// Issue validation types
export type {
IssueValidationVerdict,
IssueValidationConfidence,
IssueComplexity,
IssueValidationInput,
IssueValidationRequest,
IssueValidationResult,
IssueValidationResponse,
IssueValidationErrorResponse,
} from './issue-validation.js';

View File

@@ -0,0 +1,78 @@
/**
* Issue Validation Types
*
* Types for validating GitHub issues against the codebase using Claude SDK.
*/
/**
* Verdict from issue validation
*/
export type IssueValidationVerdict = 'valid' | 'invalid' | 'needs_clarification';
/**
* Confidence level of the validation
*/
export type IssueValidationConfidence = 'high' | 'medium' | 'low';
/**
* Complexity estimation for valid issues
*/
export type IssueComplexity = 'trivial' | 'simple' | 'moderate' | 'complex' | 'very_complex';
/**
* Issue data for validation (without projectPath)
* Used by UI when calling the validation API
*/
export interface IssueValidationInput {
issueNumber: number;
issueTitle: string;
issueBody: string;
issueLabels?: string[];
}
/**
* Full request payload for issue validation endpoint
* Includes projectPath for server-side handling
*/
export interface IssueValidationRequest extends IssueValidationInput {
projectPath: string;
}
/**
* Result from Claude's issue validation analysis
*/
export interface IssueValidationResult {
/** Whether the issue is valid, invalid, or needs clarification */
verdict: IssueValidationVerdict;
/** How confident the AI is in its assessment */
confidence: IssueValidationConfidence;
/** Detailed explanation of the verdict */
reasoning: string;
/** For bug reports: whether the bug was confirmed in the codebase */
bugConfirmed?: boolean;
/** Files related to the issue found during analysis */
relatedFiles?: string[];
/** Suggested approach to fix or implement */
suggestedFix?: string;
/** Information that's missing and needed for validation (when verdict = needs_clarification) */
missingInfo?: string[];
/** Estimated effort to address the issue */
estimatedComplexity?: IssueComplexity;
}
/**
* Successful response from validate-issue endpoint
*/
export interface IssueValidationResponse {
success: true;
issueNumber: number;
validation: IssueValidationResult;
}
/**
* Error response from validate-issue endpoint
*/
export interface IssueValidationErrorResponse {
success: false;
error: string;
}