From 3c41eb1a73a45541ddae34f63e500e68b6807d3c Mon Sep 17 00:00:00 2001 From: John Myers Date: Mon, 16 Mar 2026 14:39:40 -0700 Subject: [PATCH 1/4] chore: add vouch system for first-time contributors Add a trust gate that auto-closes PRs from unvouched external contributors. Org members and collaborators bypass automatically. Maintainers vouch users by commenting /vouch on a Vouch Request discussion, which appends the username to .github/VOUCHED. Also adds AI usage policy and good-first-issue guidance to CONTRIBUTING.md. Signed-off-by: John Myers --- .github/CODEOWNERS | 3 + .github/DISCUSSION_TEMPLATE/vouch-request.yml | 58 ++++++ .github/ISSUE_TEMPLATE/config.yml | 6 + .github/VOUCHED | 11 ++ .github/workflows/vouch-check.yml | 119 +++++++++++++ .github/workflows/vouch-command.yml | 167 ++++++++++++++++++ AGENTS.md | 8 + CONTRIBUTING.md | 35 ++++ 8 files changed, 407 insertions(+) create mode 100644 .github/DISCUSSION_TEMPLATE/vouch-request.yml create mode 100644 .github/VOUCHED create mode 100644 .github/workflows/vouch-check.yml create mode 100644 .github/workflows/vouch-command.yml diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 3cd1ecba..6ec1440e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -4,3 +4,6 @@ # Agent infrastructure — tighter review .agents/ @NVIDIA/openshell-codeowners AGENTS.md @NVIDIA/openshell-codeowners + +# Vouch list — maintainers only (bot commits bypass, but manual edits need review) +.github/VOUCHED @NVIDIA/openshell-codeowners diff --git a/.github/DISCUSSION_TEMPLATE/vouch-request.yml b/.github/DISCUSSION_TEMPLATE/vouch-request.yml new file mode 100644 index 00000000..6b49efcf --- /dev/null +++ b/.github/DISCUSSION_TEMPLATE/vouch-request.yml @@ -0,0 +1,58 @@ +title: "Vouch request: [your GitHub username]" +labels: [] +body: + - type: markdown + attributes: + value: | + ## Vouch Request + + OpenShell uses a vouch system for first-time contributors. Fill out this + form to request approval. A maintainer will review and comment `/vouch` + if approved. + + **Write in your own words.** Do not have an AI generate this request. + Requests that read like LLM output will be denied. + + - type: textarea + id: what + attributes: + label: What do you want to work on? + description: > + Describe the change you want to make. Link to an existing issue if + there is one. + placeholder: "I want to fix #123 which causes sandbox timeouts when..." + validations: + required: true + + - type: textarea + id: why + attributes: + label: Why this change? + description: > + Explain your motivation and why this matters. Keep it concise. + placeholder: "This bug affects anyone running sandboxes on ARM64 because..." + validations: + required: true + + - type: textarea + id: approach + attributes: + label: How do you plan to implement it? + description: > + Brief description of your approach. You don't need a full design doc, + but show that you understand the relevant code. + placeholder: "The timeout is set in crates/openshell-sandbox/src/runtime.rs. I'd change..." + validations: + required: true + + - type: checkboxes + id: checklist + attributes: + label: Checklist + options: + - label: I wrote this request myself (not AI-generated) + required: true + - label: I have read [CONTRIBUTING.md](https://github.com/NVIDIA/OpenShell/blob/main/CONTRIBUTING.md) + required: true + - label: I understand that OpenShell is agent-first — I will use agents as tools but I own and understand my code + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index da08fcd1..28e6b201 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,5 +1,11 @@ blank_issues_enabled: false contact_links: + - name: First-time contributor? Get vouched first + url: https://github.com/NVIDIA/OpenShell/discussions/new?category=vouch-request + about: > + First-time contributors must be vouched before submitting PRs. Open a + Vouch Request discussion describing what you want to work on. A + maintainer will approve you with /vouch. - name: Have a question? url: https://github.com/NVIDIA/OpenShell/blob/main/CONTRIBUTING.md#agent-skills-for-contributors about: > diff --git a/.github/VOUCHED b/.github/VOUCHED new file mode 100644 index 00000000..edd25782 --- /dev/null +++ b/.github/VOUCHED @@ -0,0 +1,11 @@ +# Vouched Contributors +# +# Users listed here have been approved to submit pull requests. +# Org members and collaborators with push access bypass this check automatically. +# +# To vouch a new contributor, a maintainer comments "/vouch" on their +# Vouch Request discussion. The vouch-command workflow appends their +# username here and confirms approval. +# +# Format: one GitHub username per line, no @ prefix. +# Lines starting with # are comments. diff --git a/.github/workflows/vouch-check.yml b/.github/workflows/vouch-check.yml new file mode 100644 index 00000000..92f8ba58 --- /dev/null +++ b/.github/workflows/vouch-check.yml @@ -0,0 +1,119 @@ +name: Vouch Check + +on: + pull_request_target: + types: [opened, reopened] + +permissions: + contents: read + pull-requests: write + +jobs: + vouch-gate: + if: github.repository_owner == 'NVIDIA' + runs-on: ubuntu-latest + steps: + - name: Check if contributor is vouched + uses: actions/github-script@v7 + with: + script: | + const author = context.payload.pull_request.user.login; + const authorType = context.payload.pull_request.user.type; + + // 0. Skip bots (dependabot, renovate, github-actions, etc.). + if (authorType === 'Bot') { + console.log(`${author} is a bot (type: ${authorType}). Skipping vouch check.`); + return; + } + + // 1. Check org membership — members bypass the vouch gate. + try { + const { status } = await github.rest.orgs.checkMembershipForUser({ + org: context.repo.owner, + username: author, + }); + if (status === 204 || status === 302) { + console.log(`${author} is an org member. Skipping vouch check.`); + return; + } + } catch (e) { + // 404 = not a member, continue to next check. + if (e.status !== 404) { + console.log(`Org membership check error: ${e.message}`); + } + } + + // 2. Check collaborator status — direct collaborators bypass. + try { + const { status } = await github.rest.repos.checkCollaborator({ + owner: context.repo.owner, + repo: context.repo.repo, + username: author, + }); + if (status === 204) { + console.log(`${author} is a collaborator. Skipping vouch check.`); + return; + } + } catch (e) { + if (e.status !== 404) { + console.log(`Collaborator check error: ${e.message}`); + } + } + + // 3. Check the VOUCHED file. Read from the default branch, NOT the + // PR branch — the PR author could add themselves to VOUCHED in + // their fork. + let vouched = false; + try { + const { data } = await github.rest.repos.getContent({ + owner: context.repo.owner, + repo: context.repo.repo, + path: '.github/VOUCHED', + ref: context.payload.repository.default_branch, + }); + const content = Buffer.from(data.content, 'base64').toString('utf-8'); + const usernames = content + .split('\n') + .map(line => line.trim()) + .filter(line => line && !line.startsWith('#')); + vouched = usernames.some( + name => name.toLowerCase() === author.toLowerCase() + ); + } catch (e) { + console.log(`Could not read VOUCHED file: ${e.message}`); + } + + if (vouched) { + console.log(`${author} is in the VOUCHED file. Approved.`); + return; + } + + // Not vouched — close the PR with an explanation. + console.log(`${author} is not vouched. Closing PR.`); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + body: [ + `Thank you for your interest in contributing to OpenShell, @${author}.`, + '', + 'This project uses a **vouch system** for first-time contributors. Before submitting a pull request, you need to be vouched by a maintainer.', + '', + '**To get vouched:**', + '1. Open a [Vouch Request](https://github.com/NVIDIA/OpenShell/discussions/new?category=vouch-request) discussion.', + '2. Describe what you want to change and why.', + '3. Write in your own words — do not have an AI generate the request.', + '4. A maintainer will comment `/vouch` if approved.', + '5. Once vouched, open a new PR (preferred) or reopen this one after a few minutes.', + '', + 'See [CONTRIBUTING.md](https://github.com/NVIDIA/OpenShell/blob/main/CONTRIBUTING.md#first-time-contributors) for details.', + ].join('\n'), + }); + + await github.rest.pulls.update({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.payload.pull_request.number, + state: 'closed', + }); diff --git a/.github/workflows/vouch-command.yml b/.github/workflows/vouch-command.yml new file mode 100644 index 00000000..7570467f --- /dev/null +++ b/.github/workflows/vouch-command.yml @@ -0,0 +1,167 @@ +name: Vouch Command + +on: + discussion_comment: + types: [created] + +permissions: + contents: write + discussions: write + +jobs: + process-vouch: + if: >- + github.repository_owner == 'NVIDIA' && + github.event.comment.body == '/vouch' + runs-on: ubuntu-latest + steps: + - name: Process /vouch command + uses: actions/github-script@v7 + with: + script: | + const commenter = context.payload.comment.user.login; + const discussionAuthor = context.payload.discussion.user.login; + const discussionNumber = context.payload.discussion.number; + + // --- Helpers --- + + async function getDiscussionId() { + const query = `query($owner: String!, $repo: String!, $number: Int!) { + repository(owner: $owner, name: $repo) { + discussion(number: $number) { id } + } + }`; + const { repository } = await github.graphql(query, { + owner: context.repo.owner, + repo: context.repo.repo, + number: discussionNumber, + }); + return repository.discussion.id; + } + + async function postDiscussionComment(body) { + const discussionId = await getDiscussionId(); + const mutation = `mutation($discussionId: ID!, $body: String!) { + addDiscussionComment(input: {discussionId: $discussionId, body: $body}) { + comment { id } + } + }`; + await github.graphql(mutation, { discussionId, body }); + } + + // --- Authorization --- + + let isMaintainer = false; + try { + const { data } = await github.rest.repos.getCollaboratorPermissionLevel({ + owner: context.repo.owner, + repo: context.repo.repo, + username: commenter, + }); + const level = data.permission; + isMaintainer = ['admin', 'maintain', 'write'].includes(level); + } catch (e) { + console.log(`Permission check failed: ${e.message}`); + } + + if (!isMaintainer) { + console.log(`${commenter} does not have maintainer permissions. Ignoring.`); + return; + } + + // --- Read VOUCHED file --- + + const filePath = '.github/VOUCHED'; + const branch = context.payload.repository.default_branch; + const commitMessage = + `chore: vouch ${discussionAuthor}\n\nSigned-off-by: github-actions[bot] `; + + let currentContent = ''; + let sha = ''; + try { + const { data } = await github.rest.repos.getContent({ + owner: context.repo.owner, + repo: context.repo.repo, + path: filePath, + ref: branch, + }); + currentContent = Buffer.from(data.content, 'base64').toString('utf-8'); + sha = data.sha; + } catch (e) { + console.log(`Could not read VOUCHED file: ${e.message}`); + return; + } + + // --- Check if already vouched --- + + function parseUsernames(content) { + return content + .split('\n') + .map(line => line.trim()) + .filter(line => line && !line.startsWith('#')); + } + + function isVouched(content, username) { + return parseUsernames(content).some( + name => name.toLowerCase() === username.toLowerCase() + ); + } + + if (isVouched(currentContent, discussionAuthor)) { + console.log(`${discussionAuthor} is already vouched.`); + await postDiscussionComment( + `@${discussionAuthor} is already vouched. They can submit pull requests.` + ); + return; + } + + // --- Append username and commit --- + + async function commitVouch(content, fileSha) { + const updatedContent = content.trimEnd() + '\n' + discussionAuthor + '\n'; + await github.rest.repos.createOrUpdateFileContents({ + owner: context.repo.owner, + repo: context.repo.repo, + path: filePath, + message: commitMessage, + content: Buffer.from(updatedContent).toString('base64'), + sha: fileSha, + branch, + }); + } + + try { + await commitVouch(currentContent, sha); + } catch (e) { + if (e.status === 409) { + // Concurrent write — re-read and retry once. + console.log('409 conflict. Re-reading VOUCHED file and retrying.'); + const { data: fresh } = await github.rest.repos.getContent({ + owner: context.repo.owner, + repo: context.repo.repo, + path: filePath, + ref: branch, + }); + const freshContent = Buffer.from(fresh.content, 'base64').toString('utf-8'); + + if (isVouched(freshContent, discussionAuthor)) { + console.log(`${discussionAuthor} was vouched by a concurrent operation.`); + } else { + await commitVouch(freshContent, fresh.sha); + } + } else { + throw e; + } + } + + // --- Confirm --- + + await postDiscussionComment([ + `@${discussionAuthor} has been vouched by @${commenter}.`, + '', + 'You can now submit pull requests to OpenShell. Welcome aboard.', + '', + 'Please read [CONTRIBUTING.md](https://github.com/NVIDIA/OpenShell/blob/main/CONTRIBUTING.md) before submitting.', + ].join('\n')); + + console.log(`Vouched ${discussionAuthor} (approved by ${commenter}).`); diff --git a/AGENTS.md b/AGENTS.md index 089564c6..894d6de9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -45,11 +45,19 @@ These pipelines connect skills into end-to-end workflows. Individual skill files | `.agents/agents/` | Agent personas | Sub-agent definitions (e.g., reviewer, doc writer) | | `architecture/` | Architecture docs | Design decisions and component documentation | +## Vouch System + +- First-time external contributors must be vouched before their PRs are accepted. The `vouch-check` workflow auto-closes PRs from unvouched users. +- Org members and collaborators bypass the vouch gate automatically. +- Maintainers vouch users by commenting `/vouch` on a Vouch Request discussion. The `vouch-command` workflow appends the username to `.github/VOUCHED`. +- Skills that create PRs (`create-github-pr`, `build-from-issue`) should note this requirement when operating on behalf of external contributors. + ## Issue and PR Conventions - **Bug reports** must include an agent diagnostic section — proof that the reporter's agent investigated the issue before filing. See the issue template. - **Feature requests** must include a design proposal, not just a "please build this" request. See the issue template. - **PRs** must follow the PR template structure: Summary, Related Issue, Changes, Testing, Checklist. +- **PRs from unvouched external contributors** are automatically closed. See the Vouch System section above. - **Security vulnerabilities** must NOT be filed as GitHub issues. Follow [SECURITY.md](SECURITY.md). - Skills that create issues or PRs (`create-github-issue`, `create-github-pr`, `build-from-issue`) should produce output conforming to these templates. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f558cdeb..f3966ddb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,6 +2,41 @@ OpenShell is built agent-first. We design systems and use agents to implement them. Your agent is your first collaborator — point it at this repo before opening issues, asking questions, or submitting code. +## The Critical Rule + +**You must understand your code.** Using AI agents to write code is not just acceptable, it's how this project works. But you must be able to explain what your changes do and how they interact with the rest of the system. If you can't, don't submit it. + +Submitting agent-generated code without understanding it — regardless of how clean it looks — wastes maintainer time and will result in your PR being closed. Repeat offenders will be blocked from the project. + +## AI Usage + +OpenShell is agent-first, not agent-only. The distinction matters: + +- **Do** use agents to explore the codebase, run diagnostics, generate code, and iterate on implementations. +- **Do** use the skills in `.agents/skills/` — they exist to make your agent effective. +- **Do** interrogate your agent until you understand every edge case and interaction in your changes. +- **Don't** submit code you can't explain without your agent open. +- **Don't** have an AI write your issue reports, vouch requests, or PR descriptions. We can tell. +- **Don't** use agents as a substitute for understanding the system. Read the architecture docs. + +## First-Time Contributors + +We use a vouch system. This exists because AI makes it trivial to generate plausible-looking but low-quality contributions, and we can no longer trust by default. + +1. Open a [Vouch Request](https://github.com/NVIDIA/OpenShell/discussions/new?category=vouch-request) discussion. +2. Describe what you want to change and why. +3. Write in your own words. AI-generated vouch requests will be denied. +4. A maintainer will comment `/vouch` if approved. +5. Once vouched, you can submit pull requests. + +**If you are not vouched, any pull request you open will be automatically closed.** Org members and collaborators with push access bypass this check. + +### Finding Work + +Issues labeled [`good-first-issue`](https://github.com/NVIDIA/OpenShell/issues?q=is%3Aissue+is%3Aopen+label%3Agood-first-issue) are scoped, well-documented, and friendly to new contributors. Start there. If you need guidance, comment on the issue. + +All open issues are actionable — if it's in the issue tracker, it's ready to be worked on. + ## Before You Open an Issue This project ships with [agent skills](#agent-skills-for-contributors) that can diagnose problems, explore the codebase, generate policies, and walk you through common workflows. Before filing an issue: From 5fae18eacbbe39d99781910fc07f69c6743b40a5 Mon Sep 17 00:00:00 2001 From: John Myers Date: Mon, 16 Mar 2026 14:52:15 -0700 Subject: [PATCH 2/4] refactor: use mitchellh/vouch action instead of hand-rolled workflows Replace custom vouch-check and vouch-command workflow scripts with the mitchellh/vouch GitHub Actions. Handles bot exemption, race conditions, denouncement, and the .td file format out of the box. Signed-off-by: John Myers --- .github/CODEOWNERS | 2 +- .github/VOUCHED | 11 -- .github/VOUCHED.td | 10 ++ .github/workflows/vouch-check.yml | 108 +----------------- .github/workflows/vouch-command.yml | 167 +++------------------------- AGENTS.md | 2 +- 6 files changed, 32 insertions(+), 268 deletions(-) delete mode 100644 .github/VOUCHED create mode 100644 .github/VOUCHED.td diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 6ec1440e..b73df3d2 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -6,4 +6,4 @@ AGENTS.md @NVIDIA/openshell-codeowners # Vouch list — maintainers only (bot commits bypass, but manual edits need review) -.github/VOUCHED @NVIDIA/openshell-codeowners +.github/VOUCHED.td @NVIDIA/openshell-codeowners diff --git a/.github/VOUCHED b/.github/VOUCHED deleted file mode 100644 index edd25782..00000000 --- a/.github/VOUCHED +++ /dev/null @@ -1,11 +0,0 @@ -# Vouched Contributors -# -# Users listed here have been approved to submit pull requests. -# Org members and collaborators with push access bypass this check automatically. -# -# To vouch a new contributor, a maintainer comments "/vouch" on their -# Vouch Request discussion. The vouch-command workflow appends their -# username here and confirms approval. -# -# Format: one GitHub username per line, no @ prefix. -# Lines starting with # are comments. diff --git a/.github/VOUCHED.td b/.github/VOUCHED.td new file mode 100644 index 00000000..f569e39f --- /dev/null +++ b/.github/VOUCHED.td @@ -0,0 +1,10 @@ +# Vouched Contributors +# +# Users listed here have been approved to submit pull requests. +# Org members and collaborators with write access bypass this check automatically. +# +# Maintainers vouch new contributors by commenting "/vouch" on their +# Vouch Request discussion. The vouch-command workflow updates this file. +# +# Format: one GitHub username per line, no @ prefix. Sorted alphabetically. +# Prefix with - to denounce. See https://github.com/mitchellh/vouch for details. diff --git a/.github/workflows/vouch-check.yml b/.github/workflows/vouch-check.yml index 92f8ba58..874bdf0e 100644 --- a/.github/workflows/vouch-check.yml +++ b/.github/workflows/vouch-check.yml @@ -13,107 +13,9 @@ jobs: if: github.repository_owner == 'NVIDIA' runs-on: ubuntu-latest steps: - - name: Check if contributor is vouched - uses: actions/github-script@v7 + - uses: mitchellh/vouch/action/check-pr@v1 with: - script: | - const author = context.payload.pull_request.user.login; - const authorType = context.payload.pull_request.user.type; - - // 0. Skip bots (dependabot, renovate, github-actions, etc.). - if (authorType === 'Bot') { - console.log(`${author} is a bot (type: ${authorType}). Skipping vouch check.`); - return; - } - - // 1. Check org membership — members bypass the vouch gate. - try { - const { status } = await github.rest.orgs.checkMembershipForUser({ - org: context.repo.owner, - username: author, - }); - if (status === 204 || status === 302) { - console.log(`${author} is an org member. Skipping vouch check.`); - return; - } - } catch (e) { - // 404 = not a member, continue to next check. - if (e.status !== 404) { - console.log(`Org membership check error: ${e.message}`); - } - } - - // 2. Check collaborator status — direct collaborators bypass. - try { - const { status } = await github.rest.repos.checkCollaborator({ - owner: context.repo.owner, - repo: context.repo.repo, - username: author, - }); - if (status === 204) { - console.log(`${author} is a collaborator. Skipping vouch check.`); - return; - } - } catch (e) { - if (e.status !== 404) { - console.log(`Collaborator check error: ${e.message}`); - } - } - - // 3. Check the VOUCHED file. Read from the default branch, NOT the - // PR branch — the PR author could add themselves to VOUCHED in - // their fork. - let vouched = false; - try { - const { data } = await github.rest.repos.getContent({ - owner: context.repo.owner, - repo: context.repo.repo, - path: '.github/VOUCHED', - ref: context.payload.repository.default_branch, - }); - const content = Buffer.from(data.content, 'base64').toString('utf-8'); - const usernames = content - .split('\n') - .map(line => line.trim()) - .filter(line => line && !line.startsWith('#')); - vouched = usernames.some( - name => name.toLowerCase() === author.toLowerCase() - ); - } catch (e) { - console.log(`Could not read VOUCHED file: ${e.message}`); - } - - if (vouched) { - console.log(`${author} is in the VOUCHED file. Approved.`); - return; - } - - // Not vouched — close the PR with an explanation. - console.log(`${author} is not vouched. Closing PR.`); - - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.payload.pull_request.number, - body: [ - `Thank you for your interest in contributing to OpenShell, @${author}.`, - '', - 'This project uses a **vouch system** for first-time contributors. Before submitting a pull request, you need to be vouched by a maintainer.', - '', - '**To get vouched:**', - '1. Open a [Vouch Request](https://github.com/NVIDIA/OpenShell/discussions/new?category=vouch-request) discussion.', - '2. Describe what you want to change and why.', - '3. Write in your own words — do not have an AI generate the request.', - '4. A maintainer will comment `/vouch` if approved.', - '5. Once vouched, open a new PR (preferred) or reopen this one after a few minutes.', - '', - 'See [CONTRIBUTING.md](https://github.com/NVIDIA/OpenShell/blob/main/CONTRIBUTING.md#first-time-contributors) for details.', - ].join('\n'), - }); - - await github.rest.pulls.update({ - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: context.payload.pull_request.number, - state: 'closed', - }); + pr-number: ${{ github.event.pull_request.number }} + auto-close: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/vouch-command.yml b/.github/workflows/vouch-command.yml index 7570467f..a64761dc 100644 --- a/.github/workflows/vouch-command.yml +++ b/.github/workflows/vouch-command.yml @@ -4,164 +4,27 @@ on: discussion_comment: types: [created] +concurrency: + group: vouch-manage + cancel-in-progress: false + permissions: contents: write discussions: write jobs: process-vouch: - if: >- - github.repository_owner == 'NVIDIA' && - github.event.comment.body == '/vouch' + if: github.repository_owner == 'NVIDIA' runs-on: ubuntu-latest steps: - - name: Process /vouch command - uses: actions/github-script@v7 - with: - script: | - const commenter = context.payload.comment.user.login; - const discussionAuthor = context.payload.discussion.user.login; - const discussionNumber = context.payload.discussion.number; - - // --- Helpers --- - - async function getDiscussionId() { - const query = `query($owner: String!, $repo: String!, $number: Int!) { - repository(owner: $owner, name: $repo) { - discussion(number: $number) { id } - } - }`; - const { repository } = await github.graphql(query, { - owner: context.repo.owner, - repo: context.repo.repo, - number: discussionNumber, - }); - return repository.discussion.id; - } - - async function postDiscussionComment(body) { - const discussionId = await getDiscussionId(); - const mutation = `mutation($discussionId: ID!, $body: String!) { - addDiscussionComment(input: {discussionId: $discussionId, body: $body}) { - comment { id } - } - }`; - await github.graphql(mutation, { discussionId, body }); - } - - // --- Authorization --- - - let isMaintainer = false; - try { - const { data } = await github.rest.repos.getCollaboratorPermissionLevel({ - owner: context.repo.owner, - repo: context.repo.repo, - username: commenter, - }); - const level = data.permission; - isMaintainer = ['admin', 'maintain', 'write'].includes(level); - } catch (e) { - console.log(`Permission check failed: ${e.message}`); - } - - if (!isMaintainer) { - console.log(`${commenter} does not have maintainer permissions. Ignoring.`); - return; - } - - // --- Read VOUCHED file --- - - const filePath = '.github/VOUCHED'; - const branch = context.payload.repository.default_branch; - const commitMessage = - `chore: vouch ${discussionAuthor}\n\nSigned-off-by: github-actions[bot] `; - - let currentContent = ''; - let sha = ''; - try { - const { data } = await github.rest.repos.getContent({ - owner: context.repo.owner, - repo: context.repo.repo, - path: filePath, - ref: branch, - }); - currentContent = Buffer.from(data.content, 'base64').toString('utf-8'); - sha = data.sha; - } catch (e) { - console.log(`Could not read VOUCHED file: ${e.message}`); - return; - } + - uses: actions/checkout@v4 - // --- Check if already vouched --- - - function parseUsernames(content) { - return content - .split('\n') - .map(line => line.trim()) - .filter(line => line && !line.startsWith('#')); - } - - function isVouched(content, username) { - return parseUsernames(content).some( - name => name.toLowerCase() === username.toLowerCase() - ); - } - - if (isVouched(currentContent, discussionAuthor)) { - console.log(`${discussionAuthor} is already vouched.`); - await postDiscussionComment( - `@${discussionAuthor} is already vouched. They can submit pull requests.` - ); - return; - } - - // --- Append username and commit --- - - async function commitVouch(content, fileSha) { - const updatedContent = content.trimEnd() + '\n' + discussionAuthor + '\n'; - await github.rest.repos.createOrUpdateFileContents({ - owner: context.repo.owner, - repo: context.repo.repo, - path: filePath, - message: commitMessage, - content: Buffer.from(updatedContent).toString('base64'), - sha: fileSha, - branch, - }); - } - - try { - await commitVouch(currentContent, sha); - } catch (e) { - if (e.status === 409) { - // Concurrent write — re-read and retry once. - console.log('409 conflict. Re-reading VOUCHED file and retrying.'); - const { data: fresh } = await github.rest.repos.getContent({ - owner: context.repo.owner, - repo: context.repo.repo, - path: filePath, - ref: branch, - }); - const freshContent = Buffer.from(fresh.content, 'base64').toString('utf-8'); - - if (isVouched(freshContent, discussionAuthor)) { - console.log(`${discussionAuthor} was vouched by a concurrent operation.`); - } else { - await commitVouch(freshContent, fresh.sha); - } - } else { - throw e; - } - } - - // --- Confirm --- - - await postDiscussionComment([ - `@${discussionAuthor} has been vouched by @${commenter}.`, - '', - 'You can now submit pull requests to OpenShell. Welcome aboard.', - '', - 'Please read [CONTRIBUTING.md](https://github.com/NVIDIA/OpenShell/blob/main/CONTRIBUTING.md) before submitting.', - ].join('\n')); - - console.log(`Vouched ${discussionAuthor} (approved by ${commenter}).`); + - uses: mitchellh/vouch/action/manage-by-discussion@v1 + with: + discussion-number: ${{ github.event.discussion.number }} + comment-node-id: ${{ github.event.comment.node_id }} + vouch-keyword: "/vouch" + denounce-keyword: "/denounce" + unvouch-keyword: "/unvouch" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/AGENTS.md b/AGENTS.md index 894d6de9..f5cf5269 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -49,7 +49,7 @@ These pipelines connect skills into end-to-end workflows. Individual skill files - First-time external contributors must be vouched before their PRs are accepted. The `vouch-check` workflow auto-closes PRs from unvouched users. - Org members and collaborators bypass the vouch gate automatically. -- Maintainers vouch users by commenting `/vouch` on a Vouch Request discussion. The `vouch-command` workflow appends the username to `.github/VOUCHED`. +- Maintainers vouch users by commenting `/vouch` on a Vouch Request discussion. The `vouch-command` workflow appends the username to `.github/VOUCHED.td`. - Skills that create PRs (`create-github-pr`, `build-from-issue`) should note this requirement when operating on behalf of external contributors. ## Issue and PR Conventions From adf335e857f77e9e08fc79bccba651ef08e6461d Mon Sep 17 00:00:00 2001 From: John Myers Date: Mon, 16 Mar 2026 14:54:36 -0700 Subject: [PATCH 3/4] chore: simplify vouch request template to what and why Signed-off-by: John Myers --- .github/DISCUSSION_TEMPLATE/vouch-request.yml | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/.github/DISCUSSION_TEMPLATE/vouch-request.yml b/.github/DISCUSSION_TEMPLATE/vouch-request.yml index 6b49efcf..58d98e30 100644 --- a/.github/DISCUSSION_TEMPLATE/vouch-request.yml +++ b/.github/DISCUSSION_TEMPLATE/vouch-request.yml @@ -34,17 +34,6 @@ body: validations: required: true - - type: textarea - id: approach - attributes: - label: How do you plan to implement it? - description: > - Brief description of your approach. You don't need a full design doc, - but show that you understand the relevant code. - placeholder: "The timeout is set in crates/openshell-sandbox/src/runtime.rs. I'd change..." - validations: - required: true - - type: checkboxes id: checklist attributes: @@ -54,5 +43,3 @@ body: required: true - label: I have read [CONTRIBUTING.md](https://github.com/NVIDIA/OpenShell/blob/main/CONTRIBUTING.md) required: true - - label: I understand that OpenShell is agent-first — I will use agents as tools but I own and understand my code - required: true From 45c761daed859a7f7cbffed72795ece55fd181e2 Mon Sep 17 00:00:00 2001 From: John Myers Date: Mon, 16 Mar 2026 15:00:56 -0700 Subject: [PATCH 4/4] chore: remove redundant AI-generated prose warning Signed-off-by: John Myers --- CONTRIBUTING.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f3966ddb..88a30acb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,7 +16,6 @@ OpenShell is agent-first, not agent-only. The distinction matters: - **Do** use the skills in `.agents/skills/` — they exist to make your agent effective. - **Do** interrogate your agent until you understand every edge case and interaction in your changes. - **Don't** submit code you can't explain without your agent open. -- **Don't** have an AI write your issue reports, vouch requests, or PR descriptions. We can tell. - **Don't** use agents as a substitute for understanding the system. Read the architecture docs. ## First-Time Contributors