Why CI enforcement matters
- Generation-time hooks cover interactive and agentic Claude Code sessions
- Code can also arrive from manual edits, other tools, or agents without hook coverage
- CI enforcement is the backstop: every file changed in a PR is checked, however it was produced
- Positions governance at three layers: generation time → PR review → CI gate
How it works
The workflow runs mneme check against the changed files in the PR diff.
mneme checkvalidates a file or diff against the decision corpus in.mneme/project_memory.json- Run against changed files in a PR to surface any architectural violations
- In
strictmode, exit code 0 = PASS → CI step passes - Exit code 1 = WARN and exit code 2 = FAIL or INCOMPLETE → CI step fails → PR blocked
Example workflow
# .github/workflows/mneme-governance.yml
name: Architectural Governance
on:
pull_request:
branches: [main]
jobs:
governance:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # origin/main must exist for the diff
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- run: pip install "mneme-hq>=0.10.0"
- name: Check changed files
shell: bash
run: |
# Added and modified files as "addeddeletedpath".
# Binary files show "-" counts; mneme check reads text only.
changes=$(git diff --numstat --no-renames --diff-filter=AM origin/main...HEAD) || exit 2
worst=0
while IFS=$'\t' read -r added _ f; do
[ -n "$f" ] || continue
if [ "$added" = "-" ]; then echo "skip $f (binary)"; continue; fi
# The decision files contain the terms they forbid
case "$f" in .mneme/*|docs/adr/*) echo "skip $f (decision file)"; continue ;; esac
rc=0
mneme check \
--memory .mneme/project_memory.json \
--input "$f" \
--target-path "$f" \
--query "PR diff: $f" \
--mode strict || rc=$?
# GitHub runs bash with -e: "|| rc=$?" records the code instead of
# stopping, and the worst code wins so a later PASS cannot mask a FAIL
if [ "$rc" -gt "$worst" ]; then worst=$rc; fi
done <<< "$changes"
exit "$worst"
Every changed file is checked, and the step exits with the worst result across the PR. --target-path names the repository path each file is checked as, so rules scoped with include_paths and exclude_paths resolve against it and print a PATH applicability line per scoped rule. Binary files are skipped because mneme check reads text only, and the decision files themselves are skipped because they legitimately contain the terms they forbid, so review changes to them by hand. Renamed files count as added. Each file is checked in full, so an existing violation in a file the PR touches also fails the step.
The job fails on WARN, FAIL, or INCOMPLETE. To block the merge, mark the governance job as a required status check in branch protection. The how-to guide extends the same loop with an ADR re-import check and saved JSON verdicts.
Status
- GitHub Actions CI gates are part of the shipped foundation on the roadmap
- The
mneme checkcommand is available today viapip install "mneme-hq>=0.10.0" - The GitHub Actions workflow above is the reference integration pattern
- As of mneme-hq 0.10.0, Mneme does not publish a packaged GitHub Action. You copy the workflow file above into your repository and own it, and no managed action is on the roadmap.
Layered governance model
Layer 1: Generation-time enforcement via Claude Code hooks
Layer 2: PR review (human + AI)
Layer 3: CI gate via Mneme in GitHub Actions
What the gate actually checks
The CI step runs mneme check --mode strict against each changed file and the project's decision corpus. The check returns a verdict per file, one of four:
- PASS — no enforced rule matched the file's contents. Exit code 0, CI green.
- WARN — a constraint in the corpus matched. In
strictmode (the default) WARN exits 1 and fails the step; inwarnmode PASS, WARN, and FAIL all exit 0, so findings show in the job log without blocking the PR. - FAIL — a typed rule or anti-pattern matched. Exit code 2 in strict mode: the step fails and the PR stays blocked until the code or the decision corpus changes.
- INCOMPLETE — the evaluation could not complete. Exit code 2 in both modes; it is never reported as PASS.
Mneme has no override record and no expiry field. An exception is a change to the decisions themselves, committed in the same PR. For a path-scoped FORBID_LITERAL rule, add the path to its exclude_paths (the rule must also declare include_paths) in the ADR (see ADR import) and re-import with mneme adr import docs/adr --memory .mneme/project_memory.json --apply --update-existing. Other directives, such as FORBID_DEPENDENCY, cannot be path-scoped, so change the ADR that declares them and re-import. To retire a decision, add a new ADR that lists it under supersedes (keep the old ADR file in docs/adr; the import rejects a supersedes reference to an ADR that isn’t there) and re-import: the old decision is marked superseded and stops producing verdicts. Don't edit project_memory.json by hand; from 0.10.0, a memory file whose decisions no longer match its decision index fails to load. The exception is a diff reviewers see in the PR, so weakening a constraint is a visible decision rather than a silent merge.
FAQ
Should we run this on every PR or only on main?
What if a PR needs to override a decision intentionally?
exclude_paths (the rule must also declare include_paths) in the ADR and re-import with mneme adr import docs/adr --memory .mneme/project_memory.json --apply --update-existing. Other directives, such as FORBID_DEPENDENCY, cannot be path-scoped, so change the ADR that declares them and re-import. To retire a decision, add a new ADR that lists it under supersedes (keep the old ADR file in docs/adr; the import rejects a supersedes reference to an ADR that isn’t there) and re-import: the old decision is marked superseded and stops producing verdicts. Don't edit project_memory.json by hand; from 0.10.0, a memory file whose decisions no longer match its decision index fails to load. The exception is a diff reviewers see in the PR, so weakening a constraint is a visible decision rather than a silent merge.