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 check validates 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 strict mode, 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 check command is available today via pip 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 strict mode (the default) WARN exits 1 and fails the step; in warn mode 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?
Every PR. The point of CI enforcement is to surface violations at PR time when the cost of fixing is lowest. Running only on main means failures land after merge, which defeats the gating purpose.
What if a PR needs to override a decision intentionally?
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 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.
How does this compare to running CodeRabbit or a custom linter in CI?
Different layers. Linters check syntax and style; CodeRabbit reviews PRs for correctness and security; Mneme enforces architectural decisions against the project's own decision corpus. They compose: the typical CI sequence is lint → mneme check → tests → CodeRabbit review. See the CodeRabbit comparison for the detail.