diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..c410352 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Agentics Foundation Website + url: https://agentics.org + about: Learn more about the Agentics Foundation and membership diff --git a/.github/ISSUE_TEMPLATE/project-submission.yml b/.github/ISSUE_TEMPLATE/project-submission.yml new file mode 100644 index 0000000..5c9fbb0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/project-submission.yml @@ -0,0 +1,94 @@ +name: Open Source Project Submission +description: Submit an open source project for review by the Agentics Foundation Open Source Committee +title: "[Project Submission] " +labels: ["status:pending-review"] +body: + - type: markdown + attributes: + value: | + ## Agentics Foundation — Open Source Project Submission + + Only registered members of the Agentics Foundation may submit projects. + Submissions from non-members will not be reviewed. + + Please complete all required fields below. + + - type: input + id: full-name + attributes: + label: Full Name + placeholder: Jane Doe + validations: + required: true + + - type: input + id: email + attributes: + label: Email Address + placeholder: jane@example.com + validations: + required: true + + - type: input + id: linkedin + attributes: + label: LinkedIn Profile URL + placeholder: https://linkedin.com/in/janedoe + validations: + required: true + + - type: input + id: github-profile + attributes: + label: GitHub Profile URL + placeholder: https://github.com/janedoe + validations: + required: true + + - type: input + id: repo-url + attributes: + label: Repository URL + description: Must be a public repository with a declared license. + placeholder: https://github.com/janedoe/my-project + validations: + required: true + + - type: dropdown + id: category + attributes: + label: Submission Category + options: + - Project Donation + - Website Listing + - Co-Founder Search + - Problem Support + - Contributor Engagement + validations: + required: true + + - type: textarea + id: description + attributes: + label: Project Description & Request + description: Describe the project and your specific request (max 500 characters). + placeholder: | + Brief description of what the project does and what you're asking the Foundation to help with. + validations: + required: true + + - type: checkboxes + id: acknowledgments + attributes: + label: Acknowledgments + options: + - label: I am a registered member of the Agentics Foundation in good standing + required: true + - label: The repository is public and has a clearly stated open source license + required: true + - label: The project is aligned with the Foundation's mission, values, and code of conduct + required: true + - label: There are no known IP infringements associated with this project + required: true + - label: I understand that submission does not guarantee approval + required: true diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..5ace460 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/approve-project.yml b/.github/workflows/approve-project.yml new file mode 100644 index 0000000..0dcf050 --- /dev/null +++ b/.github/workflows/approve-project.yml @@ -0,0 +1,156 @@ +name: Approve Project + +on: + issues: + types: [labeled] + +permissions: + contents: write + issues: write + +jobs: + register-project: + runs-on: ubuntu-latest + if: github.event.label.name == 'status:approved' + concurrency: + group: registry-update + cancel-in-progress: false + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + + - name: Extract project data and update registry + id: extract + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 + with: + script: | + const fs = require('fs'); + const body = context.payload.issue.body || ''; + const issueNumber = context.payload.issue.number; + + // SEC-007: Sanitize user input before embedding in registry + function sanitize(input, maxLen = 500) { + return input + .replace(/[[\](){}|`*_~#>!\\]/g, '') + .substring(0, maxLen) + .trim(); + } + + // Parse fields from issue form + function extract(label) { + const re = new RegExp(`### ${label}\\s*\\n\\s*(.+)`, 'i'); + const m = body.match(re); + return m ? m[1].trim() : ''; + } + + const name = extract('Full Name'); + const repoUrl = extract('Repository URL'); + const description = body.match(/### Project Description & Request\s*\n\s*([\s\S]*?)(?=\n###|\n---|\Z)/i); + const descText = description ? description[1].trim().split('\n')[0] : ''; + + // SEC-007: Validate repo URL format + const repoUrlPattern = /^https:\/\/github\.com\/[\w.-]+\/[\w.-]+\/?$/; + if (repoUrl && !repoUrlPattern.test(repoUrl)) { + core.warning(`Invalid repo URL format: ${repoUrl}`); + } + + // Extract category + const categoryMap = { + 'Project Donation': 'donation', + 'Website Listing': 'website-listing', + 'Co-Founder Search': 'cofounder', + 'Problem Support': 'support', + 'Contributor Engagement': 'contributors' + }; + let category = 'unknown'; + for (const [text, slug] of Object.entries(categoryMap)) { + if (body.includes(text)) { category = slug; break; } + } + + // Calculate total score from score comments + const comments = await github.paginate(github.rest.issues.listComments, { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber + }); + + let totalScore = 0; + let scoreCount = 0; + for (const c of comments) { + const match = c.body.match(/\*\*Total\*\*\s*\|\s*\*\*(\d+)\/25\*\*/); + if (match) { + totalScore += parseInt(match[1]); + scoreCount++; + } + } + const avgScore = scoreCount > 0 ? Math.round(totalScore / scoreCount) : 0; + + // SEC-009: Validate registry schema + const registryPath = 'data/approved-projects.json'; + let registry; + try { + registry = JSON.parse(fs.readFileSync(registryPath, 'utf8')); + if (!Array.isArray(registry)) { + throw new Error('Registry must be an array'); + } + } catch (e) { + core.setFailed(`Registry validation failed: ${e.message}`); + return; + } + + // Generate ID from issue number (avoids race condition with registry.length) + const id = `proj-${String(issueNumber).padStart(3, '0')}`; + + // Add entry + registry.push({ + id, + name: sanitize(descText, 80) || `Submission #${issueNumber}`, + repo_url: repoUrl, + category, + approved_date: new Date().toISOString().split('T')[0], + submitter: context.payload.issue.user.login, + description: sanitize(descText, 500), + total_score: avgScore, + issue_number: issueNumber, + status: 'active' + }); + + fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2) + '\n'); + + // Set output for commit step + core.setOutput('project_id', id); + core.setOutput('issue_number', String(issueNumber)); + + - name: Commit and push + env: + PROJECT_ID: ${{ steps.extract.outputs.project_id }} + ISSUE_NUMBER: ${{ steps.extract.outputs.issue_number }} + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add data/approved-projects.json + git commit -m "Add approved project ${PROJECT_ID} from issue #${ISSUE_NUMBER}" || exit 0 + git pull --rebase origin main + git push + + - name: Post confirmation + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 + env: + PROJECT_ID: ${{ steps.extract.outputs.project_id }} + with: + script: | + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: [ + '### Project Registered', + '', + `This project has been added to the [approved projects registry](../blob/main/data/approved-projects.json) as \`${process.env.PROJECT_ID}\`.`, + '', + 'The submitter will be contacted with next steps.', + '', + '---', + '*Automated registration.*' + ].join('\n') + }); diff --git a/.github/workflows/escalation-vote.yml b/.github/workflows/escalation-vote.yml new file mode 100644 index 0000000..188e8bb --- /dev/null +++ b/.github/workflows/escalation-vote.yml @@ -0,0 +1,170 @@ +name: Escalation Vote + +on: + issue_comment: + types: [created] + +permissions: + issues: write + +jobs: + tally-escalation: + runs-on: ubuntu-latest + concurrency: + group: escalation-${{ github.event.issue.number }} + cancel-in-progress: true + if: | + startsWith(github.event.comment.body, '/vote escalate') || + startsWith(github.event.comment.body, '/vote no-escalate') + steps: + - name: Tally escalation votes + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 + env: + QUORUM: '3' + with: + script: | + const quorum = parseInt(process.env.QUORUM); + + // SEC-003: Verify commenter is an org member/collaborator + const validAssociations = ['MEMBER', 'OWNER', 'COLLABORATOR']; + const association = context.payload.comment.author_association; + if (!validAssociations.includes(association)) { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: `@${context.payload.comment.user.login} Only organization members, owners, or collaborators can perform this action.` + }); + return; + } + + // Fetch all comments + const comments = await github.paginate(github.rest.issues.listComments, { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number + }); + + // SEC-012: Exclude issue author from voting + const issue = await github.rest.issues.get({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number + }); + const submitter = issue.data.user.login; + + // Tally votes (one per unique user, last vote wins) + const votes = {}; + for (const c of comments) { + if (c.user.type === 'Bot') continue; + if (c.user.login === submitter) continue; + const body = c.body.trim(); + if (/^\/vote escalate\s*$/.test(body)) { + votes[c.user.login] = 'escalate'; + } + if (/^\/vote no-escalate\s*$/.test(body)) { + votes[c.user.login] = 'no-escalate'; + } + } + + const escalate = Object.values(votes).filter(v => v === 'escalate').length; + const noEscalate = Object.values(votes).filter(v => v === 'no-escalate').length; + const totalVotes = escalate + noEscalate; + + // SEC-008: Rate limit - skip if bot posted a tally within last 5 minutes + const recentBotTally = comments.find(c => + c.user.login === 'github-actions[bot]' && + (c.body.includes('Vote Tally') || c.body.includes('Vote —')) && + (Date.now() - new Date(c.created_at).getTime()) < 300000 + ); + if (recentBotTally && totalVotes < quorum) { + return; // Skip posting duplicate tally + } + + // Apply label to track voting phase + const labels = (await github.rest.issues.listLabelsOnIssue({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number + })).data.map(l => l.name); + + if (!labels.includes('status:escalation-vote')) { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + labels: ['status:escalation-vote'] + }); + } + + // Check if quorum reached + if (totalVotes < quorum) { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: `**Escalation Vote Tally** — ${totalVotes}/${quorum} votes cast (quorum not yet reached)\n\n| | Count |\n|---|---|\n| Escalate | ${escalate} |\n| No Escalate | ${noEscalate} |` + }); + return; + } + + // Quorum reached — determine outcome + const majority = Math.floor(totalVotes / 2) + 1; + const escalated = escalate >= majority; + + if (escalated) { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + labels: ['escalated'] + }); + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: [ + '### Escalation Vote — Result: ESCALATED', + '', + `Votes: ${escalate} escalate, ${noEscalate} no-escalate (quorum: ${quorum})`, + '', + 'This submission has been **escalated to senior leadership** for further review.', + '', + '---', + '*Automated escalation vote result.*' + ].join('\n') + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: [ + '### Escalation Vote — Result: NOT ESCALATED', + '', + `Votes: ${escalate} escalate, ${noEscalate} no-escalate (quorum: ${quorum})`, + '', + 'This submission does **not** require escalation. Proceeding to validation vote.', + '', + 'Committee members may now vote: `/vote approve`, `/vote decline`, or `/vote defer`.', + '', + '---', + '*Automated escalation vote result.*' + ].join('\n') + }); + // Remove escalation-vote label, add validation-vote + try { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + name: 'status:escalation-vote' + }); + } catch (e) { /* label may not exist */ } + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + labels: ['status:validation-vote'] + }); + } diff --git a/.github/workflows/on-submission.yml b/.github/workflows/on-submission.yml new file mode 100644 index 0000000..b7345d2 --- /dev/null +++ b/.github/workflows/on-submission.yml @@ -0,0 +1,82 @@ +name: On Submission + +on: + issues: + types: [opened] + +permissions: + issues: write + +jobs: + triage: + runs-on: ubuntu-latest + if: contains(github.event.issue.labels.*.name, 'status:pending-review') + steps: + - name: Extract category and apply label + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 + with: + script: | + const body = context.payload.issue.body || ''; + + // SEC-007: Sanitize user input before embedding in comments + function sanitize(input, maxLen = 100) { + return input + .replace(/[[\](){}|`*_~#>!\\]/g, '') + .substring(0, maxLen) + .trim(); + } + + // Extract category from the issue form + const categoryMap = { + 'Project Donation': 'category:donation', + 'Website Listing': 'category:website-listing', + 'Co-Founder Search': 'category:cofounder', + 'Problem Support': 'category:support', + 'Contributor Engagement': 'category:contributors' + }; + + let categoryLabel = null; + for (const [text, label] of Object.entries(categoryMap)) { + if (body.includes(text)) { + categoryLabel = label; + break; + } + } + + // Apply category label + if (categoryLabel) { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + labels: [categoryLabel] + }); + } + + // Extract submitter name + const nameMatch = body.match(/### Full Name\s*\n\s*(.+)/); + const name = sanitize(nameMatch ? nameMatch[1].trim() : 'there'); + + // Post welcome comment + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: [ + `Thank you for your submission, ${name}!`, + '', + 'Your project has been received and is now **pending review** by the Open Source Committee.', + '', + '### What happens next', + '', + '1. Committee members will review your submission and score it using the [scoring rubric](../blob/main/docs/scoring-template.md)', + '2. The committee will vote on whether to escalate to senior leadership', + '3. If not escalated, a validation vote will determine approval, deferral, or decline', + '4. You will be notified of the outcome', + '', + 'If you need to update your submission, please edit the issue description above.', + '', + '---', + '*This is an automated message from the Agentics Foundation Open Source Committee intake system.*' + ].join('\n') + }); diff --git a/.github/workflows/retraction.yml b/.github/workflows/retraction.yml new file mode 100644 index 0000000..26fc943 --- /dev/null +++ b/.github/workflows/retraction.yml @@ -0,0 +1,252 @@ +name: Retraction + +on: + issue_comment: + types: [created] + +permissions: + contents: write + issues: write + +jobs: + propose-retraction: + runs-on: ubuntu-latest + concurrency: + group: registry-update + cancel-in-progress: false + if: github.event.comment.body == '/retract' + steps: + - name: Post retraction proposal + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 + with: + script: | + // SEC-003: Verify commenter is an org member/collaborator + const validAssociations = ['MEMBER', 'OWNER', 'COLLABORATOR']; + const association = context.payload.comment.author_association; + if (!validAssociations.includes(association)) { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: `@${context.payload.comment.user.login} Only organization members, owners, or collaborators can perform this action.` + }); + return; + } + + const proposer = context.payload.comment.user.login; + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: [ + `### Retraction Proposed by @${proposer}`, + '', + 'A committee member has proposed retracting approval of this project.', + '', + '**Grounds for retraction** (per governance doc):', + '- Violation of code of conduct or values', + '- Misrepresentation of project or licensing', + '- Inactive/abandoned maintenance impacting trust', + '- Legal, ethical, or reputational risk', + '- Loss of membership standing', + '', + 'Committee members: vote with `/vote retract` to support retraction or `/vote no-retract` to keep.', + 'A simple majority is required.', + '', + '---', + '*Automated retraction proposal.*' + ].join('\n') + }); + + tally-retraction: + runs-on: ubuntu-latest + if: | + github.event.comment.body == '/vote retract' || + github.event.comment.body == '/vote no-retract' + concurrency: + group: registry-update + cancel-in-progress: false + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + + - name: Tally retraction votes + id: tally + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 + env: + QUORUM: '3' + with: + script: | + const fs = require('fs'); + const quorum = parseInt(process.env.QUORUM); + + // SEC-003: Verify commenter is an org member/collaborator + const validAssociations = ['MEMBER', 'OWNER', 'COLLABORATOR']; + const association = context.payload.comment.author_association; + if (!validAssociations.includes(association)) { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: `@${context.payload.comment.user.login} Only organization members, owners, or collaborators can perform this action.` + }); + return; + } + + const comments = await github.paginate(github.rest.issues.listComments, { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number + }); + + // Count unique retraction and no-retract votes + const retractVoters = new Set(); + const keepVoters = new Set(); + for (const c of comments) { + if (c.user.type === 'Bot') continue; + const body = c.body.trim(); + if (body === '/vote retract') { + retractVoters.add(c.user.login); + keepVoters.delete(c.user.login); + } + if (body === '/vote no-retract') { + keepVoters.add(c.user.login); + retractVoters.delete(c.user.login); + } + } + + const retractCount = retractVoters.size; + const keepCount = keepVoters.size; + const totalVotes = retractCount + keepCount; + + // SEC-008: Rate limit - skip if bot posted a tally within last 5 minutes + const recentBotTally = comments.find(c => + c.user.login === 'github-actions[bot]' && + (c.body.includes('Vote Tally') || c.body.includes('Vote —')) && + (Date.now() - new Date(c.created_at).getTime()) < 300000 + ); + if (recentBotTally && totalVotes < quorum) { + return; // Skip posting duplicate tally + } + + if (totalVotes < quorum) { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: `**Retraction Vote Tally** -- ${totalVotes}/${quorum} votes (quorum not yet reached)\n\n| | Count |\n|---|---|\n| Retract | ${retractCount} |\n| Keep | ${keepCount} |` + }); + return; + } + + // Quorum reached — determine outcome + const issueNumber = context.issue.number; + + if (retractCount > keepCount) { + // SEC-009: Validate registry schema + const registryPath = 'data/approved-projects.json'; + let registry; + try { + registry = JSON.parse(fs.readFileSync(registryPath, 'utf8')); + if (!Array.isArray(registry)) { + throw new Error('Registry must be an array'); + } + } catch (e) { + const core = require('@actions/core'); + core.setFailed(`Registry validation failed: ${e.message}`); + return; + } + + const idx = registry.findIndex(p => p.issue_number === issueNumber); + if (idx !== -1) { + registry[idx].status = 'retracted'; + registry[idx].retracted_date = new Date().toISOString().split('T')[0]; + fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2) + '\n'); + + core.setOutput('updated_registry', 'true'); + core.setOutput('issue_number', String(issueNumber)); + } + + // Update labels + const labels = (await github.rest.issues.listLabelsOnIssue({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber + })).data.map(l => l.name); + + for (const sl of labels.filter(l => l.startsWith('status:'))) { + try { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + name: sl + }); + } catch (e) { /* ok */ } + } + + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + labels: ['status:retracted'] + }); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + body: [ + '### Retraction Vote -- Result: RETRACTED', + '', + `Votes to retract: ${retractCount} | Votes to keep: ${keepCount} (quorum: ${quorum})`, + '', + 'This project\'s approval has been **retracted**. It has been marked as retracted in the registry.', + '', + 'Actions that may follow:', + '- Removal from Foundation website', + '- Withdrawal of support', + '- Termination of active collaboration', + '', + '---', + '*Automated retraction result.*' + ].join('\n') + }); + + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + state: 'closed', + state_reason: 'not_planned' + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + body: [ + '### Retraction Vote -- Result: RETAINED', + '', + `Votes to retract: ${retractCount} | Votes to keep: ${keepCount} (quorum: ${quorum})`, + '', + 'This project\'s approval has been **retained** by the committee.', + '', + '---', + '*Automated retraction vote result.*' + ].join('\n') + }); + } + + - name: Commit registry update + if: steps.tally.outputs.updated_registry == 'true' + env: + ISSUE_NUMBER: ${{ steps.tally.outputs.issue_number }} + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add data/approved-projects.json + git commit -m "Retract project from issue #${ISSUE_NUMBER}" || exit 0 + git pull --rebase origin main + git push diff --git a/.github/workflows/scoring.yml b/.github/workflows/scoring.yml new file mode 100644 index 0000000..8865e45 --- /dev/null +++ b/.github/workflows/scoring.yml @@ -0,0 +1,116 @@ +name: Scoring + +on: + issue_comment: + types: [created] + +permissions: + issues: write + +jobs: + parse-score: + runs-on: ubuntu-latest + concurrency: + group: scoring-${{ github.event.issue.number }} + cancel-in-progress: true + if: startsWith(github.event.comment.body, '/score ') + steps: + - name: Parse and post score + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 + with: + script: | + // SEC-003: Verify commenter is an org member/collaborator + const validAssociations = ['MEMBER', 'OWNER', 'COLLABORATOR']; + const association = context.payload.comment.author_association; + if (!validAssociations.includes(association)) { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: `@${context.payload.comment.user.login} Only organization members, owners, or collaborators can perform this action.` + }); + return; + } + + const comment = context.payload.comment.body.trim(); + const reviewer = context.payload.comment.user.login; + + // Parse /score mission:4 quality:3 clarity:5 impact:4 risk:3 + const criteria = ['mission', 'quality', 'clarity', 'impact', 'risk']; + const scores = {}; + let valid = true; + + for (const c of criteria) { + const match = comment.match(new RegExp(`${c}:\\s*(\\d)`)); + if (match) { + const val = parseInt(match[1]); + if (val < 0 || val > 5) { valid = false; break; } + scores[c] = val; + } else { + valid = false; + break; + } + } + + if (!valid) { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: [ + `@${reviewer} — Could not parse your score. Please use the format:`, + '```', + '/score mission:4 quality:3 clarity:5 impact:4 risk:3', + '```', + 'Each score must be 0–5.' + ].join('\n') + }); + return; + } + + const total = Object.values(scores).reduce((a, b) => a + b, 0); + + let interpretation; + if (total >= 21) interpretation = 'Strong candidate for approval or escalation'; + else if (total >= 16) interpretation = 'Approve or approve with conditions'; + else if (total >= 11) interpretation = 'Defer or request clarification'; + else interpretation = 'Decline'; + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: [ + `### Score from @${reviewer}`, + '', + '| Criterion | Score |', + '|-----------|-------|', + `| Mission & Values Alignment | ${scores.mission}/5 |`, + `| Project Quality & Maturity | ${scores.quality}/5 |`, + `| Clarity of Request | ${scores.clarity}/5 |`, + `| Community Impact | ${scores.impact}/5 |`, + `| Risk & Governance (inverse) | ${scores.risk}/5 |`, + `| **Total** | **${total}/25** |`, + '', + `**Interpretation:** ${interpretation}`, + '', + '---', + '*Parsed automatically. See [scoring rubric](../blob/main/docs/scoring-template.md) for criteria details.*' + ].join('\n') + }); + + // Apply scoring label if not already present + const labels = await github.rest.issues.listLabelsOnIssue({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number + }); + + if (!labels.data.some(l => l.name === 'status:scoring')) { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + labels: ['status:scoring'] + }); + } diff --git a/.github/workflows/validation-vote.yml b/.github/workflows/validation-vote.yml new file mode 100644 index 0000000..0937bbd --- /dev/null +++ b/.github/workflows/validation-vote.yml @@ -0,0 +1,179 @@ +name: Validation Vote + +on: + issue_comment: + types: [created] + +permissions: + issues: write + +jobs: + tally-validation: + runs-on: ubuntu-latest + concurrency: + group: validation-${{ github.event.issue.number }} + cancel-in-progress: true + if: | + startsWith(github.event.comment.body, '/vote approve') || + startsWith(github.event.comment.body, '/vote decline') || + startsWith(github.event.comment.body, '/vote defer') + steps: + - name: Tally validation votes + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 + env: + QUORUM: '3' + with: + script: | + const quorum = parseInt(process.env.QUORUM); + + // SEC-003: Verify commenter is an org member/collaborator + const validAssociations = ['MEMBER', 'OWNER', 'COLLABORATOR']; + const association = context.payload.comment.author_association; + if (!validAssociations.includes(association)) { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: `@${context.payload.comment.user.login} Only organization members, owners, or collaborators can perform this action.` + }); + return; + } + + // Fetch all comments + const comments = await github.paginate(github.rest.issues.listComments, { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number + }); + + // SEC-012: Exclude issue author from voting + const issue = await github.rest.issues.get({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number + }); + const submitter = issue.data.user.login; + + // Tally votes (one per unique user, last vote wins) + const votes = {}; + for (const c of comments) { + if (c.user.type === 'Bot') continue; + if (c.user.login === submitter) continue; + const body = c.body.trim(); + if (/^\/vote approve\s*$/.test(body)) votes[c.user.login] = 'approve'; + if (/^\/vote decline\s*$/.test(body)) votes[c.user.login] = 'decline'; + if (/^\/vote defer\s*$/.test(body)) votes[c.user.login] = 'defer'; + } + + const approve = Object.values(votes).filter(v => v === 'approve').length; + const decline = Object.values(votes).filter(v => v === 'decline').length; + const defer = Object.values(votes).filter(v => v === 'defer').length; + const totalVotes = approve + decline + defer; + + // SEC-008: Rate limit - skip if bot posted a tally within last 5 minutes + const recentBotTally = comments.find(c => + c.user.login === 'github-actions[bot]' && + (c.body.includes('Vote Tally') || c.body.includes('Vote —')) && + (Date.now() - new Date(c.created_at).getTime()) < 300000 + ); + if (recentBotTally && totalVotes < quorum) { + return; // Skip posting duplicate tally + } + + // Ensure validation-vote label is applied + const labels = (await github.rest.issues.listLabelsOnIssue({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number + })).data.map(l => l.name); + + if (!labels.includes('status:validation-vote')) { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + labels: ['status:validation-vote'] + }); + } + + if (totalVotes < quorum) { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: `**Validation Vote Tally** — ${totalVotes}/${quorum} votes cast (quorum not yet reached)\n\n| | Count |\n|---|---|\n| Approve | ${approve} |\n| Decline | ${decline} |\n| Defer | ${defer} |` + }); + return; + } + + // SEC-010: Strict majority required, ties default to DEFERRED + let outcome, outcomeLabel; + if (approve > decline && approve > defer) { + outcome = 'APPROVED'; + outcomeLabel = 'status:approved'; + } else if (decline > approve && decline > defer) { + outcome = 'DECLINED'; + outcomeLabel = 'status:declined'; + } else { + outcome = 'DEFERRED'; + outcomeLabel = 'status:deferred'; + } + + // Remove old status labels, apply outcome + const statusLabels = labels.filter(l => l.startsWith('status:')); + for (const sl of statusLabels) { + try { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + name: sl + }); + } catch (e) { /* ok */ } + } + + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + labels: [outcomeLabel] + }); + + const emoji = { APPROVED: '✅', DECLINED: '❌', DEFERRED: '⏸️' }; + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: [ + `### Validation Vote — Result: ${emoji[outcome]} ${outcome}`, + '', + `| | Count |`, + `|---|---|`, + `| Approve | ${approve} |`, + `| Decline | ${decline} |`, + `| Defer | ${defer} |`, + '', + `Quorum: ${quorum} | Total votes: ${totalVotes}`, + '', + outcome === 'APPROVED' + ? 'This project has been **approved** by the Open Source Committee. The submitter will be contacted with next steps.' + : outcome === 'DEFERRED' + ? 'This submission has been **deferred**. The submitter should provide additional information or clarification.' + : 'This submission has been **declined** by the Open Source Committee.', + '', + '---', + '*Automated validation vote result.*' + ].join('\n') + }); + + // Close issue if declined + if (outcome === 'DECLINED') { + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + state: 'closed', + state_reason: 'not_planned' + }); + } diff --git a/README.md b/README.md index 9345476..67d13fc 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,84 @@ -# community-projects -Community-driven initiatives: ideate - swarm - deliver, all agentically. +# Agentics Foundation — Community Projects -This project is mainly required because we share a backlog at https://github.com/orgs/agenticsorg/projects/22 and that needs a repo associated with the board. +Open source project intake and governance for the [Agentics Foundation](https://agentics.org). + +Members of the Agentics Foundation can submit open source projects for review, support, collaboration, or inclusion in Foundation activities. + +## Submit a Project + +**[Submit your project here](../../issues/new?template=project-submission.yml)** + +You must be a registered Foundation member in good standing. Your project must be in a public repository with a declared open source license. + +### Submission Categories + +| Category | What it means | +|----------|---------------| +| **Project Donation** | Transfer long-term stewardship to the Foundation | +| **Website Listing** | Featured on the Agentics Foundation website | +| **Co-Founder Search** | Connect with potential co-founders from membership | +| **Problem Support** | Help from members on a technical/architectural/organizational challenge | +| **Contributor Engagement** | Attract contributors, reviewers, or collaborators | + +## Review Process + +1. **Submit** — Fill out the issue template with your project details +2. **Triage** — Automated labeling and acknowledgment +3. **Scoring** — Committee members score using the [scoring rubric](docs/scoring-template.md) (5 criteria, 0–5 each, max 25) +4. **Escalation Vote** — Does this need senior leadership review? (majority vote) +5. **Validation Vote** — Approve, decline, or defer (majority vote) +6. **Registration** — Approved projects are added to `data/approved-projects.json` + +Decisions can be revisited. Any committee member may propose retraction of a previously approved project if grounds exist. + +## For Committee Members + +### Scoring + +Post a score comment on any submission issue: + +``` +/score mission:4 quality:3 clarity:5 impact:4 risk:3 +``` + +See the full [scoring template](docs/scoring-template.md) for criteria details and interpretation. + +### Voting + +| Command | When to use | +|---------|-------------| +| `/vote escalate` | Escalation vote: requires senior leadership review | +| `/vote no-escalate` | Escalation vote: no escalation needed | +| `/vote approve` | Validation vote: approve the submission | +| `/vote decline` | Validation vote: decline the submission | +| `/vote defer` | Validation vote: defer pending more info | +| `/retract` | Propose retraction of a previously approved project | +| `/vote retract` | Vote to retract approval | + +All votes require a quorum of 3 and pass by simple majority. + +### Labels + +| Label | Meaning | +|-------|---------| +| `status:pending-review` | Awaiting committee review | +| `status:scoring` | Scoring in progress | +| `status:escalation-vote` | Escalation vote underway | +| `status:validation-vote` | Validation vote underway | +| `status:approved` | Approved by committee | +| `status:declined` | Declined | +| `status:deferred` | Deferred, more info needed | +| `status:retracted` | Approval retracted | +| `escalated` | Sent to senior leadership | + +## Governance + +This intake process is defined by the [Open Source Project Intake: Rules and Processes](https://docs.google.com/document/d/1-WCg3ArxhllUpdyk1tJu3bHkQf92tdUm0u80GDM0Vj4/edit) governance document, maintained by the Open Source Committee. + +## Setup (for repo admins) + +To create all labels: + +```bash +./scripts/setup-labels.sh agenticsorg/community-projects +``` diff --git a/data/approved-projects.json b/data/approved-projects.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/data/approved-projects.json @@ -0,0 +1 @@ +[] diff --git a/docs/scoring-template.md b/docs/scoring-template.md new file mode 100644 index 0000000..dd2dc89 --- /dev/null +++ b/docs/scoring-template.md @@ -0,0 +1,88 @@ +# Scoring Template + +Committee members: copy the block below into an issue comment to submit your score. + +## Slash Command Format + +Post this as a comment on the submission issue: + +``` +/score mission:_ quality:_ clarity:_ impact:_ risk:_ +``` + +Replace each `_` with a score from 0–5. Example: + +``` +/score mission:4 quality:3 clarity:5 impact:4 risk:3 +``` + +**Total = sum of all five scores (max 25).** + +## Score Scale + +| Score | Meaning | +|-------|---------| +| 0 | Does not meet minimum expectations | +| 1 | Very weak alignment or quality | +| 2 | Below average, material gaps | +| 3 | Acceptable / baseline | +| 4 | Strong | +| 5 | Exceptional | + +## Criteria + +1. **Mission & Values Alignment (mission)** — Relevance to agentic AI ecosystem, open source values, absence of harmful intent +2. **Project Quality & Maturity (quality)** — Repo structure, documentation, license clarity, evidence of working code +3. **Clarity of Request (clarity)** — Specificity of description, feasibility, alignment between category and request +4. **Community Impact (impact)** — Collaboration potential, relevance to members, likelihood of engagement +5. **Risk & Governance (risk)** — IP/licensing clarity, security, dependency risks. **Inverted: lower risk = higher score** + +## Score Interpretation + +| Total | Suggested Outcome | +|-------|-------------------| +| 21–25 | Strong candidate for approval or escalation | +| 16–20 | Approve or approve with conditions | +| 11–15 | Defer or request clarification | +| 0–10 | Decline | + +## Full Score Sheet (for detailed notes) + +``` +Submission ID: #___ +Project Name: +Category: +Reviewer: +Date: + +Criterion | Score (0–5) | Notes +Mission & Values Alignment | | +Project Quality & Maturity | | +Clarity of Request | | +Community Impact | | +Risk & Governance (inverse) | | + Total: ___ / 25 + +Flags: +[ ] Donation / Stewardship implications +[ ] Legal or licensing concern +[ ] Reputational risk +[ ] Security or safety concern +[ ] Conflict of interest + +Recommendation: +[ ] Escalate [ ] Approve [ ] Approve with Conditions +[ ] Defer [ ] Decline +``` + +## Voting Commands + +After scoring, use these commands in issue comments: + +- `/vote escalate` — This submission should be escalated to senior leadership +- `/vote no-escalate` — No escalation needed +- `/vote approve` — Approve the submission +- `/vote decline` — Decline the submission +- `/vote defer` — Defer pending more information +- `/retract` — Propose retraction of a previously approved project +- `/vote retract` — Vote to retract approval diff --git a/scripts/setup-labels.sh b/scripts/setup-labels.sh new file mode 100755 index 0000000..2ed7412 --- /dev/null +++ b/scripts/setup-labels.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# Creates all labels for the community-projects repo. +# Idempotent — safe to run multiple times (--force overwrites). +# Usage: ./scripts/setup-labels.sh [owner/repo] + +set -euo pipefail + +REPO="${1:-agenticsorg/community-projects}" + +# SEC-014: Validate repo format +if [[ ! "$REPO" =~ ^[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+$ ]]; then + echo "Error: Invalid repo format. Expected owner/repo" >&2 + exit 1 +fi + +echo "Setting up labels for ${REPO}..." +echo "This will create or overwrite labels. Press Ctrl+C within 3 seconds to cancel." +sleep 3 + +# Status labels +gh label create "status:pending-review" --color "FBCA04" --description "Awaiting committee review" --repo "$REPO" --force +gh label create "status:scoring" --color "E68A00" --description "Committee scoring in progress" --repo "$REPO" --force +gh label create "status:escalation-vote" --color "7B61FF" --description "Escalation vote in progress" --repo "$REPO" --force +gh label create "status:validation-vote" --color "1D76DB" --description "Validation vote in progress" --repo "$REPO" --force +gh label create "status:approved" --color "0E8A16" --description "Approved by committee" --repo "$REPO" --force +gh label create "status:declined" --color "D73A49" --description "Declined by committee" --repo "$REPO" --force +gh label create "status:deferred" --color "959DA5" --description "Deferred — more info needed" --repo "$REPO" --force +gh label create "status:retracted" --color "86181D" --description "Approval retracted" --repo "$REPO" --force +gh label create "escalated" --color "5319E7" --description "Escalated to senior leadership" --repo "$REPO" --force + +# Category labels +gh label create "category:donation" --color "BFD4F2" --description "Project Donation" --repo "$REPO" --force +gh label create "category:website-listing" --color "C5DEF5" --description "Website Listing" --repo "$REPO" --force +gh label create "category:cofounder" --color "D4C5F9" --description "Co-Founder Search" --repo "$REPO" --force +gh label create "category:support" --color "FEF2C0" --description "Problem Support" --repo "$REPO" --force +gh label create "category:contributors" --color "BFDADC" --description "Contributor Engagement" --repo "$REPO" --force + +echo "All labels created successfully."