Org-level defaults for every repo under aiexponent. Two kinds of thing live here:
- Community health files at the root and in
.github/: code of conduct, contributing guide, security policy, issue forms, PR template, and the org profile underprofile/. GitHub serves these to any repo in the org that does not carry its own copy. - Reusable workflows in
.github/workflows/, called by the repos rather than copied into them, plus starter files inworkflow-templates/so a new repo can add them from the Actions tab in one click.
Every gate has to be able to fail. This estate has collected nine checks that were green because they were not looking at anything: a secret scan that scanned zero commits, an audit with continue-on-error: true, a publish that treated a duplicate version as a no-op. Each workflow here states, in a comment block at the top of its own file, the exact conditions that turn it red. If a change to one of these files cannot name that condition, the change is wrong.
Three rules follow from that:
- No
continue-on-errorand no|| trueon a gate step. - Every script step runs under
set -euo pipefail, so a tool that fails to install becomes a failure instead of a skipped check. - When the thing being measured is empty in a way that means the gate is blind, the job fails. When it is empty in a way that is genuinely in scope and clean, the job passes and says so in the log.
| Workflow | Purpose | Goes red when |
|---|---|---|
gitleaks.yml |
Secret scanning with the MIT gitleaks binary | A secret appears in the PR commit range, or anywhere in history on a push. Also red if the binary download or checksum check fails, or if the PR range holds zero commits. |
npm-audit.yml |
Node dependency audit | npm audit reports an advisory at or above the audit level, default high. Also red if npm ci fails because the lockfile is missing or out of sync. |
python-publish.yml |
PyPI upload over trusted publishing | The build fails, produces no sdist or no wheel, fails twine check --strict, or the version already exists on PyPI. Also red if the OIDC exchange is refused because the trusted publisher does not match. |
ai-signal-scan.yml |
Human voice gate on changed text | A changed text file has more than two em-dashes, a cluster of terms from the AI lexicon, signpost scaffolding, or two or more negation constructions of the not-just-X shape. Also red if a listed path cannot be read, or if the shared scanner is missing. |
Add one file per gate under .github/workflows/ in the calling repo. Pin to @main for now; switch to a tag when this repo starts tagging.
name: Secret scan
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
jobs:
gitleaks:
uses: aiexponent/.github/.github/workflows/gitleaks.yml@main
# with:
# config-path: .gitleaks.toml
# extra-args: --baseline-path .gitleaks-baseline.jsonInputs: gitleaks-version (default 8.30.1), config-path, extra-args.
Two things about this one are load bearing. It uses the gitleaks binary, because gitleaks/gitleaks-action is commercially licensed for org-owned repos and refuses to run on ours. And it uses gitleaks detect, never gitleaks protect, because protect reads the uncommitted working tree and ignores --log-opts, which in CI means it scans nothing and passes. Both traps are written into the comments in the workflow file.
name: Dependency audit
on:
pull_request:
push:
branches: [main]
schedule:
- cron: "0 6 * * 1"
permissions:
contents: read
jobs:
npm-audit:
uses: aiexponent/.github/.github/workflows/npm-audit.yml@main
# with:
# node-version: "20"
# working-directory: apps/web
# audit-level: high
# production-only: falseTo accept one advisory that has no fix, record it in the calling repo with an overrides entry or an .npmrc line and a dated comment saying why. Never turn the gate off.
name: Publish to PyPI
on:
release:
types: [published]
jobs:
publish:
uses: aiexponent/.github/.github/workflows/python-publish.yml@main
permissions:
contents: read
id-token: write
with:
environment: pypienvironment is an input because the environments already differ: riskforge publishes from pypi-publish, the other packages from pypi. A PyPI trusted publisher is bound to one environment name, so a hardcoded value would lock riskforge out. Other inputs: python-version (default 3.12), package-dir, repository-url, which you can point at TestPyPI to rehearse a release.
Before the first run, configure a trusted publisher on the PyPI project with the owner aiexponent, the calling repo name, the workflow filename as it exists in the calling repo, and the environment name above. No API token is stored anywhere.
name: Human voice gate
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
jobs:
ai-signal-scan:
uses: aiexponent/.github/.github/workflows/ai-signal-scan.yml@main
# with:
# paths: "*.md *.mdx *.txt content/**/*.md"
# scan-all: falseThe scanner itself is vendored here at .github/scripts/scan-ai-signals-ci.py, and the job checks this repo out beside the caller to reach it. Callers hold no copy, so one edit here reaches every repo. The canonical signal list lives in vibe_skills/hooks/scan-ai-signals.py, which is a Claude Code editor hook: it reads a JSON payload on stdin, looks at a single edit, and warns without blocking. That shape cannot run in CI, and the repo holding it is private, so the vendored file is a port with the same thresholds in a file-in, exit-code-out shape. When the canonical list changes, update the port in the same change. The prose rules behind the thresholds are in askajay-thought-leadership/skills/_shared/ai-writing-signals.md.
Thresholds, as implemented:
| Check | Bar |
|---|---|
| Em-dashes per file | more than 2 fails |
| AI lexicon terms | 2 distinct terms in prose files, 3 elsewhere |
| Signpost scaffolding | any hit in a prose file |
Negation constructions, the not-just-X shape |
2 or more fail |
Files under 120 characters are skipped as too short to judge, and paths that quote the blocklist on purpose, such as the gate docs themselves, are skipped by name.
workflow-templates/ carries a caller file and a .properties.json for each of the four. They show up under Actions, New workflow for any repo in the org, with $default-branch filled in automatically.
- Put it in
.github/workflows/withon: workflow_calland typed inputs. - Open the file with a comment block naming what makes the job red.
- Run
actionlintover the whole directory. - Add a
workflow-templates/pair and a row in the table above. - Migrate callers in a separate change, one repo at a time. Landing a shared workflow and switching a caller are different risks and belong in different pull requests.