2 Commits

Author SHA1 Message Date
7becf99be2 Update .github/issue_template/ai-refinement.md
All checks were successful
Publish Library / Build NPM Project (push) Successful in 8s
Publish Library / Tag Version (push) Successful in 5s
2026-01-14 12:11:07 -05:00
99a1e55471 Merge pull request 'Check for duplicates before adding tickets' (#12) from ticket-duplicates into master
All checks were successful
Publish Library / Build NPM Project (push) Successful in 6s
Publish Library / Tag Version (push) Successful in 7s
Reviewed-on: #12
2025-12-31 00:02:18 -05:00
3 changed files with 28 additions and 34 deletions

View File

@@ -16,4 +16,4 @@ How can it be fixed or improved?
Steps to reproduce? Steps to reproduce?
Anything other useful information, logs or screenshots? Any other useful information? Logs, screenshots, steps to reproduce?

View File

@@ -1,6 +1,6 @@
{ {
"name": "@ztimson/ai-agents", "name": "@ztimson/ai-agents",
"version": "0.1.1", "version": "0.1.0",
"description": "AI agents", "description": "AI agents",
"keywords": ["ai", "review"], "keywords": ["ai", "review"],
"author": "ztimson", "author": "ztimson",

View File

@@ -6,8 +6,8 @@ import * as dotenv from 'dotenv';
import * as fs from 'node:fs'; import * as fs from 'node:fs';
import * as path from 'node:path'; import * as path from 'node:path';
dotenv.config({quiet: true, debug: false}); dotenv.config({quiet: true});
dotenv.config({path: '.env.local', override: true, quiet: true, debug: false}); dotenv.config({path: '.env.local', override: true, quiet: true});
(async () => { (async () => {
let p = process.argv[process.argv.length - 1]; let p = process.argv[process.argv.length - 1];
@@ -21,10 +21,6 @@ dotenv.config({path: '.env.local', override: true, quiet: true, debug: false});
ticket = process.env['TICKET'], ticket = process.env['TICKET'],
host = process.env['AI_HOST'], host = process.env['AI_HOST'],
model = process.env['AI_MODEL'], model = process.env['AI_MODEL'],
labelDupe = process.env['LABELS_DUPE'] || 'Review/Duplicate',
labelEnabled = process.env['LABEL_ENABLED'] || 'Review/AI',
labelsReq = process.env['LABELS_REQ'] || 'Kind/Aesthetic,Kind/Bug,Kind/DevOps,Kind/Document,Kind/Enhancement,Kind/Refactor,Kind/Security',
labelsOpt = process.env['LABELS_OPT'] || 'Breaking,Priority,QA',
token = process.env['AI_TOKEN']; token = process.env['AI_TOKEN'];
console.log(`Processing issue #${ticket}`); console.log(`Processing issue #${ticket}`);
@@ -36,13 +32,13 @@ dotenv.config({path: '.env.local', override: true, quiet: true, debug: false});
if(resp.ok) return resp.json(); if(resp.ok) return resp.json();
else throw new Error(`${resp.status} ${await resp.text()}`); else throw new Error(`${resp.status} ${await resp.text()}`);
}); });
if(issueData.labels?.length !== 1 || issueData.labels[0]?.name !== labelEnabled) { if(issueData.labels?.[0] !== 1 || issueData.labels?.[0]?.name !== 'Review/AI') {
console.log('Skipping'); console.log('Skipping');
return process.exit(); return process.exit();
} }
// Gather readme & template // Gather readme & template
let title = '', type = '', labels = [], readme = '', readmeP = path.join(process.cwd(), 'README.md'); let title = '', type = '', readme = '', readmeP = path.join(process.cwd(), 'README.md');
if(fs.existsSync(readmeP)) readme = fs.readFileSync(readmeP, 'utf-8'); if(fs.existsSync(readmeP)) readme = fs.readFileSync(readmeP, 'utf-8');
const template = p ? fs.readFileSync(p, 'utf-8') : `## Description const template = p ? fs.readFileSync(p, 'utf-8') : `## Description
@@ -100,27 +96,18 @@ Implementation details, constraints, dependencies, design decisions
args: {title: {type: 'string', description: 'Ticket title, must match format: Module - Verb noun', required: true}}, args: {title: {type: 'string', description: 'Ticket title, must match format: Module - Verb noun', required: true}},
fn: (args) => title = args.title fn: (args) => title = args.title
}, { }, {
name: 'add_label', name: 'type',
description: 'Add a label to the ticket', description: 'Set the ticket type, must be called EXACTLY ONCE',
args: {label: {type: 'string', description: 'Label name', required: true}}, args: {type: {type: 'string', description: 'Ticket type', enum: ['Bug', 'DevOps', 'Document', 'Enhancement', 'Refactor', 'Security'], required: true}},
fn: async (args) => { fn: (args) => type = args.type
labels.push(args.label);
fetch(`${git}/api/v1/repos/${owner}/${repo}/issues/${ticket}/labels`, {
method: 'POST',
headers: {'Authorization': `token ${auth}`, 'Content-Type': 'application/json'},
body: `{"labels":["${args.label}"]}`
}).then(async resp => { if(!resp.ok) throw new Error(`${resp.status} ${await resp.text()}`); });
}
}], }],
system: `Transform raw tickets into structured markdown following the template EXACTLY. system: `Transform raw tickets into structured markdown following the template EXACTLY.
**MANDATORY STEPS:** **MANDATORY STEPS:**
1. Call \`title\` tool EXACTLY ONCE in format: "[Module] - [Verb] [subject]" (example: Storage - fix file uploads) 1. Identify ticket type: Bug, DevOps, Document, Enhancement, Refactor, or Security
2. Identify one label from each group which best applies to the ticket: ${labelsReq.replace(',', ', ')} 2. Call \`type\` tool EXACTLY ONCE with the type from step 1
3. Call the \`add_label\` tool ONCE FOR EVERY LABEL identified in the previous step 3. Call \`title\` tool EXACTLY ONCE in format: "[Module] - [Verb] [subject]" (example: Storage - fix file uploads)
4. Filter the following labels to any that apply to this ticket: ${labelsOpt.replace(',', ', ')} 4. Output formatted markdown matching template structure below
5. Call the \`add_label\` tool ONCE FOR EVERY LABEL identified in the previous step
6. Output the new ticket description in formated markdown matching the following rules:
**TEMPLATE RULES:** **TEMPLATE RULES:**
- Use ## headers (match template exactly) - Use ## headers (match template exactly)
@@ -141,11 +128,11 @@ Implementation details, constraints, dependencies, design decisions
| **Total** | **0-15** | | **Total** | **0-15** |
**SCORING:** **SCORING:**
- Size: # of modules/layers/files affected - Size: # of modules/layers/files changed
- Complexity: Technical difficulty - Complexity: Technical difficulty
- Unknowns: Research/uncertainty needed - Unknowns: Research/uncertainty needed
**PROJECT README:** **README:**
\`\`\`markdown \`\`\`markdown
${readme.trim() || 'No README available'} ${readme.trim() || 'No README available'}
\`\`\` \`\`\`
@@ -178,11 +165,11 @@ Output ONLY markdown. No explanations, labels, or extra formatting.`});
let dupeId = null; let dupeId = null;
const dupeIds = search.map(t => t.id); const dupeIds = search.map(t => t.id);
const dupes = search.map(t => `ID: ${t.id}\nTitle: ${t.title}\n\`\`\`markdown\n${t.body}\n\`\`\``).join('\n\n'); const dupes = search.map(t => `ID: ${t.id}\nTitle: ${t.title}\n\`\`\`markdown\n${t.body}\n\`\`\``).join('\n\n');
const hasDuplicates = (await ai.language.ask(`ID: ${issueData.id}\nTitle: ${title}\n\`\`\`markdown\n${body}\n\`\`\``, { const hasDuplicates = (await ai.language.ask(`${title}\n\`\`\`markdown\n${body}\n\`\`\``, {
system: `Your job is to identify duplicates. Respond ONLY with the duplicate's ID number or "NONE" if no match exists\n\n${dupes}` system: `Your job is to identify duplicates. Respond with the ID number of the duplicate or nothing if there are no matches \n\n${dupes}`
}))?.pop()?.content; }))?.pop()?.content;
// Handle duplicates // Handle duplicates
if(hasDuplicates && hasDuplicates !== 'NONE' && (dupeId = dupeIds.find(id => id === hasDuplicates.trim()))) { if(!!hasDuplicates && (dupeId = dupeIds.find(id => new RegExp(`\\b${id}\\b`, 'm').test(hasDuplicates)))) {
await fetch(`${git}/api/v1/repos/${owner}/${repo}/issues/${ticket}/comments`, { await fetch(`${git}/api/v1/repos/${owner}/${repo}/issues/${ticket}/comments`, {
method: 'POST', method: 'POST',
headers: {'Authorization': `token ${auth}`, 'Content-Type': 'application/json'}, headers: {'Authorization': `token ${auth}`, 'Content-Type': 'application/json'},
@@ -191,7 +178,7 @@ Output ONLY markdown. No explanations, labels, or extra formatting.`});
await fetch(`${git}/api/v1/repos/${owner}/${repo}/issues/${ticket}/labels`, { await fetch(`${git}/api/v1/repos/${owner}/${repo}/issues/${ticket}/labels`, {
method: 'POST', method: 'POST',
headers: {'Authorization': `token ${auth}`, 'Content-Type': 'application/json'}, headers: {'Authorization': `token ${auth}`, 'Content-Type': 'application/json'},
body: `{"labels":["${labelDupe}"]}` body: '{"labels":["Reviewed/Duplicate"]}'
}).then(async resp => { if(!resp.ok) throw new Error(`${resp.status} ${await resp.text()}`); }); }).then(async resp => { if(!resp.ok) throw new Error(`${resp.status} ${await resp.text()}`); });
await fetch(`${git}/api/v1/repos/${owner}/${repo}/issues/${ticket}`, { await fetch(`${git}/api/v1/repos/${owner}/${repo}/issues/${ticket}`, {
method: 'PATCH', method: 'PATCH',
@@ -208,8 +195,15 @@ Output ONLY markdown. No explanations, labels, or extra formatting.`});
headers: {'Authorization': `token ${auth}`, 'Content-Type': 'application/json'}, headers: {'Authorization': `token ${auth}`, 'Content-Type': 'application/json'},
body: JSON.stringify({title, body}) body: JSON.stringify({title, body})
}).then(async resp => { if(!resp.ok) throw new Error(`${resp.status} ${await resp.text()}`); }); }).then(async resp => { if(!resp.ok) throw new Error(`${resp.status} ${await resp.text()}`); });
if(type) { // Label
await fetch(`${git}/api/v1/repos/${owner}/${repo}/issues/${ticket}/labels`, {
method: 'POST',
headers: {'Authorization': `token ${auth}`, 'Content-Type': 'application/json'},
body: `{"labels":["Reviewed/${type[0].toUpperCase() + type.slice(1).toLowerCase()}"]}`
}).then(async resp => { if(!resp.ok) throw new Error(`${resp.status} ${await resp.text()}`); });
}
console.log(`Title: ${title}\nLabels: ${labels.join(', ')}\nBody:\n${body}`); console.log(`Title: ${title}\nType: ${type}\nBody:\n${body}`);
})().catch(err => { })().catch(err => {
console.error(`Error: ${err.message || err.toString()}`); console.error(`Error: ${err.message || err.toString()}`);
process.exit(1); process.exit(1);