From 3f50cfc0c30abcbfcf434dc03f2abb82b2311c96 Mon Sep 17 00:00:00 2001 From: nothingnesses <18732253+nothingnesses@users.noreply.github.com> Date: Tue, 1 Sep 2026 17:35:57 +0100 Subject: [PATCH] fix: enforce repository attribution Require the established author identity and reject co-author trailers across complete branch history. Keep pull request tests on the merge result while the metadata check scans the branch head. Add isolated regression cases for raw identities, shallow history, and repository-location environment variables. --- .agents/AGENTS.reference.md | 1 + .agents/checks/attribution-test.sh | 189 +++++++++++++++++++++++++++++ .agents/checks/attribution.sh | 94 ++++++++++++++ .agents/checks/ci-gate.sh | 8 ++ .agents/work.toml | 74 +++-------- .github/workflows/ci.yml | 12 ++ AGENTS.md | 1 + README.md | 2 +- pack/AGENTS.md | 1 + 9 files changed, 324 insertions(+), 58 deletions(-) create mode 100755 .agents/checks/attribution-test.sh create mode 100755 .agents/checks/attribution.sh diff --git a/.agents/AGENTS.reference.md b/.agents/AGENTS.reference.md index 3f111089..0580bd09 100644 --- a/.agents/AGENTS.reference.md +++ b/.agents/AGENTS.reference.md @@ -44,6 +44,7 @@ The human owns scope and acceptance. Do not add a step, change acceptance criter - Treat external input as untrusted, keep secrets out of source and logs, and fail loudly on errors. - Format only files you changed. Run destructive checks in scratch space, not over unrelated live files. - Keep the working tree recoverable. Inspect status and diffs before committing; do not discard files you do not own. +- Commit with the repository owner's configured Git identity. Keep agent, model, and LLM attribution out of commit messages, pull request titles, and pull request bodies, and add no co-author trailer. - Report the commit or diff, changed files, commands run, results, and any unresolved risk. ## Principles diff --git a/.agents/checks/attribution-test.sh b/.agents/checks/attribution-test.sh new file mode 100755 index 00000000..4f383969 --- /dev/null +++ b/.agents/checks/attribution-test.sh @@ -0,0 +1,189 @@ +#!/usr/bin/env bash +# Deterministic proof of `.agents/checks/attribution.sh`. Each case builds a +# throwaway repository under the temporary directory, so no case reads or writes +# the tracked tree, and `.agents/checks/ci-gate.sh` runs this file before it reads +# the live history. +# +# Every case supplies its own identity, drops host Git configuration, and clears the +# variables that name a repository, so a maintainer's own name, a global mailmap, a +# signing setting, or an inherited `GIT_DIR` cannot change a result. +set -euo pipefail + +check=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/attribution.sh + +readonly owner_name='nothingnesses' +readonly owner_email='18732253+nothingnesses@users.noreply.github.com' + +export GIT_CONFIG_GLOBAL=/dev/null +export GIT_CONFIG_SYSTEM=/dev/null +export GIT_CONFIG_NOSYSTEM=1 + +# Git honours these over `-C` and over any repository configuration, so an inherited +# value would send every command below, and the check this file runs, at the caller's +# repository instead of the scratch one: the cases would then read a history they did +# not build, and a case that commits would write into that repository. `unset` drops +# them from the environment, so the check inherits the cleared set too. +unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_OBJECT_DIRECTORY \ + GIT_ALTERNATE_OBJECT_DIRECTORIES GIT_COMMON_DIR GIT_NAMESPACE + +scratch=$(mktemp -d) +trap 'rm -rf "${scratch}"' EXIT + +cases=0 +failures=0 +commits=0 + +new_repository() { + local repository=${scratch}/$1 + mkdir "${repository}" + git -C "${repository}" init --quiet --initial-branch=main + printf '%s' "${repository}" +} + +# The author identity travels in the environment, which outranks configuration, so +# a `GIT_AUTHOR_NAME` in the caller's shell cannot reach the commit. `verbatim` +# cleanup stores the message exactly as the case writes it. +add_commit() { + local repository=$1 name=$2 email=$3 message=$4 + commits=$((commits + 1)) + printf 'change %d\n' "${commits}" >>"${repository}/history.txt" + git -C "${repository}" add history.txt + GIT_AUTHOR_NAME=${name} GIT_AUTHOR_EMAIL=${email} \ + GIT_COMMITTER_NAME=${name} GIT_COMMITTER_EMAIL=${email} \ + git -C "${repository}" commit --quiet --cleanup=verbatim --message "${message}" +} + +record_failure() { + failures=$((failures + 1)) + printf 'FAIL %s: %s\n' "$1" "$2" >&2 +} + +# Each case names the scratch repository's own `HEAD` as the target, so an +# `ATTRIBUTION_TARGET` the caller set, as GitHub CI does, cannot reach a case and +# turn a built history into an unresolvable one. +expect_pass() { + local label=$1 repository=$2 output status=0 + cases=$((cases + 1)) + output=$(ATTRIBUTION_TARGET=HEAD bash "${check}" "${repository}" 2>&1) || status=$? + if ((status != 0)); then + record_failure "${label}" "the check rejected a valid history: ${output}" + return + fi + printf 'ok %s\n' "${label}" +} + +# The report has to name what it rejected and give the reason. The subject is the +# offending commit for a history violation, and the repository itself where the walk +# stops before it reads a commit. +expect_failure() { + local label=$1 repository=$2 subject=$3 reason=$4 output status=0 + cases=$((cases + 1)) + output=$(ATTRIBUTION_TARGET=HEAD bash "${check}" "${repository}" 2>&1) || status=$? + if ((status == 0)); then + record_failure "${label}" 'the check accepted a repository it must reject' + return + fi + if [[ ${output} != *"${subject}"* ]]; then + record_failure "${label}" "the report does not name ${subject}: ${output}" + return + fi + if [[ ${output} != *"${reason}"* ]]; then + record_failure "${label}" "the report does not give the reason: ${output}" + return + fi + printf 'ok %s\n' "${label}" +} + +readonly foreign_author='is not the repository owner' +readonly foreign_trailer='has a Co-Authored-By trailer' +readonly shallow_history='is shallow, so the check cannot read the complete history' + +repository=$(new_repository valid-history) +add_commit "${repository}" "${owner_name}" "${owner_email}" 'feat: add the first change' +add_commit "${repository}" "${owner_name}" "${owner_email}" \ + $'feat: add the second change\n\nThe body names Co-Authored-By in a sentence, which is not a trailer.' +add_commit "${repository}" "${owner_name}" "${owner_email}" 'docs: describe the second change' +expect_pass 'a valid history passes' "${repository}" + +repository=$(new_repository foreign-author) +add_commit "${repository}" "${owner_name}" "${owner_email}" 'feat: add the first change' +add_commit "${repository}" 'Some Agent' 'agent@example.invalid' 'feat: add a foreign change' +expect_failure 'a foreign author fails' "${repository}" \ + "$(git -C "${repository}" rev-parse HEAD)" "${foreign_author}" + +repository=$(new_repository placeholder-author) +add_commit "${repository}" 'Test' 'test@example.com' 'feat: add a placeholder change' +expect_failure 'a placeholder author fails' "${repository}" \ + "$(git -C "${repository}" rev-parse HEAD)" "${foreign_author}" + +repository=$(new_repository another-name) +add_commit "${repository}" 'Another Name' "${owner_email}" 'feat: add a renamed change' +expect_failure 'the owner email under another name fails' "${repository}" \ + "$(git -C "${repository}" rev-parse HEAD)" "${foreign_author}" + +repository=$(new_repository another-email) +add_commit "${repository}" "${owner_name}" 'nothingnesses@example.invalid' \ + 'feat: add a rerouted change' +expect_failure 'the owner name under another email fails' "${repository}" \ + "$(git -C "${repository}" rev-parse HEAD)" "${foreign_author}" + +repository=$(new_repository co-author-trailer) +add_commit "${repository}" "${owner_name}" "${owner_email}" \ + $'feat: add a shared change\n\nCo-Authored-By: Some Agent ' +expect_failure 'a co-author trailer fails' "${repository}" \ + "$(git -C "${repository}" rev-parse HEAD)" "${foreign_trailer}" + +repository=$(new_repository lower-case-trailer) +add_commit "${repository}" "${owner_name}" "${owner_email}" \ + $'feat: add a padded change\n\n co-authored-by : Some Agent ' +expect_failure 'a lower-case padded co-author trailer fails' "${repository}" \ + "$(git -C "${repository}" rev-parse HEAD)" "${foreign_trailer}" + +repository=$(new_repository upper-case-trailer) +add_commit "${repository}" "${owner_name}" "${owner_email}" \ + $'feat: add an indented change\n\n\tCO-AUTHORED-BY:\tSome Agent ' +expect_failure 'an upper-case indented co-author trailer fails' "${repository}" \ + "$(git -C "${repository}" rev-parse HEAD)" "${foreign_trailer}" + +# The rewritten history put the removed identities and trailers below the tip, so +# a check that read only the tip commit would report a clean repository. +repository=$(new_repository ancestor-violation) +add_commit "${repository}" 'Some Agent' 'agent@example.invalid' 'feat: add the first change' +ancestor=$(git -C "${repository}" rev-parse HEAD) +add_commit "${repository}" "${owner_name}" "${owner_email}" 'feat: add the second change' +add_commit "${repository}" "${owner_name}" "${owner_email}" 'docs: describe both changes' +expect_failure 'a violation below the tip fails' "${repository}" "${ancestor}" "${foreign_author}" + +# A `.mailmap` rewrites the identity that upper-case `%aN` and `%aE` report, so a +# check reading those would see the owner here and accept the history. The rule is +# about the stored identity, which lower-case `%an` and `%ae` report, and the report +# has to keep naming the commit that carries the foreign one. +repository=$(new_repository mailmap-rewrite) +add_commit "${repository}" 'Some Agent' 'agent@example.invalid' 'feat: add a foreign change' +foreign=$(git -C "${repository}" rev-parse HEAD) +printf '%s <%s> Some Agent \n' "${owner_name}" "${owner_email}" \ + >"${repository}/.mailmap" +git -C "${repository}" add .mailmap +add_commit "${repository}" "${owner_name}" "${owner_email}" 'chore: record the mailmap' +expect_failure 'a mailmap rewrite of a foreign author fails' "${repository}" \ + "${foreign}" "${foreign_author}" + +# A shallow clone holds the tip and hides its ancestors, so a check without the +# shallow guard would read the one accepted commit this clone keeps and report a +# history whose root it never saw as clean. `--depth` needs a transport rather than a +# local copy, hence the `file://` URL. The rejection names the repository, because the +# walk stops before it reads a commit. +source_repository=$(new_repository shallow-source) +add_commit "${source_repository}" 'Some Agent' 'agent@example.invalid' \ + 'feat: add the first change' +add_commit "${source_repository}" "${owner_name}" "${owner_email}" \ + 'feat: add the second change' +clone=${scratch}/shallow-clone +git clone --quiet --depth 1 "file://${source_repository}" "${clone}" +expect_failure 'a shallow clone fails' "${clone}" "${clone}" "${shallow_history}" + +if ((failures > 0)); then + printf 'error: attribution tests: %d of %d cases failed\n' "${failures}" "${cases}" >&2 + exit 1 +fi +printf 'attribution tests: %d cases passed\n' "${cases}" diff --git a/.agents/checks/attribution.sh b/.agents/checks/attribution.sh new file mode 100755 index 00000000..c26d2e54 --- /dev/null +++ b/.agents/checks/attribution.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash +# The attribution gate. It reads every commit reachable from its target and holds +# two rules the human selected: each commit carries the repository owner's raw +# author identity, and no commit message carries a co-author trailer. +# +# `.agents/checks/ci-gate.sh` runs `.agents/checks/attribution-test.sh` first, so +# scratch repositories prove both rules before this file reads the live history. +# +# Read-only: it runs `git rev-parse`, `git log`, and `git rev-list`, and writes +# nothing. +# +# Usage: attribution.sh [] +# +# `ATTRIBUTION_TARGET` names the commit to scan from; it defaults to `HEAD`. On a +# pull request GitHub CI keeps the merge-result checkout, so that builds and tests +# run against the merge, and sets this to the head commit of the branch instead: +# the merge commit's own author is GitHub's, which no allowlist can accept, and +# the branch commits are what the rules are about. +set -euo pipefail + +# The allowlist, as one exact name and one exact email. It takes no environment +# override, so nothing that runs the gate can widen it. +readonly owner_name='nothingnesses' +readonly owner_email='18732253+nothingnesses@users.noreply.github.com' + +repository=${1:-.} +target=${ATTRIBUTION_TARGET:-HEAD} + +# ASCII case folding and ASCII character classes, whatever locale the host sets. +export LC_ALL=C + +# A target the repository does not hold, such as a head SHA a partial fetch left +# out, would otherwise stop the walk before it read anything. +if ! git -C "${repository}" rev-parse --verify --quiet "${target}^{commit}" >/dev/null; then + printf 'error: %s has no commit at %s\n' "${repository}" "${target}" >&2 + exit 1 +fi + +# A shallow clone hides ancestors, so the walk below would read a partial history +# and report it as a complete one. +if [[ $(git -C "${repository}" rev-parse --is-shallow-repository) == true ]]; then + printf 'error: %s is shallow, so the check cannot read the complete history\n' \ + "${repository}" >&2 + exit 1 +fi + +scan() { + local failures=0 record commit name email line folded + local -a lines + while IFS= read -r -d '' record; do + # git separates entries with a newline, so every record after the first + # arrives with one in front of the commit hash. + record=${record#$'\n'} + mapfile -t lines <<<"${record}" + commit=${lines[0]} + name=${lines[1]} + email=${lines[2]} + if [[ ${name} != "${owner_name}" || ${email} != "${owner_email}" ]]; then + printf 'error: %s: author "%s <%s>" is not the repository owner "%s <%s>"\n' \ + "${commit}" "${name}" "${email}" "${owner_name}" "${owner_email}" >&2 + failures=$((failures + 1)) + fi + # The report names the rule and the commit, never the trailer line: the line + # holds a person's name and email, and the hash already locates it. + for line in "${lines[@]:3}"; do + folded=${line,,} + if [[ ${folded} =~ ^[[:blank:]]*co-authored-by[[:blank:]]*: ]]; then + printf 'error: %s: the commit message has a Co-Authored-By trailer\n' \ + "${commit}" >&2 + failures=$((failures + 1)) + break + fi + done + done + if ((failures > 0)); then + printf 'error: attribution: %d violations in the history reachable from %s\n' \ + "${failures}" "${target}" >&2 + return 1 + fi + return 0 +} + +# Lower-case `%an` and `%ae` report the stored identity, which is the identity the +# rule is about. A `.mailmap` reaches only their upper-case forms, whatever +# `log.mailmap` is set to, so no flag here supplies that protection and the format +# string alone carries it. The mailmap case in `.agents/checks/attribution-test.sh` +# holds the requirement: it maps a foreign author onto the owner and still expects a +# rejection, so a change to `%aN` and `%aE` fails there. +if ! git -C "${repository}" log --format='%H%n%an%n%ae%n%B%x00' "${target}" | scan; then + exit 1 +fi + +printf 'attribution: %s commits reachable from %s carry the owner identity and no co-author trailer\n' \ + "$(git -C "${repository}" rev-list --count "${target}")" "${target}" diff --git a/.agents/checks/ci-gate.sh b/.agents/checks/ci-gate.sh index 4b91be52..1bf9d8d0 100755 --- a/.agents/checks/ci-gate.sh +++ b/.agents/checks/ci-gate.sh @@ -70,6 +70,14 @@ if unpinned=$(grep -rEn '^[[:space:]]*-?[[:space:]]*uses:' .github/workflows | exit 1 fi +step 'attribution tests' +# The proof runs before the live history, so a check that had stopped rejecting a +# foreign author or a co-author trailer fails here rather than passing silently. +bash .agents/checks/attribution-test.sh + +step 'attribution' +bash .agents/checks/attribution.sh + step 'tracked tree' tracked_after=$(git status --porcelain --untracked-files=no) if [[ ${tracked_after} != "${tracked_before}" ]]; then diff --git a/.agents/work.toml b/.agents/work.toml index 5926bc88..1ce1525f 100644 --- a/.agents/work.toml +++ b/.agents/work.toml @@ -1,65 +1,25 @@ version = 1 -selected_action = "explore-general-review-surface" [[step]] -id = "enforce-reset-guardrails" +id = "prevent-false-attribution" status = "complete" blocked_by = [] -user_problem = "Nothing outside this repository stops a change that breaks the build or revives a deleted reset artefact." -change = "Add one locked-toolchain GitHub CI workflow plus one shared local gate, then have the human protect main with a required pull request and that check." -acceptance = [ - "One read-only job runs on pushes to main and on pull requests.", - "It runs the accepted cargo formatting check, strict Clippy, locked tests, repository checks, real work validation, reset tripwires and actionlint in the locked Nix environment.", - "It never runs nix fmt.", - "Actions are pinned to immutable commit SHAs.", - "Local and CI runs share one gate source.", - "The tracked tree stays clean after a run.", - "Remote protection requires a pull request and this check, with zero approvals.", -] -why_next = "Every later action is safer once a mechanical gate, not a promise, holds the reset boundary." - -[[step]] -id = "support-safe-work-paragraphs" -status = "complete" -blocked_by = ["enforce-reset-guardrails"] -user_problem = "Every work field must fit one line, so a step cannot explain its problem without cramming it." -change = "Let prose fields carry paragraphs while ids and structural fields stay single-line, and keep both projections safe." -acceptance = [ - "IDs, statuses, blockers and the selected action stay single-line.", - "Prose fields accept paragraphs.", - "Unsafe control characters are still rejected.", - "Human output renders paragraphs without heading injection.", - "JSON preserves paragraph structure.", -] -why_next = "Bounded state is only worth trusting if a step can state its problem honestly inside the size limits." - -[[step]] -id = "explore-general-review-surface" -status = "active" -blocked_by = ["support-safe-work-paragraphs"] -user_problem = "The scaffold supports delivery review, but not a standalone review of a current tree or a diff between two states." -change = "Compare a compact prompt, a dedicated CLI command and no product surface, then recommend one." -acceptance = [ - "Representative current-tree and two-ref review tasks drive the comparison.", - "Every option is judged against the same tasks.", - "The result is a capped scratch decision brief.", - "No product code, pack asset or shipped documentation changes.", -] -why_next = "Deciding the surface first avoids shipping a command the workflow turns out not to need." +user_problem = """ +Commits can name an agent or an LLM as the author or as a co-author, which credits work to someone who did not do it and adds a false contributor to the repository. -[[step]] -id = "investigate-workflow-failure-causes" -status = "pending" -blocked_by = ["explore-general-review-surface"] -user_problem = "The August audit dated the workflow failure and proved immediate mechanisms, but did not identify what caused them to start or worsen." -change = "Trace initiating causes and test delivery-value signals across code, agent-facing text and generated output." +A rewrite of the history fixed the record already. All 1512 commits reachable from the current main now carry the raw author nothingnesses <18732253+nothingnesses@users.noreply.github.com>, seven Co-Authored-By trailers are gone, and the repository-local Test identity override is gone. Nothing stops the next commit from undoing that work.""" +change = """ +Add a read-only attribution check over every commit reachable from HEAD, prove it with scratch repositories, run both from the shared just ci gate, fetch the complete history in GitHub CI, and state the rule in the shipped guidance.""" acceptance = [ - "Hypotheses and evidence grades are fixed before measurement.", - "The dated change points are traced through human messages, prompts and Git history.", - "Matched work before and after the transition tests each candidate cause.", - "Results separate demonstrated causes, supported causes and unknowns.", - "Each supported cause maps to a bounded preventive control.", - "Self-certified process metrics are excluded.", - "The result is a scratch decision brief of at most 20000 bytes.", + "just ci checks the complete reachable history, locally and in GitHub CI.", + "The gate accepts only the raw author identity nothingnesses <18732253+nothingnesses@users.noreply.github.com>.", + "Any Co-Authored-By trailer fails, whatever its case and whatever horizontal whitespace surrounds the key and the colon.", + "A report names the offending commit and the rule it broke, and prints no other commit body content.", + "Scratch repositories outside the tracked tree prove the passing case, a foreign author, a placeholder author, and a co-author trailer, under temporary Git configuration.", + "The GitHub checkout fetches the complete history, so the remote run cannot inspect only the tip, and it keeps the default merge-result ref, so a pull request still builds and tests the merge rather than the branch tip alone.", + "On a pull request the remote run scans the commits of the branch rather than the synthetic author of GitHub's merge commit.", + "The shipped guidance requires the owner identity and forbids agent and LLM attribution in commits and pull requests.", + "The gate stays read-only over the tracked tree, and the existing formatting, Clippy, test, reset tripwire, action pin, and clean-tree checks still pass.", ] -why_next = "Causal evidence can prevent another failure without turning activity counts into another target." +why_next = """ +The history is clean today only because a human rewrote it. A mechanical gate keeps it clean without asking every later author to remember the rule, and it is cheap to add while the evidence of what went wrong is fresh.""" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6360fc75..2ac117ff 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,6 +23,12 @@ jobs: # Pinned to immutable commit SHAs, not tags: a tag can be moved onto other code. - name: Check out the commit under test uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + with: + # The attribution check reads every commit reachable from its target, so + # the default single-commit checkout would hide all but the tip. The ref + # stays the default, so a pull request still builds and tests the merge + # result rather than the branch tip alone. + fetch-depth: 0 - name: Install Nix uses: DeterminateSystems/nix-installer-action@ef8a148080ab6020fd15196c2084a2eea5ff2d25 # v22 # The committed flake supplies the locked toolchain; `.agents/checks/ci-gate.sh` @@ -31,4 +37,10 @@ jobs: - name: Run the shared gate env: SKIP_DIRENV: "1" + # On a pull request the checked out tip is a merge commit GitHub itself + # authors, which no author allowlist can accept. Point the attribution + # check at the head commit of the branch, and at the pushed commit on a + # push. The merge commit has that head as a parent, so the fetched + # history already contains it. + ATTRIBUTION_TARGET: ${{ github.event.pull_request.head.sha || github.sha }} run: nix develop --command just ci diff --git a/AGENTS.md b/AGENTS.md index 3f111089..0580bd09 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -44,6 +44,7 @@ The human owns scope and acceptance. Do not add a step, change acceptance criter - Treat external input as untrusted, keep secrets out of source and logs, and fail loudly on errors. - Format only files you changed. Run destructive checks in scratch space, not over unrelated live files. - Keep the working tree recoverable. Inspect status and diffs before committing; do not discard files you do not own. +- Commit with the repository owner's configured Git identity. Keep agent, model, and LLM attribution out of commit messages, pull request titles, and pull request bodies, and add no co-author trailer. - Report the commit or diff, changed files, commands run, results, and any unresolved risk. ## Principles diff --git a/README.md b/README.md index 3dbea83b..2726e6b0 100644 --- a/README.md +++ b/README.md @@ -362,7 +362,7 @@ just ci # the full quality gate, exactly what GitHub CI runs just run -- --help ``` -Run `just ci` before each commit, and keep all text ASCII-clean. It runs `.agents/checks/ci-gate.sh`, the one gate the `quality` job in `.github/workflows/ci.yml` also runs through the locked flake: `cargo fmt --all -- --check`, Clippy with warnings denied, the locked tests, `agent-flow checks`, `agent-flow validate`, `actionlint`, tripwires for the process artefacts `RESET.md` deleted, and a check that the run left the tracked tree unchanged. +Run `just ci` before each commit, and keep all text ASCII-clean. It runs `.agents/checks/ci-gate.sh`, the one gate the `quality` job in `.github/workflows/ci.yml` also runs through the locked flake: `cargo fmt --all -- --check`, Clippy with warnings denied, the locked tests, `agent-flow checks`, `agent-flow validate`, `actionlint`, tripwires for the process artefacts `RESET.md` deleted, the scratch-repository tests for the attribution check, the attribution check itself over every commit reachable from `HEAD`, or from the branch commit named by `ATTRIBUTION_TARGET` when GitHub CI checks out a pull request's merge result, and a check that the run left the tracked tree unchanged. The gate deliberately does not run `nix fmt`. That formatter applies Rust 2024 formatting to this Rust 2021 crate and reflows the retained `docs/audits/` records, so `cargo fmt` is the accepted formatting check. diff --git a/pack/AGENTS.md b/pack/AGENTS.md index 145b0b8a..5e59cccc 100644 --- a/pack/AGENTS.md +++ b/pack/AGENTS.md @@ -44,6 +44,7 @@ The human owns scope and acceptance. Do not add a step, change acceptance criter - Treat external input as untrusted, keep secrets out of source and logs, and fail loudly on errors. - Format only files you changed. Run destructive checks in scratch space, not over unrelated live files. - Keep the working tree recoverable. Inspect status and diffs before committing; do not discard files you do not own. +- Commit with the repository owner's configured Git identity. Keep agent, model, and LLM attribution out of commit messages, pull request titles, and pull request bodies, and add no co-author trailer. - Report the commit or diff, changed files, commands run, results, and any unresolved risk. ## Principles