From 63801778381800dfe5b2fea0041e1f58a8b84686 Mon Sep 17 00:00:00 2001 From: Mirko Brombin Date: Sun, 13 Sep 2026 13:15:00 +0200 Subject: [PATCH] ci: enforce AI disclosure Assisted-by: Codex:GPT-5 AI-Scope: Added repository instructions, README disclosure notice, pull request validation, workflow, and tests for the requested policy. --- .github/PULL_REQUEST_TEMPLATE.md | 8 ++ .github/copilot-instructions.md | 1 + .github/scripts/check-ai-disclosure.cjs | 81 ++++++++++++++++++++ .github/scripts/check-ai-disclosure.test.cjs | 71 +++++++++++++++++ .github/workflows/ai-disclosure.yml | 41 ++++++++++ AGENTS.md | 23 ++++++ CLAUDE.md | 1 + GEMINI.md | 4 + README.md | 18 +++++ 9 files changed, 248 insertions(+) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/copilot-instructions.md create mode 100644 .github/scripts/check-ai-disclosure.cjs create mode 100644 .github/scripts/check-ai-disclosure.test.cjs create mode 100644 .github/workflows/ai-disclosure.yml create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 100644 GEMINI.md diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..4260918 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,8 @@ +## Generative AI assistance + +AI assistance: none + +Change `none` to `trivial` for exempt single-line completions, renames, or +formatting. Change it to `disclosed` when assisted commits contain the +`Assisted-by` and `AI-Scope` trailers required by +[AGENTS.md](https://github.com/singularityos-lab/atom/blob/main/AGENTS.md). diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..dba71e9 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1 @@ +@../AGENTS.md diff --git a/.github/scripts/check-ai-disclosure.cjs b/.github/scripts/check-ai-disclosure.cjs new file mode 100644 index 0000000..438a615 --- /dev/null +++ b/.github/scripts/check-ai-disclosure.cjs @@ -0,0 +1,81 @@ +const allowedModes = new Set(["none", "trivial", "disclosed"]); + +function disclosureMode(body) { + const lines = body + .split(/\r?\n/) + .filter((line) => /^\s*AI assistance:/i.test(line)); + + if (lines.length === 0) return { mode: "none" }; + if (lines.length > 1) { + return { error: "Keep one AI assistance declaration in the pull request." }; + } + + const mode = lines[0].split(":", 2)[1].trim().toLowerCase(); + if (!allowedModes.has(mode)) { + return { error: "AI assistance must be none, trivial, or disclosed." }; + } + return { mode }; +} + +function commitDisclosure(commit) { + const assistedLines = commit.message + .split(/\r?\n/) + .filter((line) => /^Assisted-by:/i.test(line)); + const scopeLines = commit.message + .split(/\r?\n/) + .filter((line) => /^AI(?:-| )Scope:/i.test(line)); + const validAssisted = assistedLines.every((line) => + /^Assisted-by:\s*[^:\r\n]+:[^\s\r\n]+\s*$/i.test(line), + ); + const validScope = scopeLines.every((line) => + /^AI-Scope:\s*\S(?:.*\S)?\s*$/i.test(line), + ); + const disclosed = assistedLines.length > 0 || scopeLines.length > 0; + + return { + disclosed, + valid: + disclosed && + assistedLines.length > 0 && + scopeLines.length > 0 && + validAssisted && + validScope, + }; +} + +function checkDisclosure(body, commits) { + const errors = []; + const declared = disclosureMode(body); + if (declared.error) errors.push(declared.error); + + let disclosed = 0; + for (const commit of commits) { + const result = commitDisclosure(commit); + if (!result.disclosed) continue; + disclosed++; + if (!result.valid) { + errors.push( + `${commit.sha.slice(0, 7)} must contain valid Assisted-by and AI-Scope trailers.`, + ); + } + } + + if (!declared.error && declared.mode === "disclosed" && disclosed === 0) { + errors.push( + "AI assistance is disclosed, but no commit contains the required trailers.", + ); + } + if ( + !declared.error && + declared.mode !== "disclosed" && + disclosed > 0 + ) { + errors.push( + "Set AI assistance to disclosed when a commit contains disclosure trailers.", + ); + } + + return errors; +} + +module.exports = { checkDisclosure }; diff --git a/.github/scripts/check-ai-disclosure.test.cjs b/.github/scripts/check-ai-disclosure.test.cjs new file mode 100644 index 0000000..06d219b --- /dev/null +++ b/.github/scripts/check-ai-disclosure.test.cjs @@ -0,0 +1,71 @@ +const test = require("node:test"); +const assert = require("node:assert/strict"); +const { checkDisclosure } = require("./check-ai-disclosure.cjs"); + +const plainCommit = { sha: "1111111", message: "fix: repair launch" }; +const assistedCommit = { + sha: "2222222", + message: + "docs: explain launch\n\nAssisted-by: Codex:gpt-5\nAI-Scope: Drafted the documentation from the issue description.", +}; + +test("accepts work without AI assistance", () => { + assert.deepEqual(checkDisclosure("", [plainCommit]), []); + assert.deepEqual( + checkDisclosure("AI assistance: none", [plainCommit]), + [], + ); +}); + +test("accepts exempt trivial completions", () => { + assert.deepEqual( + checkDisclosure("AI assistance: trivial", [plainCommit]), + [], + ); +}); + +test("accepts a disclosed assisted commit", () => { + assert.deepEqual( + checkDisclosure("AI assistance: disclosed", [plainCommit, assistedCommit]), + [], + ); +}); + +test("requires trailers for disclosed assistance", () => { + assert.match( + checkDisclosure("AI assistance: disclosed", [plainCommit])[0], + /no commit contains/, + ); +}); + +test("requires the pull request to match its trailers", () => { + assert.match( + checkDisclosure("AI assistance: none", [assistedCommit])[0], + /Set AI assistance to disclosed/, + ); +}); + +test("rejects incomplete and legacy trailers", () => { + const commit = { + sha: "3333333", + message: "docs: update\n\nAssisted-by: Codex:gpt-5\nAI scope: docs", + }; + assert.match( + checkDisclosure("AI assistance: disclosed", [commit])[0], + /must contain valid/, + ); +}); + +test("rejects invalid or repeated declarations", () => { + assert.match( + checkDisclosure("AI assistance: sometimes", [plainCommit])[0], + /must be none, trivial, or disclosed/, + ); + assert.match( + checkDisclosure( + "AI assistance: none\nAI assistance: disclosed", + [plainCommit], + )[0], + /Keep one/, + ); +}); diff --git a/.github/workflows/ai-disclosure.yml b/.github/workflows/ai-disclosure.yml new file mode 100644 index 0000000..a445951 --- /dev/null +++ b/.github/workflows/ai-disclosure.yml @@ -0,0 +1,41 @@ +name: AI disclosure + +on: + pull_request_target: + types: [opened, synchronize, reopened, edited] + +permissions: + contents: read + pull-requests: read + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + ref: ${{ github.event.pull_request.base.sha }} + persist-credentials: false + + - name: Test disclosure policy + run: node --test .github/scripts/check-ai-disclosure.test.cjs + + - name: Check pull request + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd + with: + script: | + const { checkDisclosure } = require("./.github/scripts/check-ai-disclosure.cjs"); + const commits = await github.paginate(github.rest.pulls.listCommits, { + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number, + per_page: 100, + }); + const errors = checkDisclosure( + context.payload.pull_request.body || "", + commits.map((commit) => ({ + sha: commit.sha, + message: commit.commit.message, + })), + ); + if (errors.length > 0) core.setFailed(errors.join("\n")); diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..93051c1 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,23 @@ +# Repository instructions + +## Generative AI disclosure + +Read this section before changing files, creating a commit, or opening a pull +request. + +When generative AI contributes more than a trivial completion, add both of +these trailers to every assisted commit: + +```plain +Assisted-by: : +AI-Scope: +``` + +Single-line completions, renames, and formatting changes do not need trailers. +Work completed without generative AI does not need trailers. + +Before opening a pull request, set `AI assistance` in the pull request template +to `none`, `trivial`, or `disclosed`. Use `disclosed` when at least one commit +has the trailers above. Review every assisted change before submission. + +Do not create the commit or pull request until its disclosure matches the work. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 0000000..61a8dc8 --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,4 @@ +# Repository instructions + +Read and follow [AGENTS.md](AGENTS.md) before changing files, creating a commit, +or opening a pull request. diff --git a/README.md b/README.md index 7a9b09a..acbbd6f 100644 --- a/README.md +++ b/README.md @@ -36,3 +36,21 @@ atom does not manage OS updates. Boot confirmation, rollback and the update flow ## License GPL-3.0-only. See [LICENSE](LICENSE). + +## Use of Generative AI + +Maintainers may use generative AI tools as assistants while working on atom. Non-trivial assisted commits disclose the tool, model, and scope of the work. + +AI tools may assist with code comments, documentation, repetitive code, and issue triage. Maintainers make project decisions and review every assisted change before it is merged. + +Use these trailers for non-trivial assisted commits: + +```plain +Assisted-by: : +AI-Scope: +``` + +Single-line completions, renames, and formatting changes do not need trailers. + +Coding agents must also follow [AGENTS.md](AGENTS.md) before changing files, +creating commits, or opening pull requests.