Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@
## Checklist

- [ ] The title follows Conventional Commits, e.g. `feat(api): add athlete height endpoint`
- [ ] No secrets, media or files over 5 MB
- [ ] No secrets
- [ ] README.md and AGENTS.md still match how to install, run and test
- [ ] DECISIONS.md has a line for any decision made here
8 changes: 4 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ the reasons, is CONTRIBUTING.md in the BuildexLabsAI/.github repository.
everything in docs/. Everywhere: no spaces, no non-ASCII characters, and no two names that
differ only by letter case. Inside src/, tests/ and scripts/, follow the language's own
convention (`snake_case.py`, `Button.tsx`).
- **Never commit** secrets (.env files, keys, tokens), video, audio, PDF, Office documents,
design source files, archives, or any file over 5 MB. Media lives outside git; link to it
from README.md. Small assets the app ships (icons, fonts, wasm) belong in src/ or public/.
- **Never commit** secrets (.env files, keys, tokens). Media, documents and large files may go
into git; GitHub itself refuses any file over 100 MiB. Small assets the app ships (icons,
fonts, wasm) belong in src/ or public/.
- **Exceptions** go in `.github/standard-exceptions`, one path per line with its reason:
`docs/contract.pdf # signed contract, required for the audit`.
`prisma # Prisma reads its schema from a folder at the root`.
- **Branches.** Never commit to main directly. Name branches `feat/…`, `fix/…`, `docs/…`,
`chore/…`, `refactor/…` or `test/…`. Agent tools' own prefixes (`claude/…`, `codex/…`) are
accepted.
Expand Down
16 changes: 9 additions & 7 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,20 +47,22 @@ BuildexLabsAI block in each repository's AGENTS.md.
- **Secrets:** `.env` files, keys, certificates, tokens. Commit `.env.example` with placeholder
values instead. If a secret is pushed, rotate it first: deleting the file does not remove it
from git history.
- **Media and documents:** video, audio, PDF, Office files, design source files (Photoshop,
Figma, Blender) and archives.
- **Any file over 5 MB.**

Media lives outside git, and README.md links to where it is. Small files the app ships
(icons, fonts, wasm) belong in `src/` or `public/`.
**Allowed:** video, audio, PDF, Office files, design source files (Photoshop, Figma, Blender)
and archives, up to GitHub's limit. The check allows these file types and has no size limit.
Top-level images are rejected: app images go in `src/` or `public/`, other images in `docs/`.

**GitHub's limit:** GitHub blocks files larger than 100 MiB. Git warns above 50 MiB.

Small files the app ships (icons, fonts, wasm) belong in `src/` or `public/`. Each repository's
README.md has a "Media and large files" section saying where its media lives.

## Exceptions

When a file really has to break a rule, list it in `.github/standard-exceptions` with the
reason. The reason is required, and the pull request shows it to the reviewer.

```
docs/contract.pdf # signed contract, required for the audit
prisma # Prisma reads its schema from a folder at the root
```

Expand Down Expand Up @@ -122,7 +124,7 @@ git clone https://github.com/b1rdmania/readme-generator ~/.claude/skills/readme-
| Where | When | What happens |
|---|---|---|
| Claude Code hook | Claude creates a file | A name that breaks the rules is blocked |
| git pre-commit hook | `git commit` | A commit with a bad new name, media or a file over 5 MB is blocked |
| git pre-commit hook | `git commit` | A commit with a bad new name is blocked |
| CI (`standard`) | Every pull request and push to main | The whole repository, the pull request title and a secret scan; problems show as a red X |

Run it yourself with `bash .github/scripts/check-standard.sh`. What is enforced on the
Expand Down
1 change: 1 addition & 0 deletions DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ Choices that neither the code nor the git log shows, newest first. One line each
- 2026-10-07: Secrets are scanned with the gitleaks binary from its GitHub release, pinned by checksum. GitHub's push protection needs a paid add-on for private repositories, and gitleaks-action needs a license key for organizations.
- 2026-10-07: Branches that agent tools create (claude/, codex/) are accepted, because Claude Code names its worktree branches that way.
- 2026-10-07: Folder and file names may contain the route symbols frameworks use ([slug], (group), @slot, $param, {-$optional}); UAE-SponSlot's TanStack Router routes need $.
- 2026-10-16: Video, audio, PDF, Office files, design sources, archives and files of any size may be committed; the check rejects files by type only for secrets and operating system junk (images still stay out of the top level), and has no size limit, so GitHub's own 100 MiB block is the only ceiling. Esko lifted the 2026-10-07 ban because project repositories must hold their deliverables, such as a 9.6 MB client proposal PDF: PDFs and files over 5 MB "may and must be there".
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ check that enforces the standard in every repository.
flowchart TD
S[src/check-standard.sh on main] -- copy --> R[Each repository]
R -- Claude Code creates a file --> H[Claude Code hook: blocks a name that breaks the rules]
R -- git commit --> P[pre-commit hook: blocks bad new names, media, files over 5 MB]
R -- git commit --> P[pre-commit hook: blocks bad new names]
R -- pull request or push to main --> C[standard-check.yml: whole repository, pull request title, secret scan]
S -- current check --> C
C -- compares the repository's copies with src/ --> W[Warning when a copy is out of date]
Expand Down
2 changes: 1 addition & 1 deletion docs/enforcement.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on GitHub Team.
|---|---|---|---|
| AGENTS.md and CLAUDE.md | Tell every AI agent the rules | Instruction only | Instruction only |
| Claude Code hook | Stops Claude from creating a badly named file | Blocks (Claude Code only) | Same |
| git pre-commit hook | Stops a commit with a bad new name, media or a file over 5 MB | Blocks, once switched on in the clone | Same |
| git pre-commit hook | Stops a commit with a bad new name | Blocks, once switched on in the clone | Same |
| CI check `standard / check` | Checks the whole repository, the pull request title and secrets | Red X; merging still works | Blocks the merge |
| Merge settings | Squash merge only; branches deleted after merge | Works | Works |
| Repository creation | Only owners create repositories, so each one starts from the template | Works | Works |
Expand Down
8 changes: 4 additions & 4 deletions src/agents-block.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ the reasons, is CONTRIBUTING.md in the BuildexLabsAI/.github repository.
everything in docs/. Everywhere: no spaces, no non-ASCII characters, and no two names that
differ only by letter case. Inside src/, tests/ and scripts/, follow the language's own
convention (`snake_case.py`, `Button.tsx`).
- **Never commit** secrets (.env files, keys, tokens), video, audio, PDF, Office documents,
design source files, archives, or any file over 5 MB. Media lives outside git; link to it
from README.md. Small assets the app ships (icons, fonts, wasm) belong in src/ or public/.
- **Never commit** secrets (.env files, keys, tokens). Media, documents and large files may go
into git; GitHub itself refuses any file over 100 MiB. Small assets the app ships (icons,
fonts, wasm) belong in src/ or public/.
- **Exceptions** go in `.github/standard-exceptions`, one path per line with its reason:
`docs/contract.pdf # signed contract, required for the audit`.
`prisma # Prisma reads its schema from a folder at the root`.
- **Branches.** Never commit to main directly. Name branches `feat/…`, `fix/…`, `docs/…`,
`chore/…`, `refactor/…` or `test/…`. Agent tools' own prefixes (`claude/…`, `codex/…`) are
accepted.
Expand Down
46 changes: 3 additions & 43 deletions src/check-standard.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@

export LC_ALL=C

MAX_BYTES=$((5 * 1024 * 1024))
# Besides letters, digits and . _ - this allows the route symbols frameworks put in file names:
# [slug] (group) @slot +page $param {-$optional}.
# shellcheck disable=SC2016 # the $ is a literal character here, not an expansion
Expand Down Expand Up @@ -130,12 +129,6 @@ set_kind() { # NAME — sets $kind for file types with their own rule, or emptie
if [ -z "$kind" ]; then
case "$ext" in
pem|key|p12|pfx|jks|keystore) kind=secret ;;
mp4|mov|avi|mkv|webm|m4v|wmv|flv|mpg|mpeg) kind=Video ;;
mp3|wav|aac|flac|ogg|m4a|aif|aiff|wma) kind=Audio ;;
pdf) kind=PDF ;;
doc|docx|ppt|pptx|xls|xlsx|pages|numbers|odt|ods|odp|rtf) kind=Office ;;
psd|ai|fig|sketch|xd|indd|blend|afdesign|afphoto|aep|prproj) kind='Design source' ;;
zip|rar|7z|tar|gz|tgz|bz2|xz|dmg|iso) kind=Archive ;;
png|jpg|jpeg|gif|webp|avif|svg|ico|bmp|tif|tiff|heic) kind=image ;;
esac
fi
Expand Down Expand Up @@ -172,8 +165,7 @@ check_path() { # PATH — name and file-type rules for one repository-relative p
'') ;;
secret) report error "$path" "Secrets never go into git (.env files, keys, certificates). Commit .env.example with placeholders instead, and rotate anything already pushed." ;;
junk) report error "$path" "Operating system junk file. Delete it and add it to .gitignore." ;;
image) [ "$depth" -eq 1 ] && report error "$path" "Images do not belong at the top level. App assets go in src/ or public/; other media stays outside git." ;;
*) report error "$path" "$kind files do not belong in git. Keep media outside the repository and link it from README.md, or list the file in $EXCEPTIONS_FILE with the reason." ;;
image) [ "$depth" -eq 1 ] && report error "$path" "Images do not belong at the top level. App assets go in src/ or public/, other images in docs/." ;;
esac
if [ "$depth" -eq 1 ] && [ -z "$kind" ]; then
case "$comp" in
Expand All @@ -193,11 +185,6 @@ note_top_dir() { # PATH — warns once about each unexpected top-level folder
report warning "$dir/" "Unexpected top-level folder. Code goes in src/ and documents in docs/. If a tool needs this folder at the root, list it in $EXCEPTIONS_FILE with the reason."
}

size_error() { # PATH BYTES
is_excepted "$1" && return
report error "$1" "File is $(awk -v b="$2" 'BEGIN { printf "%.1f", b / 1048576 }') MB; the limit is 5 MB. Large files live outside git (link them from README.md), or list the file in $EXCEPTIONS_FILE with the reason."
}

# --- whole-repository rules ---------------------------------------------------------------

check_required() {
Expand Down Expand Up @@ -227,22 +214,6 @@ check_collisions() { # names in $paths that differ only by letter case, at any f
awk '{ l = tolower($0); if (l in seen) { if (seen[l] != $0) print seen[l] "\t" $0 } else seen[l] = $0 }')
}

check_index_sizes() { # every file in the git index
local rec meta sha size i=0
local shas=() names=()
while IFS= read -r -d '' rec; do
meta=${rec%%$'\t'*}
sha=${meta#* }; sha=${sha%% *}
shas+=("$sha"); names+=("${rec#*$'\t'}")
done < <(git ls-files -s -z)
[ "${#shas[@]}" -eq 0 ] && return
while read -r size; do
i=$((i + 1))
case "$size" in ''|*[!0-9]*) continue ;; esac # e.g. a submodule commit
[ "$size" -gt "$MAX_BYTES" ] && size_error "${names[$((i - 1))]}" "$size"
done < <(printf '%s\n' "${shas[@]}" | git cat-file --batch-check='%(objectsize)')
}

check_drift() { # DIR — compare this repo's copies with the canonical ones in DIR
local dir=$1 copy=.github/scripts/check-standard.sh
if [ -f AGENTS.md ] && grep -q 'buildexlabs-standard:start' AGENTS.md &&
Expand All @@ -269,38 +240,27 @@ check_pull_request() {
# --- modes --------------------------------------------------------------------------------

run_full() {
local p size
local p
while IFS= read -r -d '' p; do paths+=("$p"); done < <(git ls-files -z --cached --others --exclude-standard)
check_required
for p in ${paths[@]+"${paths[@]}"}; do
check_path "$p"
note_top_dir "$p"
done
check_collisions
check_index_sizes
while IFS= read -r -d '' p; do # new files are not in the index yet
[ -f "$p" ] || continue
size=$(wc -c < "$p")
size=${size//[!0-9]/} # macOS wc pads the number with spaces
[ "$size" -gt "$MAX_BYTES" ] && size_error "$p" "$size"
done < <(git ls-files -z --others --exclude-standard)
[ -n "$canonical" ] && check_drift "$canonical"
check_pull_request
}

run_staged() {
local base p size
local base p
if git rev-parse -q --verify HEAD >/dev/null 2>&1; then base=HEAD
else base=$(git hash-object -t tree /dev/null); fi # first commit: compare with nothing
# Only new names are checked, so a commit that edits an old, non-standard file still goes in.
while IFS= read -r -d '' p; do
check_path "$p"
note_top_dir "$p"
done < <(git diff --cached --name-only -z --diff-filter=ACR "$base")
while IFS= read -r -d '' p; do
size=$(git cat-file -s ":$p" 2>/dev/null) || continue
[ "$size" -gt "$MAX_BYTES" ] && size_error "$p" "$size"
done < <(git diff --cached --name-only -z --diff-filter=ACMR "$base")
while IFS= read -r -d '' p; do paths+=("$p"); done < <(git ls-files -z)
check_collisions
}
Expand Down
22 changes: 12 additions & 10 deletions tests/check-standard.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -107,13 +107,12 @@ git update-index --add --cacheinfo "100644,$blob,src/assets/b.png"
run; expect "names differing only by letter case fail" 1 "src/Assets"

echo "== media, size, secrets =="
new_repo; touch docs/demo.mp4; run; expect "video fails" 1 "docs/demo.mp4"
new_repo; touch docs/deck.pdf; run; expect "PDF fails" 1 "docs/deck.pdf"
new_repo; touch scripts/backup.zip; run; expect "archive fails" 1 "scripts/backup.zip"
new_repo; touch docs/demo.mp4 docs/voice.mp3 docs/deck.pdf docs/brief.docx docs/scene.blend scripts/backup.zip
run; expect_clean "video, audio, PDF, Office files, design sources and archives pass"
new_repo; touch logo.png; run; expect "image at the top level fails" 1 "logo.png"
new_repo; mkdir -p public; touch public/icon-192.png docs/architecture.png; run; expect_clean "images in public and docs pass"
new_repo; head -c 6000000 /dev/zero > src/model.bin; run; expect "file over 5 MB fails" 1 "src/model.bin"
new_repo; head -c 5000000 /dev/zero > src/small.wasm; run; expect_clean "file under 5 MB passes"
new_repo; head -c 10000000 /dev/zero > docs/deck.pdf; run; expect_clean "a 10 MB file passes"
new_repo; head -c 10000000 /dev/zero > docs/deck.pdf; git add -A; run; expect_clean "a 10 MB file in the index passes"
new_repo; echo 'KEY=1' > .env; git add -f .env; run; expect "committed .env fails" 1 ".env"
new_repo; echo 'KEY=' > .env.example; run; expect_clean ".env.example passes"
new_repo; touch src/server.pem; run; expect "key file fails" 1 "src/server.pem"
Expand All @@ -124,10 +123,10 @@ new_repo; mkdir tasks; touch tasks/todo.md; run; expect "unknown top-level folde
new_repo; touch notes.md; run; expect "loose top-level document warns but passes" 0 "notes.md"

echo "== exceptions =="
new_repo; touch docs/contract.pdf
printf 'docs/contract.pdf # signed contract, required for the audit\n' > .github/standard-exceptions
new_repo; touch logo.png
printf 'logo.png # the hosting service reads it from the root\n' > .github/standard-exceptions
run; expect_clean "listed exception passes"
new_repo; touch docs/contract.pdf; printf 'docs/contract.pdf\n' > .github/standard-exceptions
new_repo; touch logo.png; printf 'logo.png\n' > .github/standard-exceptions
run; expect "exception without a reason fails" 1 "reason"
new_repo; mkdir tasks; touch tasks/todo.md
printf '# Folders a tool needs at the root\ntasks # the task runner reads it from the root\n' > .github/standard-exceptions
Expand Down Expand Up @@ -159,7 +158,9 @@ new_repo; touch "docs/my notes.md"; git add -A; run --staged; expect "new bad na
new_repo; touch docs/plan.md; git add -A; run --staged; expect_clean "new good name passes"
new_repo; touch "docs/old notes.md"; git add -A; git_q commit -qm legacy
echo change >> "docs/old notes.md"; git add -A; run --staged; expect_clean "editing a legacy file does not block the commit"
new_repo; head -c 6000000 /dev/zero > src/model.bin; git add -A; run --staged; expect "file over 5 MB blocks the commit" 1 "src/model.bin"
new_repo; head -c 10000000 /dev/zero > docs/deck.pdf; git add -A; run --staged; expect_clean "a 10 MB PDF commits"
new_repo; echo draft > docs/deck.pdf; git add -A; git_q commit -qm draft
head -c 10000000 /dev/zero > docs/deck.pdf; git add -A; run --staged; expect_clean "a committed file grown to 10 MB commits"
new_repo; mkdir tasks; touch tasks/todo.md; git add -A; run --staged; expect "new unknown top-level folder warns but passes" 0 "tasks/"
dir=$(mktemp -d "$TMP/empty.XXXXXX"); cd "$dir" || exit 1; git init -q -b main; touch "Bad Name.md"; git add -A
run --staged; expect "first commit of an empty repository is checked" 1 "Bad Name.md"
Expand All @@ -169,7 +170,8 @@ new_repo; run_hook "$PWD/My File.md"; expect "blocks a new file with a space" 2
new_repo; run_hook "$PWD/docs/athlete-model.md"; expect_clean "allows a good new file"
new_repo; run_hook "$PWD/docs/new folder/plan.md"; expect "blocks a new folder with a space" 2 "new folder"
new_repo; run_hook "$PWD/docs/tähti.md"; expect "blocks a non-ASCII name" 2 "docs/t"
new_repo; run_hook "$PWD/docs/demo.mp4"; expect "blocks a video" 2 "docs/demo.mp4"
new_repo; run_hook "$PWD/src/server.pem"; expect "blocks a key file" 2 "src/server.pem"
new_repo; run_hook "$PWD/docs/deck.pdf"; expect_clean "allows a PDF"
new_repo; mkdir -p src/assets; touch src/assets/a.png; git add -A; git_q commit -qm assets
run_hook "$PWD/src/Assets/b.png"; expect "blocks a name that clashes by letter case" 2 "assets"
new_repo; touch "docs/old notes.md"; run_hook "$PWD/docs/old notes.md"; expect_clean "allows editing an existing file"
Expand Down
Loading