mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
feat(github): add Codex/OpenCode model support for issue validation
- Support Codex and OpenCode models in issue validation - Add reasoningEffort parameter for Codex model configuration - Update validation logic to use structured output for Claude/Codex - Update UI hooks and types for multi-provider model selection
This commit is contained in:
@@ -5,6 +5,43 @@
|
||||
import type { Request, Response } from 'express';
|
||||
import { execAsync, execEnv, getErrorMessage, logError } from './common.js';
|
||||
|
||||
const GIT_REMOTE_ORIGIN_COMMAND = 'git remote get-url origin';
|
||||
const GH_REPO_VIEW_COMMAND = 'gh repo view --json name,owner';
|
||||
const GITHUB_REPO_URL_PREFIX = 'https://github.com/';
|
||||
const GITHUB_HTTPS_REMOTE_REGEX = /https:\/\/github\.com\/([^/]+)\/([^/.]+)/;
|
||||
const GITHUB_SSH_REMOTE_REGEX = /git@github\.com:([^/]+)\/([^/.]+)/;
|
||||
|
||||
interface GhRepoViewResponse {
|
||||
name?: string;
|
||||
owner?: {
|
||||
login?: string;
|
||||
};
|
||||
}
|
||||
|
||||
async function resolveRepoFromGh(projectPath: string): Promise<{
|
||||
owner: string;
|
||||
repo: string;
|
||||
} | null> {
|
||||
try {
|
||||
const { stdout } = await execAsync(GH_REPO_VIEW_COMMAND, {
|
||||
cwd: projectPath,
|
||||
env: execEnv,
|
||||
});
|
||||
|
||||
const data = JSON.parse(stdout) as GhRepoViewResponse;
|
||||
const owner = typeof data.owner?.login === 'string' ? data.owner.login : null;
|
||||
const repo = typeof data.name === 'string' ? data.name : null;
|
||||
|
||||
if (!owner || !repo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { owner, repo };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export interface GitHubRemoteStatus {
|
||||
hasGitHubRemote: boolean;
|
||||
remoteUrl: string | null;
|
||||
@@ -21,19 +58,38 @@ export async function checkGitHubRemote(projectPath: string): Promise<GitHubRemo
|
||||
};
|
||||
|
||||
try {
|
||||
// Get the remote URL (origin by default)
|
||||
const { stdout } = await execAsync('git remote get-url origin', {
|
||||
cwd: projectPath,
|
||||
env: execEnv,
|
||||
});
|
||||
let remoteUrl = '';
|
||||
try {
|
||||
// Get the remote URL (origin by default)
|
||||
const { stdout } = await execAsync(GIT_REMOTE_ORIGIN_COMMAND, {
|
||||
cwd: projectPath,
|
||||
env: execEnv,
|
||||
});
|
||||
remoteUrl = stdout.trim();
|
||||
status.remoteUrl = remoteUrl || null;
|
||||
} catch {
|
||||
// Ignore missing origin remote
|
||||
}
|
||||
|
||||
const remoteUrl = stdout.trim();
|
||||
status.remoteUrl = remoteUrl;
|
||||
const ghRepo = await resolveRepoFromGh(projectPath);
|
||||
if (ghRepo) {
|
||||
status.hasGitHubRemote = true;
|
||||
status.owner = ghRepo.owner;
|
||||
status.repo = ghRepo.repo;
|
||||
if (!status.remoteUrl) {
|
||||
status.remoteUrl = `${GITHUB_REPO_URL_PREFIX}${ghRepo.owner}/${ghRepo.repo}`;
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
// Check if it's a GitHub URL
|
||||
// Formats: https://github.com/owner/repo.git, git@github.com:owner/repo.git
|
||||
const httpsMatch = remoteUrl.match(/https:\/\/github\.com\/([^/]+)\/([^/.]+)/);
|
||||
const sshMatch = remoteUrl.match(/git@github\.com:([^/]+)\/([^/.]+)/);
|
||||
if (!remoteUrl) {
|
||||
return status;
|
||||
}
|
||||
|
||||
const httpsMatch = remoteUrl.match(GITHUB_HTTPS_REMOTE_REGEX);
|
||||
const sshMatch = remoteUrl.match(GITHUB_SSH_REMOTE_REGEX);
|
||||
|
||||
const match = httpsMatch || sshMatch;
|
||||
if (match) {
|
||||
|
||||
@@ -25,19 +25,24 @@ interface GraphQLComment {
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface GraphQLCommentConnection {
|
||||
totalCount: number;
|
||||
pageInfo: {
|
||||
hasNextPage: boolean;
|
||||
endCursor: string | null;
|
||||
};
|
||||
nodes: GraphQLComment[];
|
||||
}
|
||||
|
||||
interface GraphQLIssueOrPullRequest {
|
||||
__typename: 'Issue' | 'PullRequest';
|
||||
comments: GraphQLCommentConnection;
|
||||
}
|
||||
|
||||
interface GraphQLResponse {
|
||||
data?: {
|
||||
repository?: {
|
||||
issue?: {
|
||||
comments: {
|
||||
totalCount: number;
|
||||
pageInfo: {
|
||||
hasNextPage: boolean;
|
||||
endCursor: string | null;
|
||||
};
|
||||
nodes: GraphQLComment[];
|
||||
};
|
||||
};
|
||||
issueOrPullRequest?: GraphQLIssueOrPullRequest | null;
|
||||
};
|
||||
};
|
||||
errors?: Array<{ message: string }>;
|
||||
@@ -45,6 +50,7 @@ interface GraphQLResponse {
|
||||
|
||||
/** Timeout for GitHub API requests in milliseconds */
|
||||
const GITHUB_API_TIMEOUT_MS = 30000;
|
||||
const COMMENTS_PAGE_SIZE = 50;
|
||||
|
||||
/**
|
||||
* Validate cursor format (GraphQL cursors are typically base64 strings)
|
||||
@@ -54,7 +60,7 @@ function isValidCursor(cursor: string): boolean {
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch comments for a specific issue using GitHub GraphQL API
|
||||
* Fetch comments for a specific issue or pull request using GitHub GraphQL API
|
||||
*/
|
||||
async function fetchIssueComments(
|
||||
projectPath: string,
|
||||
@@ -70,24 +76,52 @@ async function fetchIssueComments(
|
||||
|
||||
// Use GraphQL variables instead of string interpolation for safety
|
||||
const query = `
|
||||
query GetIssueComments($owner: String!, $repo: String!, $issueNumber: Int!, $cursor: String) {
|
||||
query GetIssueComments(
|
||||
$owner: String!
|
||||
$repo: String!
|
||||
$issueNumber: Int!
|
||||
$cursor: String
|
||||
$pageSize: Int!
|
||||
) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
issue(number: $issueNumber) {
|
||||
comments(first: 50, after: $cursor) {
|
||||
totalCount
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
nodes {
|
||||
id
|
||||
author {
|
||||
login
|
||||
avatarUrl
|
||||
issueOrPullRequest(number: $issueNumber) {
|
||||
__typename
|
||||
... on Issue {
|
||||
comments(first: $pageSize, after: $cursor) {
|
||||
totalCount
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
nodes {
|
||||
id
|
||||
author {
|
||||
login
|
||||
avatarUrl
|
||||
}
|
||||
body
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
... on PullRequest {
|
||||
comments(first: $pageSize, after: $cursor) {
|
||||
totalCount
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
nodes {
|
||||
id
|
||||
author {
|
||||
login
|
||||
avatarUrl
|
||||
}
|
||||
body
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
body
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -99,6 +133,7 @@ async function fetchIssueComments(
|
||||
repo,
|
||||
issueNumber,
|
||||
cursor: cursor || null,
|
||||
pageSize: COMMENTS_PAGE_SIZE,
|
||||
};
|
||||
|
||||
const requestBody = JSON.stringify({ query, variables });
|
||||
@@ -140,10 +175,10 @@ async function fetchIssueComments(
|
||||
throw new Error(response.errors[0].message);
|
||||
}
|
||||
|
||||
const commentsData = response.data?.repository?.issue?.comments;
|
||||
const commentsData = response.data?.repository?.issueOrPullRequest?.comments;
|
||||
|
||||
if (!commentsData) {
|
||||
throw new Error('Issue not found or no comments data available');
|
||||
throw new Error('Issue or pull request not found or no comments data available');
|
||||
}
|
||||
|
||||
const comments: GitHubComment[] = commentsData.nodes.map((node) => ({
|
||||
|
||||
@@ -9,6 +9,17 @@ import { checkGitHubRemote } from './check-github-remote.js';
|
||||
import { createLogger } from '@automaker/utils';
|
||||
|
||||
const logger = createLogger('ListIssues');
|
||||
const OPEN_ISSUES_LIMIT = 100;
|
||||
const CLOSED_ISSUES_LIMIT = 50;
|
||||
const ISSUE_LIST_FIELDS = 'number,title,state,author,createdAt,labels,url,body,assignees';
|
||||
const ISSUE_STATE_OPEN = 'open';
|
||||
const ISSUE_STATE_CLOSED = 'closed';
|
||||
const GH_ISSUE_LIST_COMMAND = 'gh issue list';
|
||||
const GH_STATE_FLAG = '--state';
|
||||
const GH_JSON_FLAG = '--json';
|
||||
const GH_LIMIT_FLAG = '--limit';
|
||||
const LINKED_PRS_BATCH_SIZE = 20;
|
||||
const LINKED_PRS_TIMELINE_ITEMS = 10;
|
||||
|
||||
export interface GitHubLabel {
|
||||
name: string;
|
||||
@@ -69,34 +80,68 @@ async function fetchLinkedPRs(
|
||||
|
||||
// Build GraphQL query for batch fetching linked PRs
|
||||
// We fetch up to 20 issues at a time to avoid query limits
|
||||
const batchSize = 20;
|
||||
for (let i = 0; i < issueNumbers.length; i += batchSize) {
|
||||
const batch = issueNumbers.slice(i, i + batchSize);
|
||||
for (let i = 0; i < issueNumbers.length; i += LINKED_PRS_BATCH_SIZE) {
|
||||
const batch = issueNumbers.slice(i, i + LINKED_PRS_BATCH_SIZE);
|
||||
|
||||
const issueQueries = batch
|
||||
.map(
|
||||
(num, idx) => `
|
||||
issue${idx}: issue(number: ${num}) {
|
||||
number
|
||||
timelineItems(first: 10, itemTypes: [CROSS_REFERENCED_EVENT, CONNECTED_EVENT]) {
|
||||
nodes {
|
||||
... on CrossReferencedEvent {
|
||||
source {
|
||||
... on PullRequest {
|
||||
number
|
||||
title
|
||||
state
|
||||
url
|
||||
issue${idx}: issueOrPullRequest(number: ${num}) {
|
||||
... on Issue {
|
||||
number
|
||||
timelineItems(
|
||||
first: ${LINKED_PRS_TIMELINE_ITEMS}
|
||||
itemTypes: [CROSS_REFERENCED_EVENT, CONNECTED_EVENT]
|
||||
) {
|
||||
nodes {
|
||||
... on CrossReferencedEvent {
|
||||
source {
|
||||
... on PullRequest {
|
||||
number
|
||||
title
|
||||
state
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
... on ConnectedEvent {
|
||||
subject {
|
||||
... on PullRequest {
|
||||
number
|
||||
title
|
||||
state
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
... on ConnectedEvent {
|
||||
subject {
|
||||
... on PullRequest {
|
||||
number
|
||||
title
|
||||
state
|
||||
url
|
||||
}
|
||||
}
|
||||
... on PullRequest {
|
||||
number
|
||||
timelineItems(
|
||||
first: ${LINKED_PRS_TIMELINE_ITEMS}
|
||||
itemTypes: [CROSS_REFERENCED_EVENT, CONNECTED_EVENT]
|
||||
) {
|
||||
nodes {
|
||||
... on CrossReferencedEvent {
|
||||
source {
|
||||
... on PullRequest {
|
||||
number
|
||||
title
|
||||
state
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
... on ConnectedEvent {
|
||||
subject {
|
||||
... on PullRequest {
|
||||
number
|
||||
title
|
||||
state
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -213,16 +258,35 @@ export function createListIssuesHandler() {
|
||||
}
|
||||
|
||||
// Fetch open and closed issues in parallel (now including assignees)
|
||||
const repoQualifier =
|
||||
remoteStatus.owner && remoteStatus.repo ? `${remoteStatus.owner}/${remoteStatus.repo}` : '';
|
||||
const repoFlag = repoQualifier ? `-R ${repoQualifier}` : '';
|
||||
const [openResult, closedResult] = await Promise.all([
|
||||
execAsync(
|
||||
'gh issue list --state open --json number,title,state,author,createdAt,labels,url,body,assignees --limit 100',
|
||||
[
|
||||
GH_ISSUE_LIST_COMMAND,
|
||||
repoFlag,
|
||||
`${GH_STATE_FLAG} ${ISSUE_STATE_OPEN}`,
|
||||
`${GH_JSON_FLAG} ${ISSUE_LIST_FIELDS}`,
|
||||
`${GH_LIMIT_FLAG} ${OPEN_ISSUES_LIMIT}`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' '),
|
||||
{
|
||||
cwd: projectPath,
|
||||
env: execEnv,
|
||||
}
|
||||
),
|
||||
execAsync(
|
||||
'gh issue list --state closed --json number,title,state,author,createdAt,labels,url,body,assignees --limit 50',
|
||||
[
|
||||
GH_ISSUE_LIST_COMMAND,
|
||||
repoFlag,
|
||||
`${GH_STATE_FLAG} ${ISSUE_STATE_CLOSED}`,
|
||||
`${GH_JSON_FLAG} ${ISSUE_LIST_FIELDS}`,
|
||||
`${GH_LIMIT_FLAG} ${CLOSED_ISSUES_LIMIT}`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' '),
|
||||
{
|
||||
cwd: projectPath,
|
||||
env: execEnv,
|
||||
|
||||
@@ -6,6 +6,17 @@ import type { Request, Response } from 'express';
|
||||
import { execAsync, execEnv, getErrorMessage, logError } from './common.js';
|
||||
import { checkGitHubRemote } from './check-github-remote.js';
|
||||
|
||||
const OPEN_PRS_LIMIT = 100;
|
||||
const MERGED_PRS_LIMIT = 50;
|
||||
const PR_LIST_FIELDS =
|
||||
'number,title,state,author,createdAt,labels,url,isDraft,headRefName,reviewDecision,mergeable,body';
|
||||
const PR_STATE_OPEN = 'open';
|
||||
const PR_STATE_MERGED = 'merged';
|
||||
const GH_PR_LIST_COMMAND = 'gh pr list';
|
||||
const GH_STATE_FLAG = '--state';
|
||||
const GH_JSON_FLAG = '--json';
|
||||
const GH_LIMIT_FLAG = '--limit';
|
||||
|
||||
export interface GitHubLabel {
|
||||
name: string;
|
||||
color: string;
|
||||
@@ -57,16 +68,36 @@ export function createListPRsHandler() {
|
||||
return;
|
||||
}
|
||||
|
||||
const repoQualifier =
|
||||
remoteStatus.owner && remoteStatus.repo ? `${remoteStatus.owner}/${remoteStatus.repo}` : '';
|
||||
const repoFlag = repoQualifier ? `-R ${repoQualifier}` : '';
|
||||
|
||||
const [openResult, mergedResult] = await Promise.all([
|
||||
execAsync(
|
||||
'gh pr list --state open --json number,title,state,author,createdAt,labels,url,isDraft,headRefName,reviewDecision,mergeable,body --limit 100',
|
||||
[
|
||||
GH_PR_LIST_COMMAND,
|
||||
repoFlag,
|
||||
`${GH_STATE_FLAG} ${PR_STATE_OPEN}`,
|
||||
`${GH_JSON_FLAG} ${PR_LIST_FIELDS}`,
|
||||
`${GH_LIMIT_FLAG} ${OPEN_PRS_LIMIT}`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' '),
|
||||
{
|
||||
cwd: projectPath,
|
||||
env: execEnv,
|
||||
}
|
||||
),
|
||||
execAsync(
|
||||
'gh pr list --state merged --json number,title,state,author,createdAt,labels,url,isDraft,headRefName,reviewDecision,mergeable,body --limit 50',
|
||||
[
|
||||
GH_PR_LIST_COMMAND,
|
||||
repoFlag,
|
||||
`${GH_STATE_FLAG} ${PR_STATE_MERGED}`,
|
||||
`${GH_JSON_FLAG} ${PR_LIST_FIELDS}`,
|
||||
`${GH_LIMIT_FLAG} ${MERGED_PRS_LIMIT}`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' '),
|
||||
{
|
||||
cwd: projectPath,
|
||||
env: execEnv,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*
|
||||
* Scans the codebase to determine if an issue is valid, invalid, or needs clarification.
|
||||
* Runs asynchronously and emits events for progress and completion.
|
||||
* Supports both Claude models and Cursor models.
|
||||
* Supports Claude, Codex, Cursor, and OpenCode models.
|
||||
*/
|
||||
|
||||
import type { Request, Response } from 'express';
|
||||
@@ -11,13 +11,19 @@ import type { EventEmitter } from '../../../lib/events.js';
|
||||
import type {
|
||||
IssueValidationResult,
|
||||
IssueValidationEvent,
|
||||
ModelAlias,
|
||||
CursorModelId,
|
||||
ModelId,
|
||||
GitHubComment,
|
||||
LinkedPRInfo,
|
||||
ThinkingLevel,
|
||||
ReasoningEffort,
|
||||
} from '@automaker/types';
|
||||
import {
|
||||
DEFAULT_PHASE_MODELS,
|
||||
isClaudeModel,
|
||||
isCodexModel,
|
||||
isCursorModel,
|
||||
isOpencodeModel,
|
||||
} from '@automaker/types';
|
||||
import { isCursorModel, DEFAULT_PHASE_MODELS } from '@automaker/types';
|
||||
import { resolvePhaseModel } from '@automaker/model-resolver';
|
||||
import { extractJson } from '../../../lib/json-extractor.js';
|
||||
import { writeValidation } from '../../../lib/validation-storage.js';
|
||||
@@ -39,9 +45,6 @@ import {
|
||||
import type { SettingsService } from '../../../services/settings-service.js';
|
||||
import { getAutoLoadClaudeMdSetting } from '../../../lib/settings-helpers.js';
|
||||
|
||||
/** Valid Claude model values for validation */
|
||||
const VALID_CLAUDE_MODELS: readonly ModelAlias[] = ['opus', 'sonnet', 'haiku'] as const;
|
||||
|
||||
/**
|
||||
* Request body for issue validation
|
||||
*/
|
||||
@@ -51,10 +54,12 @@ interface ValidateIssueRequestBody {
|
||||
issueTitle: string;
|
||||
issueBody: string;
|
||||
issueLabels?: string[];
|
||||
/** Model to use for validation (opus, sonnet, haiku, or cursor model IDs) */
|
||||
model?: ModelAlias | CursorModelId;
|
||||
/** Thinking level for Claude models (ignored for Cursor models) */
|
||||
/** Model to use for validation (Claude alias or provider model ID) */
|
||||
model?: ModelId;
|
||||
/** Thinking level for Claude models (ignored for non-Claude models) */
|
||||
thinkingLevel?: ThinkingLevel;
|
||||
/** Reasoning effort for Codex models (ignored for non-Codex models) */
|
||||
reasoningEffort?: ReasoningEffort;
|
||||
/** Comments to include in validation analysis */
|
||||
comments?: GitHubComment[];
|
||||
/** Linked pull requests for this issue */
|
||||
@@ -66,7 +71,7 @@ interface ValidateIssueRequestBody {
|
||||
*
|
||||
* Emits events for start, progress, complete, and error.
|
||||
* Stores result on completion.
|
||||
* Supports both Claude models (with structured output) and Cursor models (with JSON parsing).
|
||||
* Supports Claude/Codex models (structured output) and Cursor/OpenCode models (JSON parsing).
|
||||
*/
|
||||
async function runValidation(
|
||||
projectPath: string,
|
||||
@@ -74,13 +79,14 @@ async function runValidation(
|
||||
issueTitle: string,
|
||||
issueBody: string,
|
||||
issueLabels: string[] | undefined,
|
||||
model: ModelAlias | CursorModelId,
|
||||
model: ModelId,
|
||||
events: EventEmitter,
|
||||
abortController: AbortController,
|
||||
settingsService?: SettingsService,
|
||||
comments?: ValidationComment[],
|
||||
linkedPRs?: ValidationLinkedPR[],
|
||||
thinkingLevel?: ThinkingLevel
|
||||
thinkingLevel?: ThinkingLevel,
|
||||
reasoningEffort?: ReasoningEffort
|
||||
): Promise<void> {
|
||||
// Emit start event
|
||||
const startEvent: IssueValidationEvent = {
|
||||
@@ -111,8 +117,8 @@ async function runValidation(
|
||||
|
||||
let responseText = '';
|
||||
|
||||
// Determine if we should use structured output (Claude supports it, Cursor doesn't)
|
||||
const useStructuredOutput = !isCursorModel(model);
|
||||
// Determine if we should use structured output (Claude/Codex support it, Cursor/OpenCode don't)
|
||||
const useStructuredOutput = isClaudeModel(model) || isCodexModel(model);
|
||||
|
||||
// Build the final prompt - for Cursor, include system prompt and JSON schema instructions
|
||||
let finalPrompt = basePrompt;
|
||||
@@ -138,14 +144,20 @@ ${basePrompt}`;
|
||||
'[ValidateIssue]'
|
||||
);
|
||||
|
||||
// Use thinkingLevel from request if provided, otherwise fall back to settings
|
||||
// Use request overrides if provided, otherwise fall back to settings
|
||||
let effectiveThinkingLevel: ThinkingLevel | undefined = thinkingLevel;
|
||||
if (!effectiveThinkingLevel) {
|
||||
let effectiveReasoningEffort: ReasoningEffort | undefined = reasoningEffort;
|
||||
if (!effectiveThinkingLevel || !effectiveReasoningEffort) {
|
||||
const settings = await settingsService?.getGlobalSettings();
|
||||
const phaseModelEntry =
|
||||
settings?.phaseModels?.validationModel || DEFAULT_PHASE_MODELS.validationModel;
|
||||
const resolved = resolvePhaseModel(phaseModelEntry);
|
||||
effectiveThinkingLevel = resolved.thinkingLevel;
|
||||
if (!effectiveThinkingLevel) {
|
||||
effectiveThinkingLevel = resolved.thinkingLevel;
|
||||
}
|
||||
if (!effectiveReasoningEffort && typeof phaseModelEntry !== 'string') {
|
||||
effectiveReasoningEffort = phaseModelEntry.reasoningEffort;
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`Using model: ${model}`);
|
||||
@@ -158,6 +170,7 @@ ${basePrompt}`;
|
||||
systemPrompt: useStructuredOutput ? ISSUE_VALIDATION_SYSTEM_PROMPT : undefined,
|
||||
abortController,
|
||||
thinkingLevel: effectiveThinkingLevel,
|
||||
reasoningEffort: effectiveReasoningEffort,
|
||||
readOnly: true, // Issue validation only reads code, doesn't write
|
||||
settingSources: autoLoadClaudeMd ? ['user', 'project', 'local'] : undefined,
|
||||
outputFormat: useStructuredOutput
|
||||
@@ -262,6 +275,7 @@ export function createValidateIssueHandler(
|
||||
issueLabels,
|
||||
model = 'opus',
|
||||
thinkingLevel,
|
||||
reasoningEffort,
|
||||
comments: rawComments,
|
||||
linkedPRs: rawLinkedPRs,
|
||||
} = req.body as ValidateIssueRequestBody;
|
||||
@@ -309,14 +323,17 @@ export function createValidateIssueHandler(
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate model parameter at runtime - accept Claude models or Cursor models
|
||||
const isValidClaudeModel = VALID_CLAUDE_MODELS.includes(model as ModelAlias);
|
||||
const isValidCursorModel = isCursorModel(model);
|
||||
// Validate model parameter at runtime - accept any supported provider model
|
||||
const isValidModel =
|
||||
isClaudeModel(model) ||
|
||||
isCursorModel(model) ||
|
||||
isCodexModel(model) ||
|
||||
isOpencodeModel(model);
|
||||
|
||||
if (!isValidClaudeModel && !isValidCursorModel) {
|
||||
if (!isValidModel) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: `Invalid model. Must be one of: ${VALID_CLAUDE_MODELS.join(', ')}, or a Cursor model ID`,
|
||||
error: 'Invalid model. Must be a Claude, Cursor, Codex, or OpenCode model ID (or alias).',
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -347,7 +364,8 @@ export function createValidateIssueHandler(
|
||||
settingsService,
|
||||
validationComments,
|
||||
validationLinkedPRs,
|
||||
thinkingLevel
|
||||
thinkingLevel,
|
||||
reasoningEffort
|
||||
)
|
||||
.catch(() => {
|
||||
// Error is already handled inside runValidation (event emitted)
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
IssueValidationEvent,
|
||||
StoredValidation,
|
||||
} from '@/lib/electron';
|
||||
import type { LinkedPRInfo, PhaseModelEntry, ModelAlias, CursorModelId } from '@automaker/types';
|
||||
import type { LinkedPRInfo, PhaseModelEntry, ModelId } from '@automaker/types';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import { toast } from 'sonner';
|
||||
import { isValidationStale } from '../utils';
|
||||
@@ -19,12 +19,10 @@ const logger = createLogger('IssueValidation');
|
||||
/**
|
||||
* Extract model string from PhaseModelEntry or string (handles both formats)
|
||||
*/
|
||||
function extractModel(
|
||||
entry: PhaseModelEntry | string | undefined
|
||||
): ModelAlias | CursorModelId | undefined {
|
||||
function extractModel(entry: PhaseModelEntry | string | undefined): ModelId | undefined {
|
||||
if (!entry) return undefined;
|
||||
if (typeof entry === 'string') {
|
||||
return entry as ModelAlias | CursorModelId;
|
||||
return entry as ModelId;
|
||||
}
|
||||
return entry.model;
|
||||
}
|
||||
@@ -228,8 +226,8 @@ export function useIssueValidation({
|
||||
issue: GitHubIssue,
|
||||
options: {
|
||||
forceRevalidate?: boolean;
|
||||
model?: string | PhaseModelEntry; // Accept either string (backward compat) or PhaseModelEntry
|
||||
modelEntry?: PhaseModelEntry; // New preferred way to pass model with thinking level
|
||||
model?: ModelId | PhaseModelEntry; // Accept either model ID (backward compat) or PhaseModelEntry
|
||||
modelEntry?: PhaseModelEntry; // New preferred way to pass model with thinking/reasoning
|
||||
comments?: GitHubComment[];
|
||||
linkedPRs?: LinkedPRInfo[];
|
||||
} = {}
|
||||
@@ -267,15 +265,16 @@ export function useIssueValidation({
|
||||
? modelEntry
|
||||
: model
|
||||
? typeof model === 'string'
|
||||
? { model: model as ModelAlias | CursorModelId }
|
||||
? { model: model as ModelId }
|
||||
: model
|
||||
: phaseModels.validationModel;
|
||||
const normalizedEntry =
|
||||
typeof effectiveModelEntry === 'string'
|
||||
? { model: effectiveModelEntry as ModelAlias | CursorModelId }
|
||||
? { model: effectiveModelEntry as ModelId }
|
||||
: effectiveModelEntry;
|
||||
const modelToUse = normalizedEntry.model;
|
||||
const thinkingLevelToUse = normalizedEntry.thinkingLevel;
|
||||
const reasoningEffortToUse = normalizedEntry.reasoningEffort;
|
||||
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
@@ -292,7 +291,8 @@ export function useIssueValidation({
|
||||
currentProject.path,
|
||||
validationInput,
|
||||
modelToUse,
|
||||
thinkingLevelToUse
|
||||
thinkingLevelToUse,
|
||||
reasoningEffortToUse
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { GitHubIssue, StoredValidation, GitHubComment } from '@/lib/electron';
|
||||
import type { ModelAlias, CursorModelId, LinkedPRInfo, PhaseModelEntry } from '@automaker/types';
|
||||
import type { ModelId, LinkedPRInfo, PhaseModelEntry } from '@automaker/types';
|
||||
|
||||
export interface IssueRowProps {
|
||||
issue: GitHubIssue;
|
||||
@@ -37,7 +37,7 @@ export interface IssueDetailPanelProps {
|
||||
/** Model override state */
|
||||
modelOverride: {
|
||||
effectiveModelEntry: PhaseModelEntry;
|
||||
effectiveModel: ModelAlias | CursorModelId;
|
||||
effectiveModel: ModelId;
|
||||
isOverridden: boolean;
|
||||
setOverride: (entry: PhaseModelEntry | null) => void;
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* Types for validating GitHub issues against the codebase using Claude SDK.
|
||||
*/
|
||||
|
||||
import type { ModelAlias } from './model.js';
|
||||
import type { ModelId } from './model.js';
|
||||
|
||||
/**
|
||||
* Verdict from issue validation
|
||||
@@ -137,8 +137,8 @@ export type IssueValidationEvent =
|
||||
issueTitle: string;
|
||||
result: IssueValidationResult;
|
||||
projectPath: string;
|
||||
/** Model used for validation (opus, sonnet, haiku) */
|
||||
model: ModelAlias;
|
||||
/** Model used for validation */
|
||||
model: ModelId;
|
||||
}
|
||||
| {
|
||||
type: 'issue_validation_error';
|
||||
@@ -162,8 +162,8 @@ export interface StoredValidation {
|
||||
issueTitle: string;
|
||||
/** ISO timestamp when validation was performed */
|
||||
validatedAt: string;
|
||||
/** Model used for validation (opus, sonnet, haiku) */
|
||||
model: ModelAlias;
|
||||
/** Model used for validation */
|
||||
model: ModelId;
|
||||
/** The validation result */
|
||||
result: IssueValidationResult;
|
||||
/** ISO timestamp when user viewed this validation (undefined = not yet viewed) */
|
||||
|
||||
Reference in New Issue
Block a user