From 6bb78d0862bab1cd007bbb53db70d4fc1079711f Mon Sep 17 00:00:00 2001 From: ztimson Date: Tue, 30 Dec 2025 13:47:59 -0500 Subject: [PATCH 1/3] Added ticket refinement bot --- .github/issue_template/ai-refinement.md | 53 +++++++++ .../workflows/{review.yml => code-review.yml} | 1 + .github/workflows/ticket-refinement.yml | 26 +++++ package.json | 3 +- src/refine.mjs | 106 ++++++++++++++++++ 5 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 .github/issue_template/ai-refinement.md rename .github/workflows/{review.yml => code-review.yml} (99%) create mode 100644 .github/workflows/ticket-refinement.yml create mode 100644 src/refine.mjs diff --git a/.github/issue_template/ai-refinement.md b/.github/issue_template/ai-refinement.md new file mode 100644 index 0000000..f56a7a5 --- /dev/null +++ b/.github/issue_template/ai-refinement.md @@ -0,0 +1,53 @@ +--- + +name: "AI refinement" +about: "Use AI to refine ticket" +ref: "develop" +labels: +- Review/AI + +--- + +# [Module] - [Add/Change/Fix/Refactor/Remove] [Feature/Component] + +## Type: [Bug/DevOps/Enhancement/Refactor/Security] + +| | Score | +|------------|----------| +| Size | 0-5 | +| Complexity | 0-5 | +| Unknowns | 0-5 | +| **Total** | **0-15** | + +## Description + +A clear explanation of the issue, feature, or change needed + +## Current Behavior + +For bugs: what's happening now +For refactors: what exists today +For enhancements: current state/gap + +## Expected Behavior + +What should happen instead + +## Steps to Reproduce + +1. First step +2. Second step +3. Third step + +## Additional Context + +Logs, screenshots, links, related issues + +## Acceptance Criteria + +- [ ] Todo requirement +- [X] Completed requirement + +## Technical Notes + +Implementation details, constraints, dependencies, design decisions diff --git a/.github/workflows/review.yml b/.github/workflows/code-review.yml similarity index 99% rename from .github/workflows/review.yml rename to .github/workflows/code-review.yml index b312f9b..3af0034 100644 --- a/.github/workflows/review.yml +++ b/.github/workflows/code-review.yml @@ -1,4 +1,5 @@ name: Code review + on: pull_request: types: [opened, synchronize, reopened] diff --git a/.github/workflows/ticket-refinement.yml b/.github/workflows/ticket-refinement.yml new file mode 100644 index 0000000..ec95497 --- /dev/null +++ b/.github/workflows/ticket-refinement.yml @@ -0,0 +1,26 @@ +name: Ticket refinement + +on: + issues: + types: [opened, labeled] +jobs: + format: + runs-on: ubuntu-latest + container: node:22 + steps: + - name: Fetch code + run: | + git clone "$(echo ${{github.server_url}}/${{github.repository}}.git | sed s%://%://${{github.token}}@% )" . + git checkout ${{ github.event.repository.default_branch }} + + - name: Run AI Formatter + run: npx -y @ztimson/ai-agents@latest format .github/issue_templates/ai-refinement.md + env: + AI_HOST: anthropic + AI_MODEL: claude-sonnet-4-5 + AI_TOKEN: ${{ secrets.ANTHROPIC_TOKEN }} + GIT_HOST: ${{ github.server_url }} + GIT_OWNER: ${{ github.repository_owner }} + GIT_REPO: ${{ github.event.repository.name }} + GIT_TOKEN: ${{ secrets.ASSISTANT_TOKEN }} + TICKET: ${{ github.event.issue.number }} diff --git a/package.json b/package.json index 9d29b97..974e4f2 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,13 @@ { "name": "@ztimson/ai-agents", - "version": "0.0.3", + "version": "0.0.4", "description": "AI agents", "keywords": ["ai", "review"], "author": "ztimson", "license": "ISC", "type": "module", "bin": { + "refine": "./src/refine.mjs", "review": "./src/review.mjs" }, "dependencies": { diff --git a/src/refine.mjs b/src/refine.mjs new file mode 100644 index 0000000..83b1a1b --- /dev/null +++ b/src/refine.mjs @@ -0,0 +1,106 @@ +#!/usr/bin/env node + +import {Ai} from '@ztimson/ai-utils'; +import * as os from 'node:os'; +import * as dotenv from 'dotenv'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; + +dotenv.config({quiet: true}); +dotenv.config({path: '.env.local', override: true, quiet: true}); + +(async () => { + let p = process.argv[process.argv.length - 1]; + if(p === 'review' || p.endsWith('review.mjs')) p = null; + if(!/(\/|[A-Z]:)/.test(p)) p = path.join(process.cwd(), p); + + if(!p || !fs.existsSync(p)) throw new Error('Please provide a template'); + + const git = process.env['GIT_HOST'], + owner = process.env['GIT_OWNER'], + repo = process.env['GIT_REPO'], + auth = process.env['GIT_TOKEN'], + ticket = process.env['TICKET'], + host = process.env['AI_HOST'], + model = process.env['AI_MODEL'], + token = process.env['AI_TOKEN']; + + console.log(`Processing issue #${ticket}`); + + // Fetch issue + const issueRes = await fetch(`${git}/api/v1/repos/${owner}/${repo}/issues/${ticket}`, { + headers: {'Authorization': `token ${auth}`} + }); + if(!issueRes.ok) throw new Error(`${issueRes.status} ${await issueRes.text()}`); + const issueData = await issueRes.json(); + if(!issueData.labels?.some(l => l.name === 'Review/AI')) { + console.log('Skipping'); + return process.exit(); + } + + let readme = '', readmeP = path.join(process.cwd(), 'README.md'); + if(fs.existsSync(readmeP)) readme = fs.readFileSync(readmeP, 'utf-8'); + const template = fs.readFileSync(p, 'utf-8'); + + let options = {ollama: {model, host}}; + if(host === 'anthropic') options = {anthropic: {model, token}}; + else if(host === 'openai') options = {openAi: {model, token}}; + const ai = new Ai({ + ...options, + model: [host, model], + path: process.env['path'] || os.tmpdir(), + system: `You are a ticket formatter. Transform raw issue descriptions into structured tickets. + +**CRITICAL RULES:** +1. Identify the ticket type (Bug, DevOps, Enhancement, Refactor, Security) +2. Output MUST only contain the new ticket information in markdown, no extra fluff +3. Follow the template structure EXACTLY: + - Title format: [Module] - [Verb] [noun] + Example: Storage - Fix file uploads + - Fill in the identified ticket type + - Write a clear description + - For bugs: fill Steps to Reproduce with numbered list + - For enhancements/refactors: REMOVE the Steps to Reproduce section entirely + - Acceptance Criteria: convert requirements into checkboxes (- [ ]) + - Weight scoring (0-5 each): + * Size: Number of modules, layers & files affected by change + * Complexity: Technical difficulty to implement + * Unknowns: Research/uncertainty in work estimation + * Calculate Total as sum of the three + - Remove sections that are not applicable based on ticket type + - Use proper markdown headers (##) + +**README:** +\`\`\`markdown +${readme.trim() || 'No README available'} +\`\`\` + +**TEMPLATE:** +\`\`\`markdown +${template.trim()} +\`\`\` + +Output ONLY the formatted ticket, no explanation.` + }); + + const messages = await ai.language.ask(`Title: ${issueData.title}\n\nDescription:\n${issueData.body || 'No description provided'}`); + const content = messages.pop().content; + const title = /^# (.+)$/m.exec(content)?.[1] || issueData.title; + const typeMatch = /^## Type:\s*(.+)$/m.exec(content); + const type = typeMatch?.[1]?.split('/')[0]?.trim() || 'Unassigned'; + const body = content.replace(/^# .+$/m, '').replace(/^## Type:.+$/m, '').trim(); + const updateRes = await fetch(`${git}/api/v1/repos/${owner}/${repo}/issues/${ticket}`, { + method: 'PATCH', + headers: { + 'Authorization': `token ${auth}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + title, + body, + labels: [`Type/${type[0].toUpperCase() + type.slice(1).toLowerCase()}`] + }) + }); + if(!updateRes.ok) throw new Error(`${updateRes.status} ${await updateRes.text()}`); + console.log(body); +})(); From 1f48b5a872e4d81dfade2ac10a5bcc364cb3911d Mon Sep 17 00:00:00 2001 From: ztimson Date: Tue, 30 Dec 2025 14:15:40 -0500 Subject: [PATCH 2/3] Fixed misc bugs --- .github/workflows/ticket-refinement.yml | 2 +- src/refine.mjs | 14 +++++++++----- src/review.mjs | 2 +- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ticket-refinement.yml b/.github/workflows/ticket-refinement.yml index ec95497..7f7088d 100644 --- a/.github/workflows/ticket-refinement.yml +++ b/.github/workflows/ticket-refinement.yml @@ -14,7 +14,7 @@ jobs: git checkout ${{ github.event.repository.default_branch }} - name: Run AI Formatter - run: npx -y @ztimson/ai-agents@latest format .github/issue_templates/ai-refinement.md + run: npx -y @ztimson/ai-agents@latest format .github/issue_template/ai-refinement.md env: AI_HOST: anthropic AI_MODEL: claude-sonnet-4-5 diff --git a/src/refine.mjs b/src/refine.mjs index 83b1a1b..96fafbd 100644 --- a/src/refine.mjs +++ b/src/refine.mjs @@ -11,7 +11,7 @@ dotenv.config({path: '.env.local', override: true, quiet: true}); (async () => { let p = process.argv[process.argv.length - 1]; - if(p === 'review' || p.endsWith('review.mjs')) p = null; + if(p === 'refine' || p.endsWith('refine.mjs')) p = null; if(!/(\/|[A-Z]:)/.test(p)) p = path.join(process.cwd(), p); if(!p || !fs.existsSync(p)) throw new Error('Please provide a template'); @@ -81,10 +81,14 @@ ${template.trim()} \`\`\` Output ONLY the formatted ticket, no explanation.` - }); + }) - const messages = await ai.language.ask(`Title: ${issueData.title}\n\nDescription:\n${issueData.body || 'No description provided'}`); - const content = messages.pop().content; + const messages = await ai.language.ask(`Title: ${issueData.title}\n\nDescription:\n${issueData.body || 'No description provided'}`).catch(() => []);; + const content = messages?.pop()?.content; + if(!content) { + console.log('Invalid response from AI'); + return process.exit(1); + } const title = /^# (.+)$/m.exec(content)?.[1] || issueData.title; const typeMatch = /^## Type:\s*(.+)$/m.exec(content); const type = typeMatch?.[1]?.split('/')[0]?.trim() || 'Unassigned'; @@ -98,7 +102,7 @@ Output ONLY the formatted ticket, no explanation.` body: JSON.stringify({ title, body, - labels: [`Type/${type[0].toUpperCase() + type.slice(1).toLowerCase()}`] + labels: type?.length ? [`Kind/${type[0].toUpperCase() + type.slice(1).toLowerCase()}`] : [] }) }); if(!updateRes.ok) throw new Error(`${updateRes.status} ${await updateRes.text()}`); diff --git a/src/review.mjs b/src/review.mjs index 502a94f..b05b2ae 100644 --- a/src/review.mjs +++ b/src/review.mjs @@ -37,7 +37,7 @@ dotenv.config({path: '.env.local', override: true, quiet: true, debug: false}); ...options, model: [host, model], path: process.env['path'] || os.tmpdir(), - system: `You are a code reviewer. Analyze the git diff and use the \`recommend\` tool for EACH issue you find. You must call \`recommend\` exactly once for every bug or improvement opportunity directly related to changes. Ignore formatting recommendations. After making all recommendations, provide one brief bullet-point summary.`, + system: `You are a code reviewer. Analyze the git diff and use the \`recommend\` tool for EACH issue you find. You must call \`recommend\` exactly once for every bug or improvement opportunity directly related to changes. Ignore formatting recommendations. After making all recommendations, provide some concluding remarks about the overall state of the changes.`, tools: [{ name: 'read_file', description: 'Read contents of a file', From af09bd0f5339cb5a9f9cf9a2f3c11df375cf1f14 Mon Sep 17 00:00:00 2001 From: ztimson Date: Tue, 30 Dec 2025 15:18:02 -0500 Subject: [PATCH 3/3] Small issue refiner fixes --- .github/workflows/ticket-refinement.yml | 2 +- src/refine.mjs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ticket-refinement.yml b/.github/workflows/ticket-refinement.yml index 7f7088d..87a9fc9 100644 --- a/.github/workflows/ticket-refinement.yml +++ b/.github/workflows/ticket-refinement.yml @@ -14,7 +14,7 @@ jobs: git checkout ${{ github.event.repository.default_branch }} - name: Run AI Formatter - run: npx -y @ztimson/ai-agents@latest format .github/issue_template/ai-refinement.md + run: npx -y @ztimson/ai-agents@latest refine .github/issue_template/ai-refinement.md env: AI_HOST: anthropic AI_MODEL: claude-sonnet-4-5 diff --git a/src/refine.mjs b/src/refine.mjs index 96fafbd..bedcfa2 100644 --- a/src/refine.mjs +++ b/src/refine.mjs @@ -12,7 +12,7 @@ dotenv.config({path: '.env.local', override: true, quiet: true}); (async () => { let p = process.argv[process.argv.length - 1]; if(p === 'refine' || p.endsWith('refine.mjs')) p = null; - if(!/(\/|[A-Z]:)/.test(p)) p = path.join(process.cwd(), p); + if(!/^(\/|[A-Z]:)/m.test(p)) p = path.join(process.cwd(), p); if(!p || !fs.existsSync(p)) throw new Error('Please provide a template'); @@ -83,7 +83,7 @@ ${template.trim()} Output ONLY the formatted ticket, no explanation.` }) - const messages = await ai.language.ask(`Title: ${issueData.title}\n\nDescription:\n${issueData.body || 'No description provided'}`).catch(() => []);; + const messages = await ai.language.ask(`Title: ${issueData.title}\n\nDescription:\n${issueData.body || 'No description provided'}`).catch(() => []); const content = messages?.pop()?.content; if(!content) { console.log('Invalid response from AI');