@@ -14,7 +14,6 @@ import { retryWithBackoff } from "./retry_utils.js";
1414const MODEL_ID = "us.anthropic.claude-sonnet-4-20250514-v1:0" ;
1515const SIMILARITY_THRESHOLD = 0.8 ;
1616const BATCH_SIZE = 10 ;
17- const DAYS_TO_SEARCH = 90 ;
1817
1918// Security: Maximum lengths for input validation
2019const MAX_TITLE_LENGTH = 500 ;
@@ -65,7 +64,8 @@ function sanitizePromptInput(input: string, maxLength: number): string {
6564}
6665
6766/**
68- * Fetch existing open issues from repository
67+ * Fetch existing open issues from repository with Bug or Feature type
68+ * Falls back to bug/feature labels if issue types are not configured
6969 */
7070export async function fetchExistingIssues (
7171 owner : string ,
@@ -74,38 +74,81 @@ export async function fetchExistingIssues(
7474 githubToken : string
7575) : Promise < IssueData [ ] > {
7676 const client = new Octokit ( { auth : githubToken } ) ;
77- const cutoffDate = new Date ( ) ;
78- cutoffDate . setDate ( cutoffDate . getDate ( ) - DAYS_TO_SEARCH ) ;
7977
8078 try {
81- const { data : issues } = await client . issues . listForRepo ( {
82- owner,
83- repo,
84- state : "open" ,
85- per_page : 100 ,
86- sort : "created" ,
87- direction : "desc" ,
79+ // Fetch all open issues (up to 1000 for better duplicate detection)
80+ // GitHub API allows max 100 per page, so we'll fetch multiple pages
81+ const allIssues : any [ ] = [ ] ;
82+ let page = 1 ;
83+ const perPage = 100 ;
84+ const maxPages = 10 ; // Fetch up to 1000 issues
85+
86+ while ( page <= maxPages ) {
87+ const { data : pageIssues } = await client . issues . listForRepo ( {
88+ owner,
89+ repo,
90+ state : "open" ,
91+ per_page : perPage ,
92+ page : page ,
93+ sort : "created" ,
94+ direction : "desc" ,
95+ } ) ;
96+
97+ if ( pageIssues . length === 0 ) {
98+ break ; // No more issues
99+ }
100+
101+ allIssues . push ( ...pageIssues ) ;
102+
103+ if ( pageIssues . length < perPage ) {
104+ break ; // Last page
105+ }
106+
107+ page ++ ;
108+ }
109+
110+ // Filter for Bug or Feature types, or bug/feature labels
111+ const filteredIssues = allIssues . filter ( ( issue : any ) => {
112+ // Exclude current issue and pull requests
113+ if ( issue . number === currentIssueNumber || issue . pull_request ) {
114+ return false ;
115+ }
116+
117+ // Check if issue has Bug or Feature type (type is an object with a name property)
118+ if ( issue . type && typeof issue . type === 'object' && issue . type . name ) {
119+ if ( issue . type . name === "Bug" || issue . type . name === "Feature" ) {
120+ return true ;
121+ }
122+ }
123+
124+ // Fallback: Check for bug or feature labels
125+ const labelNames = issue . labels . map ( ( l : any ) =>
126+ typeof l === "string" ? l . toLowerCase ( ) : ( l . name || "" ) . toLowerCase ( )
127+ ) ;
128+ return labelNames . includes ( "bug" ) || labelNames . includes ( "feature" ) ;
88129 } ) ;
89130
90- return issues
91- . filter (
92- ( issue ) =>
93- issue . number !== currentIssueNumber &&
94- ! issue . pull_request &&
95- new Date ( issue . created_at ) >= cutoffDate
96- )
97- . map ( ( issue ) => ( {
98- number : issue . number ,
99- title : issue . title ,
100- body : issue . body || "" ,
101- created_at : new Date ( issue . created_at ) ,
102- updated_at : new Date ( issue . updated_at ) ,
103- labels : issue . labels . map ( ( l ) =>
104- typeof l === "string" ? l : l . name || ""
105- ) ,
106- url : issue . html_url ,
107- state : issue . state ,
108- } ) ) ;
131+ const hasTypes = allIssues . some ( i => i . type && i . type . name ) ;
132+ const filterMethod = hasTypes
133+ ? "issue types (Bug/Feature)"
134+ : "labels (bug/feature)" ;
135+
136+ console . log (
137+ `Filtered ${ filteredIssues . length } issues with Bug/Feature type (from ${ allIssues . length } total) using ${ filterMethod } `
138+ ) ;
139+
140+ return filteredIssues . map ( ( issue : any ) => ( {
141+ number : issue . number ,
142+ title : issue . title ,
143+ body : issue . body || "" ,
144+ created_at : new Date ( issue . created_at ) ,
145+ updated_at : new Date ( issue . updated_at ) ,
146+ labels : issue . labels . map ( ( l : any ) =>
147+ typeof l === "string" ? l : l . name || ""
148+ ) ,
149+ url : issue . html_url ,
150+ state : issue . state ,
151+ } ) ) ;
109152 } catch ( error ) {
110153 console . error ( "Error fetching existing issues:" , error ) ;
111154 return [ ] ;
@@ -330,28 +373,30 @@ export function generateDuplicateComment(duplicates: DuplicateMatch[]): string {
330373 return "" ;
331374 }
332375
333- const header = `## Potential Duplicate Issues Detected
334-
335- This issue appears to be similar to the following existing issue(s):
376+ const DUPLICATE_CLOSE_DAYS = 3 ;
336377
337- ` ;
338-
339- const issueList = duplicates
378+ const duplicateList = duplicates
340379 . map (
341380 ( dup ) =>
342- `- [#${ dup . issue_number } : ${ dup . issue_title } ](${ dup . url } ) (${ (
381+ `\n - [#${ dup . issue_number } : ${ dup . issue_title } ](${ dup . url } ) (${ (
343382 dup . similarity_score * 100
344- ) . toFixed ( 0 ) } % similar)\n ${ dup . reasoning } `
383+ ) . toFixed ( 0 ) } % similar)`
345384 )
346- . join ( "\n\n" ) ;
385+ . join ( "" ) ;
386+
387+ const comment = `🤖 **Potential Duplicate Detected**
347388
348- const footer = `
389+ This issue appears to be similar to: ${ duplicateList }
349390
350- ---
391+ **What happens next?**
392+ - ⏰ This issue will be automatically closed in ${ DUPLICATE_CLOSE_DAYS } days
393+ - 🏷️ Remove the \`duplicate\` label if this is NOT a duplicate
394+ - 💬 Comment on the original issue if you have additional information
351395
352- If you believe this is not a duplicate, please provide additional details to help us understand the difference. A maintainer will review and remove the duplicate label if appropriate.` ;
396+ **Why is this marked as duplicate?**
397+ ${ duplicates [ 0 ] . reasoning } `;
353398
354- return header + issueList + footer ;
399+ return comment ;
355400}
356401
357402/**
0 commit comments