Universal code quality enforcement for monorepos and standalone projects.
Two layers:
- Shell layer (
lib/): bash + grep + awk + git. Zero runtime dependencies. Runs everywhere. - Python layer (
ami/ci/): HTTP API queries (PyPI, npm, Docker Hub) and AST analysis. Requires Python 3.11+ viauv run.
The shell layer does 90% of the work. Python exists only where shell can't: HTTP APIs and AST parsing.
AI coding agents generate code fast, but they also generate garbage fast. They suppress linters, sneak in type-ignore comments, create stub implementations, commit .env files, attribute themselves as co-authors, produce 800-line god-files, stuff logic into __init__.py, and leave behind default-path wrappers that nobody asked for.
AMI-CI catches all of that before it hits the repository.
AMI-CI/
├── lib/ # Shell layer (zero dependencies)
│ ├── ci.sh # Core: colors, file listing, YAML reader
│ ├── checks.sh # All check functions (sources ci.sh)
│ ├── parse_banned_words.awk # YAML → records parser
│ ├── parse_exceptions.awk # Exception YAML parser (universal + project)
│ └── parse_precommit_config.awk # .pre-commit-config.yaml → hook records
├── ami/ci/ # Python layer (HTTP + AST)
│ ├── types.py # NamedTuple definitions
│ ├── check_dependency_versions.py # PyPI/npm/Docker version checker
│ ├── check_dead_code.py # Dead code detection CLI
│ ├── dead_code_analyzer.py # AST analysis engine
│ └── _docker_versions.py # Docker Hub API queries
├── scripts/ # Executable tools
│ ├── generate-hooks # .pre-commit-config.yaml → native git hooks
│ ├── audit-workspace # Discover repos, audit AMI-CI integration
│ ├── cleanup-precommit # Remove pre-commit framework traces
│ └── rewrite-history # Strip blocked patterns from git history
├── config/ # YAML rule files (see below)
├── hooks/ # Generated git hook templates (output of generate-hooks)
├── tests/ # Shell + Python tests
│ ├── run_tests.sh # Shell test runner
│ ├── test_helpers.sh # Shell test framework
│ ├── test_core.sh # ci.sh function tests
│ ├── test_checks.sh # checks.sh function tests
│ ├── test_blocked_patterns.sh # Blocked commit pattern tests
│ ├── test_rewrite_history.sh # History rewriting tests
│ ├── test_check_dependency_versions.py # PyPI version checker tests
│ ├── test_check_npm_dependency_versions.py # npm version checker tests
│ ├── test_docker_versions.py # Docker version checker tests
│ ├── test_dead_code_analyzer.py # AST analysis engine tests
│ ├── test_dead_code_analyzer_detection.py # Detection edge case tests
│ ├── test_check_dead_code.py # Dead code CLI tests
│ ├── test_check_dead_code_cli.py # CLI output formatting tests
│ └── test_dependency_checker_integration.py # End-to-end dep checker tests
├── docs/
│ ├── SPEC.md # Full technical specification
│ └── HOOKS.md # Hook generation and integration guide
├── pyproject.toml # Python package definition
└── .pre-commit-config.yaml # Hook config (consumed by generate-hooks)
| Function | What | Config |
|---|---|---|
ci_check_unstaged |
Blocks commits with unstaged changes. Auto-stages and fails. | None |
ci_check_banned_words |
50+ patterns: linter suppression, loose typing, default-path code, reflection abuse, bad data models, hardcoded paths, container hygiene, UUID correctness | banned_words.yaml |
ci_block_sensitive_files |
Blocks files likely containing secrets (14 extensions, 14 keywords, 22 safe exceptions) | sensitive_files.yaml |
ci_check_file_length |
Enforces max 512 lines per source file | file_length_limits.yaml |
ci_check_init_files |
Enforces completely empty __init__.py files |
None |
| Function | What | Config |
|---|---|---|
ci_check_commit_message |
Enforces conventional commit format (type: description) |
None |
ci_block_coauthored |
Blocks Co-authored-by and noreply@anthropic.com in commit messages | blocked_commit_patterns.yaml |
| Function | What | Config |
|---|---|---|
ci_block_coauthored_history |
Scans full commit history for blocked patterns (catches rebased/amended commits) | blocked_commit_patterns.yaml |
ci_verify_coverage |
Runs test suites with minimum coverage thresholds | coverage_thresholds.yaml |
| Module | What | Why not shell |
|---|---|---|
check_dependency_versions.py |
Strict pinning + latest version enforcement across PyPI, npm, Docker Hub | HTTP API queries, TOML parsing, auto-upgrade |
check_dead_code.py + dead_code_analyzer.py |
AST-based dead code detection with cross-module reference graph | Python AST requires Python runtime |
_docker_versions.py |
Docker Hub registry queries, Dockerfile/compose image tag parsing | HTTP API queries |
The pre-commit Python framework is not used. Instead, scripts/generate-hooks reads .pre-commit-config.yaml and generates native bash scripts in .git/hooks/.
# Install hooks (reads .pre-commit-config.yaml, writes .git/hooks/*)
scripts/generate-hooks
# Or via Makefile
make install-hooksKey differences from pre-commit:
- No stashing. If unstaged changes exist, the hook auto-stages and fails. Files never disappear from disk.
- No Python runtime for the hook framework itself. Just bash.
- No remote code execution. All hooks are local
language: systemcommands. - Formatters are idempotent. After ruff format/lint, if files changed, the hook auto-stages and fails. You re-run commit.
| Script | Purpose |
|---|---|
generate-hooks |
Parse .pre-commit-config.yaml → generate native .git/hooks/{pre-commit,pre-push,commit-msg} |
audit-workspace |
Discover all git repos in workspace, report AMI-CI integration level |
cleanup-precommit |
Remove pre-commit Python package, cache, and generated hooks |
rewrite-history |
Strip blocked patterns (Co-authored-by, etc.) from commit history with backup |
All rules live in config/. Nothing is hardcoded in shell.
| File | Purpose |
|---|---|
banned_words.yaml |
50+ content patterns, filename rules, directory rules, universal exceptions |
banned_words_exceptions.yaml |
Universal path-based exceptions (tests/, .toml) |
blocked_commit_patterns.yaml |
Patterns blocked in commit messages and push history |
sensitive_files.yaml |
Sensitive extensions, keywords, safe exceptions, safe prefixes |
file_length_limits.yaml |
Max lines (default 512), enforced extensions, ignore list |
coverage_thresholds.yaml |
Per-suite test paths, minimum coverage, runner config |
dead_code.yaml |
AST scan paths, entry points, ignore patterns |
dependency_excludes.yaml |
Packages excluded from version checking |
Projects can add exception files in their own config/ directory or repo root:
| File | Purpose |
|---|---|
banned_words_exceptions.yaml |
Exempt specific patterns in specific paths |
sensitive_files_exceptions.yaml |
Additional safe files |
dependency_excludes_exceptions.yaml |
Additional excluded packages |
coverage_thresholds.yaml |
Project-specific test suites and coverage minimums |
# Shell tests (core functions, checks, blocked patterns, history rewriting)
./tests/run_tests.sh
# Python tests (dependency checker, dead code analyzer)
uv run pytest tests/ -v| File | Tests | What |
|---|---|---|
test_core.sh |
Shell | ci.sh functions: YAML reader, file listing, filter |
test_checks.sh |
Shell | checks.sh: banned words, commit message, dead code, deps |
test_blocked_patterns.sh |
Shell | Blocked commit pattern detection |
test_rewrite_history.sh |
Shell | History rewriting with backup and verification |
test_check_dependency_versions.py |
Python | PyPI version queries, pinning validation |
test_check_npm_dependency_versions.py |
Python | npm exact semver checking |
test_docker_versions.py |
Python | Docker image tag scanning |
test_dead_code_analyzer.py |
Python | AST visitor, cross-reference graph |
test_dead_code_analyzer_detection.py |
Python | Detection edge cases |
test_check_dead_code.py |
Python | CLI interface, config loading |
test_check_dead_code_cli.py |
Python | CLI output formatting, dry-run |
test_dependency_checker_integration.py |
Python | End-to-end dependency checking |
The Python test files test Python code (ami/ci/*.py), not shell code. This is correct -- the Python layer requires Python to test. The shell layer is tested by shell tests (test_core.sh, test_checks.sh, test_blocked_patterns.sh, test_rewrite_history.sh). There is no cross-layer testing (Python testing shell or vice versa).
- Shell first: if it can be done with grep/awk/git, it stays in shell. Python exists only for HTTP APIs and AST parsing.
- Source-and-call: no subprocesses.
source checks.sh, call functions. Fast. \034delimiter: AWK parsers use ASCII File Separator instead of tab becauseIFS=$'\t'withreadcollapses consecutive tabs (losing empty fields in parsed YAML).- No pre-commit framework:
generate-hooksreads the same.pre-commit-config.yamlformat but produces native git hooks. The pre-commit Python package is never installed or executed. - Conservative dead code detection: the AST analyzer avoids false positives by skipping private names, dunder methods, and dynamically referenced names. It reports only high-confidence dead code.