gitpadi 2.0.7 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/.gitlab/duo/chat-rules.md +40 -0
  2. package/.gitlab/duo/mr-review-instructions.md +44 -0
  3. package/.gitlab-ci.yml +136 -0
  4. package/README.md +585 -57
  5. package/action.yml +21 -2
  6. package/dist/applicant-scorer.js +27 -105
  7. package/dist/cli.js +1040 -34
  8. package/dist/commands/apply-for-issue.js +396 -0
  9. package/dist/commands/bounty-hunter.js +441 -0
  10. package/dist/commands/contribute.js +245 -51
  11. package/dist/commands/gitlab-issues.js +87 -0
  12. package/dist/commands/gitlab-mrs.js +163 -0
  13. package/dist/commands/gitlab-pipelines.js +95 -0
  14. package/dist/commands/prs.js +3 -3
  15. package/dist/core/github.js +24 -0
  16. package/dist/core/gitlab.js +233 -0
  17. package/dist/gitlab-agents/ci-recovery-agent.js +173 -0
  18. package/dist/gitlab-agents/contributor-scoring-agent.js +159 -0
  19. package/dist/gitlab-agents/grade-assignment-agent.js +252 -0
  20. package/dist/gitlab-agents/mr-review-agent.js +200 -0
  21. package/dist/gitlab-agents/reminder-agent.js +164 -0
  22. package/dist/grade-assignment.js +262 -0
  23. package/dist/remind-contributors.js +127 -0
  24. package/dist/review-and-merge.js +125 -0
  25. package/examples/gitpadi.yml +152 -0
  26. package/package.json +20 -4
  27. package/src/applicant-scorer.ts +33 -141
  28. package/src/cli.ts +1073 -33
  29. package/src/commands/apply-for-issue.ts +452 -0
  30. package/src/commands/bounty-hunter.ts +529 -0
  31. package/src/commands/contribute.ts +264 -50
  32. package/src/commands/gitlab-issues.ts +87 -0
  33. package/src/commands/gitlab-mrs.ts +185 -0
  34. package/src/commands/gitlab-pipelines.ts +104 -0
  35. package/src/commands/prs.ts +3 -3
  36. package/src/core/github.ts +24 -0
  37. package/src/core/gitlab.ts +397 -0
  38. package/src/gitlab-agents/ci-recovery-agent.ts +201 -0
  39. package/src/gitlab-agents/contributor-scoring-agent.ts +196 -0
  40. package/src/gitlab-agents/grade-assignment-agent.ts +275 -0
  41. package/src/gitlab-agents/mr-review-agent.ts +231 -0
  42. package/src/gitlab-agents/reminder-agent.ts +203 -0
  43. package/src/grade-assignment.ts +283 -0
  44. package/src/remind-contributors.ts +159 -0
  45. package/src/review-and-merge.ts +143 -0
@@ -0,0 +1,127 @@
1
+ import { initGitHub, getOctokit, getOwner, getRepo } from './core/github.js';
2
+ import chalk from 'chalk';
3
+ // Tiered Escalation Thresholds (in hours)
4
+ const TIER_1_HOURS = 24; // Gentle reminder: "Please create a draft PR"
5
+ const TIER_2_HOURS = 48; // Warning: "Will be unassigned in 24h"
6
+ const TIER_3_HOURS = 72; // Auto-unassign
7
+ // Signature markers for each tier (prevents duplicate comments)
8
+ const SIG_TIER_1 = '<!-- gitpadi-reminder-24h -->';
9
+ const SIG_TIER_2 = '<!-- gitpadi-reminder-48h -->';
10
+ const SIG_TIER_3 = '<!-- gitpadi-unassigned -->';
11
+ async function run() {
12
+ console.log(chalk.bold('\nšŸš€ GitPadi Escalating Reminder Engine\n'));
13
+ try {
14
+ initGitHub();
15
+ const octokit = getOctokit();
16
+ const owner = getOwner();
17
+ const repo = getRepo();
18
+ if (!owner || !repo) {
19
+ console.error(chalk.red('āŒ Owner or Repo not found in environment/config.'));
20
+ process.exit(1);
21
+ }
22
+ console.log(chalk.dim(`šŸ”Ž Scanning ${owner}/${repo} for inactive contributors...\n`));
23
+ // 1. Fetch all open issues
24
+ const { data: issues } = await octokit.issues.listForRepo({
25
+ owner, repo, state: 'open', per_page: 100
26
+ });
27
+ const issuesWithAssignees = issues.filter(i => i.assignees && i.assignees.length > 0 && !i.pull_request);
28
+ if (issuesWithAssignees.length === 0) {
29
+ console.log(chalk.green('āœ… No open issues with assignees found.'));
30
+ return;
31
+ }
32
+ const now = new Date();
33
+ let remindCount = 0, warnCount = 0, unassignCount = 0;
34
+ for (const issue of issuesWithAssignees) {
35
+ const issueNumber = issue.number;
36
+ console.log(chalk.cyan(` ā–ø Issue #${issueNumber}: "${issue.title.substring(0, 50)}..."`));
37
+ // 2. Check for existing reminder comments
38
+ const { data: comments } = await octokit.issues.listComments({
39
+ owner, repo, issue_number: issueNumber
40
+ });
41
+ const hasTier1 = comments.some(c => c.body?.includes(SIG_TIER_1));
42
+ const hasTier2 = comments.some(c => c.body?.includes(SIG_TIER_2));
43
+ const hasTier3 = comments.some(c => c.body?.includes(SIG_TIER_3));
44
+ if (hasTier3) {
45
+ console.log(chalk.dim(` ā© Already processed (unassigned). Skipping.`));
46
+ continue;
47
+ }
48
+ // 3. Check for linked draft/open PRs
49
+ const { data: prs } = await octokit.pulls.list({
50
+ owner, repo, state: 'open', per_page: 100
51
+ });
52
+ const linkedPR = prs.find(pr => pr.body?.includes(`#${issueNumber}`) ||
53
+ pr.head.ref.includes(`issue-${issueNumber}`));
54
+ if (linkedPR) {
55
+ console.log(chalk.green(` āœ… PR found: #${linkedPR.number} (${linkedPR.draft ? 'draft' : 'open'}). Skipping.`));
56
+ continue;
57
+ }
58
+ // 4. Find when the user was assigned
59
+ const { data: events } = await octokit.issues.listEvents({
60
+ owner, repo, issue_number: issueNumber
61
+ });
62
+ const assignmentEvent = [...events].reverse().find(e => e.event === 'assigned');
63
+ if (!assignmentEvent) {
64
+ console.log(chalk.dim(` ā“ No assignment event found. Skipping.`));
65
+ continue;
66
+ }
67
+ const assignedAt = new Date(assignmentEvent.created_at);
68
+ const diffHours = Math.floor((now.getTime() - assignedAt.getTime()) / (1000 * 60 * 60));
69
+ const assignee = assignmentEvent.assignee?.login;
70
+ console.log(chalk.dim(` šŸ•’ Assigned ${diffHours}h ago (@${assignee})`));
71
+ // 5. Tiered escalation
72
+ if (diffHours >= TIER_3_HOURS && !hasTier3) {
73
+ // ── TIER 3: Auto-unassign ──
74
+ const message = `ā° Hi @${assignee}, it's been **${diffHours} hours** since you were assigned to this issue, and no draft PR has been created.
75
+
76
+ To keep the project moving, we're **unassigning you** from this issue. You're welcome to re-claim it anytime!
77
+
78
+ ${SIG_TIER_3}`;
79
+ await octokit.issues.createComment({
80
+ owner, repo, issue_number: issueNumber, body: message
81
+ });
82
+ await octokit.issues.removeAssignees({
83
+ owner, repo, issue_number: issueNumber,
84
+ assignees: [assignee]
85
+ });
86
+ console.log(chalk.red(` šŸ”“ TIER 3: @${assignee} unassigned after ${diffHours}h.`));
87
+ unassignCount++;
88
+ }
89
+ else if (diffHours >= TIER_2_HOURS && !hasTier2) {
90
+ // ── TIER 2: Warning ──
91
+ const message = `āš ļø Hi @${assignee}, it's been **${diffHours} hours** since you were assigned to this issue.
92
+
93
+ We haven't seen a draft PR yet. Please create one to confirm you're actively working on this — otherwise, you may be **unassigned in 24 hours** to keep things moving.
94
+
95
+ ${SIG_TIER_2}`;
96
+ await octokit.issues.createComment({
97
+ owner, repo, issue_number: issueNumber, body: message
98
+ });
99
+ console.log(chalk.yellow(` 🟔 TIER 2: Warning sent to @${assignee} (${diffHours}h).`));
100
+ warnCount++;
101
+ }
102
+ else if (diffHours >= TIER_1_HOURS && !hasTier1) {
103
+ // ── TIER 1: Gentle reminder ──
104
+ const message = `šŸ‘‹ Hi @${assignee}, you've been assigned to this issue for **${diffHours} hours**.
105
+
106
+ Just a friendly nudge — please create a **draft PR** when you start working, so we know you're on it! šŸš€
107
+
108
+ ${SIG_TIER_1}`;
109
+ await octokit.issues.createComment({
110
+ owner, repo, issue_number: issueNumber, body: message
111
+ });
112
+ console.log(chalk.green(` 🟢 TIER 1: Gentle reminder sent to @${assignee} (${diffHours}h).`));
113
+ remindCount++;
114
+ }
115
+ else {
116
+ console.log(chalk.dim(` ā³ Within threshold (${diffHours}h). No action needed.`));
117
+ }
118
+ }
119
+ console.log(chalk.bold(`\n✨ Reminder sweep complete.`));
120
+ console.log(chalk.dim(` šŸ“Š Reminded: ${remindCount} | Warned: ${warnCount} | Unassigned: ${unassignCount}\n`));
121
+ }
122
+ catch (error) {
123
+ console.error(chalk.red(`\nāŒ Error during reminder sweep: ${error.message}`));
124
+ process.exit(1);
125
+ }
126
+ }
127
+ run();
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env node
2
+ // review-and-merge.ts — Reviews a PR, checks CI, and auto-merges if all pass
3
+ //
4
+ // Usage via Action: action: review-and-merge
5
+ // Usage via CLI: npx tsx src/review-and-merge.ts
6
+ import { Octokit } from '@octokit/rest';
7
+ import chalk from 'chalk';
8
+ const TOKEN = process.env.GITHUB_TOKEN || '';
9
+ const OWNER = process.env.GITHUB_OWNER || '';
10
+ const REPO = process.env.GITHUB_REPO || '';
11
+ const PR_NUMBER = parseInt(process.env.PR_NUMBER || '0');
12
+ const AUTO_MERGE = process.env.AUTO_MERGE !== 'false'; // default: true
13
+ const octokit = new Octokit({ auth: TOKEN });
14
+ async function main() {
15
+ if (!PR_NUMBER) {
16
+ console.error('āŒ PR_NUMBER is required');
17
+ process.exit(1);
18
+ }
19
+ console.log(`\nšŸ¤– GitPadi Review & Merge — ${OWNER}/${REPO} #${PR_NUMBER}\n`);
20
+ const { data: pr } = await octokit.pulls.get({ owner: OWNER, repo: REPO, pull_number: PR_NUMBER });
21
+ const checks = [];
22
+ // ── 1. PR Size Check ──────────────────────────────────────────────
23
+ const { data: files } = await octokit.pulls.listFiles({ owner: OWNER, repo: REPO, pull_number: PR_NUMBER });
24
+ const totalChanges = files.reduce((sum, f) => sum + f.additions + f.deletions, 0);
25
+ if (totalChanges > 1000) {
26
+ checks.push({ name: 'PR Size', status: 'āŒ', detail: `${totalChanges} lines. Consider splitting.` });
27
+ }
28
+ else if (totalChanges > 500) {
29
+ checks.push({ name: 'PR Size', status: 'āš ļø', detail: `${totalChanges} lines. Large PR.` });
30
+ }
31
+ else {
32
+ checks.push({ name: 'PR Size', status: 'āœ…', detail: `${totalChanges} lines changed.` });
33
+ }
34
+ // ── 2. Linked Issues ──────────────────────────────────────────────
35
+ const hasIssueRef = /(?:fixes|closes|resolves)\s+#\d+/i.test(pr.body || '');
36
+ checks.push({
37
+ name: 'Linked Issues',
38
+ status: hasIssueRef ? 'āœ…' : 'āš ļø',
39
+ detail: hasIssueRef ? 'Issue reference found' : 'No issue reference (Fixes #N)',
40
+ });
41
+ // ── 3. Test Files ─────────────────────────────────────────────────
42
+ const srcFiles = files.filter(f => !f.filename.includes('test') && !f.filename.includes('spec') && (f.filename.endsWith('.ts') || f.filename.endsWith('.rs') || f.filename.endsWith('.js')));
43
+ const testFiles = files.filter(f => f.filename.includes('test') || f.filename.includes('spec'));
44
+ if (srcFiles.length > 0 && testFiles.length === 0) {
45
+ checks.push({ name: 'Test Coverage', status: 'āš ļø', detail: 'No test files changed.' });
46
+ }
47
+ else {
48
+ checks.push({ name: 'Test Coverage', status: 'āœ…', detail: `${testFiles.length} test file(s) included.` });
49
+ }
50
+ // ── 4. CI Status ──────────────────────────────────────────────────
51
+ const sha = pr.head.sha;
52
+ const { data: ciChecks } = await octokit.checks.listForRef({ owner: OWNER, repo: REPO, ref: sha });
53
+ const { data: statuses } = await octokit.repos.getCombinedStatusForRef({ owner: OWNER, repo: REPO, ref: sha });
54
+ const ciPassed = statuses.state === 'success' || (ciChecks.total_count > 0 && ciChecks.check_runs.every(c => c.conclusion === 'success'));
55
+ const ciPending = statuses.state === 'pending' || ciChecks.check_runs.some(c => c.status !== 'completed');
56
+ const ciFailed = statuses.state === 'failure' || ciChecks.check_runs.some(c => c.conclusion === 'failure');
57
+ if (ciFailed) {
58
+ const failedNames = ciChecks.check_runs.filter(c => c.conclusion === 'failure').map(c => c.name).join(', ');
59
+ checks.push({ name: 'CI Status', status: 'āŒ', detail: `Failed: ${failedNames || 'status checks'}` });
60
+ }
61
+ else if (ciPending) {
62
+ checks.push({ name: 'CI Status', status: 'āš ļø', detail: 'CI is still running...' });
63
+ }
64
+ else {
65
+ checks.push({ name: 'CI Status', status: 'āœ…', detail: 'All checks passed.' });
66
+ }
67
+ // ── Build Review Comment ──────────────────────────────────────────
68
+ const hasCritical = checks.some(c => c.status === 'āŒ');
69
+ const hasWarning = checks.some(c => c.status === 'āš ļø');
70
+ const headerEmoji = hasCritical ? '🚨' : hasWarning ? 'āš ļø' : 'āœ…';
71
+ let body = `## ${headerEmoji} GitPadi — Automated PR Review\n\n`;
72
+ body += `| Check | Status | Details |\n|-------|--------|--------|\n`;
73
+ checks.forEach(c => { body += `| ${c.name} | ${c.status} | ${c.detail} |\n`; });
74
+ body += `\n`;
75
+ if (ciFailed) {
76
+ body += `> 🚨 **CI Failed.** Please fix the failing checks and push again. Use \`gitpadi\` → \`Fix & Re-push\` for a guided workflow.\n`;
77
+ }
78
+ else if (hasCritical) {
79
+ body += `> 🚨 **Action Required:** Critical issues found. Please address before merging.\n`;
80
+ }
81
+ else if (ciPassed && !hasCritical) {
82
+ body += `> āœ… **All checks passed.** ${AUTO_MERGE ? 'Auto-merging...' : 'Ready for manual merge.'}\n`;
83
+ }
84
+ body += `\n---\n_Review by [GitPadi](https://github.com/Netwalls/contributor-agent) šŸ¤–_`;
85
+ // Post the review comment
86
+ await octokit.issues.createComment({ owner: OWNER, repo: REPO, issue_number: PR_NUMBER, body });
87
+ console.log('āœ… Posted review comment');
88
+ // ── Auto-Merge Logic ──────────────────────────────────────────────
89
+ if (ciPassed && !hasCritical && AUTO_MERGE) {
90
+ console.log('šŸ”€ CI passed. Attempting auto-merge...');
91
+ try {
92
+ await octokit.pulls.merge({
93
+ owner: OWNER, repo: REPO, pull_number: PR_NUMBER,
94
+ merge_method: 'squash',
95
+ commit_title: `${pr.title} (#${PR_NUMBER})`,
96
+ });
97
+ console.log(chalk.green(`āœ… PR #${PR_NUMBER} merged successfully!`));
98
+ // Close any linked issues (handles non-default-branch merges too)
99
+ const issueRefs = (pr.body || '').matchAll(/(?:fixes|closes|resolves)\s+#(\d+)/gi);
100
+ for (const match of issueRefs) {
101
+ const issueNumber = parseInt(match[1]);
102
+ try {
103
+ await octokit.issues.update({ owner: OWNER, repo: REPO, issue_number: issueNumber, state: 'closed' });
104
+ await octokit.issues.createComment({
105
+ owner: OWNER, repo: REPO, issue_number: issueNumber,
106
+ body: `āœ… Closed by PR #${PR_NUMBER} — merged via GitPadi šŸ¤–`,
107
+ });
108
+ console.log(`āœ… Closed linked issue #${issueNumber}`);
109
+ }
110
+ catch {
111
+ console.log(`āš ļø Could not close issue #${issueNumber}`);
112
+ }
113
+ }
114
+ }
115
+ catch (e) {
116
+ console.log(chalk.yellow(`āš ļø Auto-merge failed: ${e.message}`));
117
+ }
118
+ }
119
+ else if (ciFailed) {
120
+ console.log(chalk.red(`āŒ CI failed. Contributor notified to fix.`));
121
+ process.exit(1);
122
+ }
123
+ checks.forEach(c => console.log(` ${c.status} ${c.name}: ${c.detail}`));
124
+ }
125
+ main().catch((e) => { console.error('Fatal:', e); process.exit(1); });
@@ -0,0 +1,152 @@
1
+ # ─────────────────────────────────────────────────────────────────────────────
2
+ # GitPadi — Drop-in GitHub Automation
3
+ # Copy this file to .github/workflows/gitpadi.yml in your repo.
4
+ #
5
+ # What this does automatically:
6
+ # āœ… Reviews every PR (size, tests, security, linked issues)
7
+ # āœ… Scores contributors when they apply on issues + assigns the best one
8
+ # āœ… Sends escalating reminders for stale PRs (24h → 48h → 72h)
9
+ # āœ… Grades assignment submissions and auto-merges passing ones
10
+ # āœ… Closes linked issues when PRs are merged
11
+ #
12
+ # Optional manual triggers:
13
+ # - Bulk create issues from a JSON/MD file
14
+ # - Force review-and-merge any PR
15
+ #
16
+ # Setup:
17
+ # No secrets needed — uses the built-in GITHUB_TOKEN.
18
+ # For assignment grading, set GITPADI_ASSIGNMENT_MODE: true (see below).
19
+ # ─────────────────────────────────────────────────────────────────────────────
20
+
21
+ name: GitPadi
22
+
23
+ on:
24
+ # Fires on every PR open, update, or reopen
25
+ pull_request:
26
+ types: [opened, synchronize, reopened]
27
+
28
+ # Fires when someone comments on an issue (contributor applications)
29
+ issue_comment:
30
+ types: [created]
31
+
32
+ # Fires daily at 9am UTC — sends reminders for stale PRs
33
+ schedule:
34
+ - cron: '0 9 * * *'
35
+
36
+ # Manual triggers — run from Actions tab
37
+ workflow_dispatch:
38
+ inputs:
39
+ action:
40
+ description: 'Action to run manually'
41
+ required: true
42
+ type: choice
43
+ options:
44
+ - review-and-merge
45
+ - create-issues
46
+ - remind-contributors
47
+ pr_number:
48
+ description: 'PR number (for review-and-merge)'
49
+ required: false
50
+ issues_file:
51
+ description: 'Path to issues file (for create-issues)'
52
+ required: false
53
+ default: 'issues.json'
54
+
55
+ permissions:
56
+ contents: write # needed to merge PRs
57
+ issues: write # needed to assign, close, comment on issues
58
+ pull-requests: write # needed to review and comment on PRs
59
+
60
+ jobs:
61
+
62
+ # ── Review every PR ──────────────────────────────────────────────────────
63
+ review-pr:
64
+ name: Review PR
65
+ runs-on: ubuntu-latest
66
+ if: github.event_name == 'pull_request'
67
+ steps:
68
+ - uses: actions/checkout@v4
69
+ - uses: Netwalls/contributor-agent@main
70
+ with:
71
+ action: review-pr
72
+ github-token: ${{ secrets.GITHUB_TOKEN }}
73
+
74
+ # ── Grade assignment PRs (only if GITPADI_ASSIGNMENT_MODE is enabled) ───
75
+ # To enable: add GITPADI_ASSIGNMENT_MODE = true to your repo Variables
76
+ # (Settings → Secrets and variables → Actions → Variables)
77
+ grade-assignment:
78
+ name: Grade Assignment
79
+ runs-on: ubuntu-latest
80
+ if: |
81
+ github.event_name == 'pull_request' &&
82
+ vars.GITPADI_ASSIGNMENT_MODE == 'true'
83
+ steps:
84
+ - uses: actions/checkout@v4
85
+ - uses: Netwalls/contributor-agent@main
86
+ with:
87
+ action: grade-assignment
88
+ github-token: ${{ secrets.GITHUB_TOKEN }}
89
+ pass-threshold: '40' # score out of 100 needed to auto-merge
90
+ auto-merge: 'true'
91
+
92
+ # ── Score contributors when they apply on issues ─────────────────────────
93
+ # Triggered when someone comments "I want to work on this" (or similar)
94
+ # on any issue. Scores applicant + auto-assigns the best candidate.
95
+ score-applicant:
96
+ name: Score Contributor
97
+ runs-on: ubuntu-latest
98
+ if: |
99
+ github.event_name == 'issue_comment' &&
100
+ github.event.issue.pull_request == null
101
+ steps:
102
+ - uses: actions/checkout@v4
103
+ - uses: Netwalls/contributor-agent@main
104
+ with:
105
+ action: score-applicant
106
+ github-token: ${{ secrets.GITHUB_TOKEN }}
107
+ # Uncomment to notify a maintainer on every new applicant:
108
+ # notify-user: 'your-github-username'
109
+
110
+ # ── Daily reminders for stale PRs ────────────────────────────────────────
111
+ remind-contributors:
112
+ name: Remind Contributors
113
+ runs-on: ubuntu-latest
114
+ if: github.event_name == 'schedule'
115
+ steps:
116
+ - uses: actions/checkout@v4
117
+ - uses: Netwalls/contributor-agent@main
118
+ with:
119
+ action: remind-contributors
120
+ github-token: ${{ secrets.GITHUB_TOKEN }}
121
+
122
+ # ── Manual: review + merge a specific PR ─────────────────────────────────
123
+ review-and-merge:
124
+ name: Review & Merge PR
125
+ runs-on: ubuntu-latest
126
+ if: |
127
+ github.event_name == 'workflow_dispatch' &&
128
+ github.event.inputs.action == 'review-and-merge'
129
+ env:
130
+ PR_NUMBER: ${{ github.event.inputs.pr_number }}
131
+ steps:
132
+ - uses: actions/checkout@v4
133
+ - uses: Netwalls/contributor-agent@main
134
+ with:
135
+ action: review-and-merge
136
+ github-token: ${{ secrets.GITHUB_TOKEN }}
137
+ auto-merge: 'true'
138
+
139
+ # ── Manual: bulk create issues from a file ───────────────────────────────
140
+ create-issues:
141
+ name: Create Issues
142
+ runs-on: ubuntu-latest
143
+ if: |
144
+ github.event_name == 'workflow_dispatch' &&
145
+ github.event.inputs.action == 'create-issues'
146
+ steps:
147
+ - uses: actions/checkout@v4
148
+ - uses: Netwalls/contributor-agent@main
149
+ with:
150
+ action: create-issues
151
+ github-token: ${{ secrets.GITHUB_TOKEN }}
152
+ issues-file: ${{ github.event.inputs.issues_file }}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "gitpadi",
3
- "version": "2.0.7",
4
- "description": "GitPadi — Your AI-powered GitHub management CLI. Create repos, manage issues & PRs, score contributors, and automate everything.",
3
+ "version": "2.1.1",
4
+ "description": "GitPadi — AI-powered GitHub & GitLab management CLI. Fork repos, manage issues & PRs, score contributors, grade assignments, and automate everything. Powered by Anthropic Claude via GitLab Duo Agent Platform.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "gitpadi": "./dist/cli.js"
@@ -10,6 +10,8 @@
10
10
  "dist/",
11
11
  "src/",
12
12
  "action.yml",
13
+ ".gitlab-ci.yml",
14
+ ".gitlab/",
13
15
  "examples/",
14
16
  "README.md",
15
17
  "LICENSE"
@@ -21,21 +23,35 @@
21
23
  "dev": "tsx watch src/cli.ts",
22
24
  "create-issues": "tsx src/create-issues.ts",
23
25
  "review-pr": "tsx src/pr-review.ts",
24
- "score-applicant": "tsx src/applicant-scorer.ts"
26
+ "score-applicant": "tsx src/applicant-scorer.ts",
27
+ "grade-assignment": "tsx src/grade-assignment.ts",
28
+ "gl:score": "tsx src/gitlab-agents/contributor-scoring-agent.ts",
29
+ "gl:review": "tsx src/gitlab-agents/mr-review-agent.ts",
30
+ "gl:recover": "tsx src/gitlab-agents/ci-recovery-agent.ts",
31
+ "gl:remind": "tsx src/gitlab-agents/reminder-agent.ts",
32
+ "gl:grade": "tsx src/gitlab-agents/grade-assignment-agent.ts"
25
33
  },
26
34
  "keywords": [
27
35
  "github",
36
+ "gitlab",
28
37
  "cli",
29
38
  "github-action",
39
+ "gitlab-ci",
30
40
  "issue-management",
31
41
  "pr-review",
42
+ "merge-request",
32
43
  "contributor-scoring",
33
44
  "automation",
45
+ "ai-agent",
46
+ "anthropic",
47
+ "claude",
48
+ "gitlab-duo",
34
49
  "gitpadi"
35
50
  ],
36
51
  "author": "Netwalls",
37
52
  "license": "MIT",
38
53
  "dependencies": {
54
+ "@anthropic-ai/sdk": "^0.40.1",
39
55
  "@octokit/rest": "^21.1.1",
40
56
  "boxen": "^8.0.1",
41
57
  "chalk": "^5.4.1",
@@ -52,4 +68,4 @@
52
68
  "@types/node": "^22.10.5",
53
69
  "typescript": "^5.9.3"
54
70
  }
55
- }
71
+ }
@@ -6,7 +6,14 @@
6
6
  // Usage via Action: action: score-applicant
7
7
  // Usage (CLI): GITHUB_TOKEN=xxx ISSUE_NUMBER=5 COMMENT_ID=123 npx tsx src/applicant-scorer.ts
8
8
 
9
- import { Octokit } from '@octokit/rest';
9
+ import { initGitHub, getOctokit } from './core/github.js';
10
+ import {
11
+ isApplicationComment,
12
+ fetchProfile,
13
+ scoreApplicant,
14
+ TIER_EMOJI,
15
+ type ScoredApplicant,
16
+ } from './core/scorer.js';
10
17
 
11
18
  // ── Config ─────────────────────────────────────────────────────────────
12
19
  const GITHUB_TOKEN = process.env.GITHUB_TOKEN!;
@@ -18,148 +25,10 @@ const NOTIFY_USER = process.env.NOTIFY_USER || '';
18
25
 
19
26
  if (!GITHUB_TOKEN) { console.error('āŒ GITHUB_TOKEN required'); process.exit(1); }
20
27
 
21
- const octokit = new Octokit({ auth: GITHUB_TOKEN });
22
-
23
- // ── Application Detection ──────────────────────────────────────────────
24
- const APPLICATION_PATTERNS = [
25
- /i('d| would) (like|love|want) to (work on|tackle|take|pick up|handle)/i,
26
- /can i (work on|take|pick up|handle|be assigned)/i,
27
- /assign (this |it )?(to )?me/i,
28
- /i('m| am) interested/i,
29
- /let me (work on|take|handle)/i,
30
- /i('ll| will) (work on|take|handle|do)/i,
31
- /i want to contribute/i,
32
- /please assign/i,
33
- /i can (do|handle|take care of|work on)/i,
34
- /picking this up/i,
35
- /claiming this/i,
36
- ];
37
-
38
- function isApplicationComment(body: string): boolean {
39
- return APPLICATION_PATTERNS.some((p) => p.test(body));
40
- }
41
-
42
- // ── Types ──────────────────────────────────────────────────────────────
43
- interface ApplicantProfile {
44
- username: string;
45
- avatarUrl: string;
46
- accountAge: number;
47
- publicRepos: number;
48
- followers: number;
49
- totalContributions: number;
50
- repoContributions: number;
51
- relevantLanguages: string[];
52
- hasReadme: boolean;
53
- prsMerged: number;
54
- prsOpen: number;
55
- issuesCreated: number;
56
- commentBody: string;
57
- }
58
-
59
- interface ScoreBreakdown {
60
- accountMaturity: number; // 0-15
61
- repoExperience: number; // 0-30
62
- githubPresence: number; // 0-15
63
- activityLevel: number; // 0-15
64
- applicationQuality: number; // 0-15
65
- languageRelevance: number; // 0-10
66
- total: number;
67
- }
68
-
69
- interface ScoredApplicant extends ApplicantProfile {
70
- score: number;
71
- breakdown: ScoreBreakdown;
72
- tier: 'S' | 'A' | 'B' | 'C' | 'D';
73
- }
74
-
75
- // ── Fetch Profile ──────────────────────────────────────────────────────
76
- async function fetchProfile(username: string, commentBody: string): Promise<ApplicantProfile> {
77
- const { data: user } = await octokit.users.getByUsername({ username });
78
- const accountAge = Math.floor((Date.now() - new Date(user.created_at).getTime()) / 86400000);
79
-
80
- let totalContributions = 0;
81
- try {
82
- const { data: events } = await octokit.activity.listPublicEventsForUser({ username, per_page: 100 });
83
- totalContributions = events.length;
84
- } catch { /* continue */ }
85
-
86
- let prsMerged = 0, prsOpen = 0, issuesCreated = 0;
87
- try {
88
- const { data: mp } = await octokit.search.issuesAndPullRequests({ q: `repo:${OWNER}/${REPO} type:pr author:${username} is:merged` });
89
- prsMerged = mp.total_count;
90
- const { data: op } = await octokit.search.issuesAndPullRequests({ q: `repo:${OWNER}/${REPO} type:pr author:${username} is:open` });
91
- prsOpen = op.total_count;
92
- const { data: ci } = await octokit.search.issuesAndPullRequests({ q: `repo:${OWNER}/${REPO} type:issue author:${username}` });
93
- issuesCreated = ci.total_count;
94
- } catch { /* rate limit */ }
95
-
96
- let relevantLanguages: string[] = [];
97
- try {
98
- const { data: repos } = await octokit.repos.listForUser({ username, sort: 'updated', per_page: 20 });
99
- const langs = new Set<string>();
100
- repos.forEach((r) => { if (r.language) langs.add(r.language); });
101
- relevantLanguages = Array.from(langs);
102
- } catch { /* continue */ }
103
-
104
- let hasReadme = false;
105
- try { await octokit.repos.get({ owner: username, repo: username }); hasReadme = true; } catch { /* no readme */ }
106
-
107
- return {
108
- username, avatarUrl: user.avatar_url, accountAge,
109
- publicRepos: user.public_repos, followers: user.followers,
110
- totalContributions, repoContributions: prsMerged + prsOpen + issuesCreated,
111
- relevantLanguages, hasReadme, prsMerged, prsOpen, issuesCreated, commentBody,
112
- };
113
- }
114
-
115
- // ── Scoring Algorithm ──────────────────────────────────────────────────
116
- function scoreApplicant(p: ApplicantProfile, issueLabels: string[]): ScoredApplicant {
117
- const b: ScoreBreakdown = { accountMaturity: 0, repoExperience: 0, githubPresence: 0, activityLevel: 0, applicationQuality: 0, languageRelevance: 0, total: 0 };
118
-
119
- // 1. Account Maturity (0-15)
120
- b.accountMaturity = p.accountAge > 730 ? 15 : p.accountAge > 365 ? 12 : p.accountAge > 180 ? 9 : p.accountAge > 90 ? 6 : p.accountAge > 30 ? 3 : 1;
121
-
122
- // 2. Repo Experience (0-30) — highest weight
123
- b.repoExperience = Math.min(15, p.prsMerged * 5) + Math.min(5, p.prsOpen * 3) + Math.min(5, p.issuesCreated * 2) + Math.min(5, p.repoContributions);
124
-
125
- // 3. GitHub Presence (0-15)
126
- b.githubPresence = Math.min(5, Math.floor(p.publicRepos / 5)) + Math.min(5, Math.floor(p.followers / 10)) + (p.hasReadme ? 5 : 0);
127
-
128
- // 4. Activity Level (0-15)
129
- b.activityLevel = p.totalContributions >= 80 ? 15 : p.totalContributions >= 50 ? 12 : p.totalContributions >= 30 ? 9 : p.totalContributions >= 15 ? 6 : p.totalContributions >= 5 ? 3 : 1;
130
-
131
- // 5. Application Quality (0-15)
132
- const words = p.commentBody.split(/\s+/).length;
133
- const hasApproach = /approach|plan|implement|would|will|by|using|step/i.test(p.commentBody);
134
- const hasExp = /experience|worked|built|familiar|know|background/i.test(p.commentBody);
135
- b.applicationQuality = (words >= 50 && hasApproach && hasExp) ? 15 : (words >= 30 && (hasApproach || hasExp)) ? 12 : (words >= 20 && hasApproach) ? 9 : words >= 15 ? 6 : words >= 8 ? 3 : 1;
136
-
137
- // 6. Language Relevance (0-10) — auto-detect from repo languages
138
- b.languageRelevance = 5; // default neutral
139
- if (issueLabels.length > 0 && p.relevantLanguages.length > 0) {
140
- // Approximate: check if user knows popular labels' likely languages
141
- const labelLower = issueLabels.map((l) => l.toLowerCase()).join(' ');
142
- const hasRust = p.relevantLanguages.includes('Rust');
143
- const hasTS = p.relevantLanguages.includes('TypeScript') || p.relevantLanguages.includes('JavaScript');
144
- const needsRust = /contract|rust|wasm|soroban/i.test(labelLower);
145
- const needsTS = /backend|frontend|api|websocket|typescript/i.test(labelLower);
146
-
147
- let matches = 0, needed = 0;
148
- if (needsRust) { needed++; if (hasRust) matches++; }
149
- if (needsTS) { needed++; if (hasTS) matches++; }
150
- if (needed > 0) b.languageRelevance = Math.round((matches / needed) * 10);
151
- }
152
-
153
- b.total = b.accountMaturity + b.repoExperience + b.githubPresence + b.activityLevel + b.applicationQuality + b.languageRelevance;
154
-
155
- const tier: ScoredApplicant['tier'] = b.total >= 75 ? 'S' : b.total >= 55 ? 'A' : b.total >= 40 ? 'B' : b.total >= 25 ? 'C' : 'D';
156
-
157
- return { ...p, score: b.total, breakdown: b, tier };
158
- }
28
+ // Initialise shared GitHub client so core/scorer.ts functions work
29
+ initGitHub(GITHUB_TOKEN, OWNER, REPO);
159
30
 
160
31
  // ── Build Comments ─────────────────────────────────────────────────────
161
- const TIER_EMOJI: Record<string, string> = { S: 'šŸ†', A: '🟢', B: '🟔', C: '🟠', D: 'šŸ”“' };
162
-
163
32
  function buildReviewComment(s: ScoredApplicant, count: number): string {
164
33
  let c = `## šŸ¤– Contributor Agent — Applicant Review\n\n`;
165
34
  c += `**Applicant:** @${s.username} ${TIER_EMOJI[s.tier]} **Tier ${s.tier}** (${s.score}/100)\n\n`;
@@ -214,6 +83,8 @@ async function main(): Promise<void> {
214
83
  console.log(`\nšŸ¤– Applicant Scorer — ${OWNER}/${REPO} #${ISSUE_NUMBER}\n`);
215
84
  if (!ISSUE_NUMBER) { console.error('āŒ ISSUE_NUMBER required'); process.exit(1); }
216
85
 
86
+ const octokit = getOctokit();
87
+
217
88
  // Get trigger comment
218
89
  let trigger: { body: string; user: string };
219
90
  if (COMMENT_ID) {
@@ -280,6 +151,27 @@ async function main(): Promise<void> {
280
151
  }
281
152
  console.log(`āœ… Comparison posted (${allScored.length} candidates)\n`);
282
153
  }
154
+
155
+ // Auto-assign the top-scoring S or A tier candidate
156
+ const sorted = [...allScored].sort((a, b) => b.score - a.score);
157
+ const top = sorted[0];
158
+ if (top && (top.tier === 'S' || top.tier === 'A')) {
159
+ // Only assign if the issue has no assignees yet
160
+ const currentAssignees = issue.assignees?.map((a) => a.login) || [];
161
+ if (currentAssignees.length === 0) {
162
+ await octokit.issues.addAssignees({
163
+ owner: OWNER,
164
+ repo: REPO,
165
+ issue_number: ISSUE_NUMBER,
166
+ assignees: [top.username],
167
+ });
168
+ console.log(`āœ… Auto-assigned @${top.username} to issue #${ISSUE_NUMBER} (Tier ${top.tier}, ${top.score}/100)`);
169
+ } else {
170
+ console.log(`ā„¹ļø Issue already assigned to: ${currentAssignees.join(', ')} — skipping auto-assign`);
171
+ }
172
+ } else if (top) {
173
+ console.log(`ā„¹ļø Top candidate @${top.username} is Tier ${top.tier} (${top.score}/100) — below auto-assign threshold (S/A)`);
174
+ }
283
175
  }
284
176
 
285
177
  main().catch((e) => { console.error('Fatal:', e); process.exit(1); });