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 commit checked regardless of how it was produced
  • Positions governance at three layers: generation time → MR review → CI gate

How it works

The pipeline job runs mneme check against the changed files in the merge request diff.

  • mneme check validates a file or diff against the decision corpus in .mneme/project_memory.json
  • Run against changed files in a merge request to surface any architectural violations
  • In strict mode, the default: exit code 0 = PASS → the job passes
  • Exit code 1 = WARN and exit code 2 = FAIL or INCOMPLETE → the job fails. To block the merge, turn on “Pipelines must succeed” in the project’s merge request settings.

Example pipeline

# .gitlab-ci.yml
mneme-decisions:
  stage: test
  image: python:3.12          # includes git; the slim image doesn't
  variables:
    GIT_DEPTH: "0"            # full history, so the merge request base is available
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
  script:
    - python -m pip install "mneme-hq==0.10.0"
    # Fail if an edited ADR wasn't re-imported into the memory file
    - mneme adr import docs/adr --memory .mneme/project_memory.json --apply --update-existing
    - git diff --exit-code -- .mneme/project_memory.json
    # Check each added or modified text file against its repository path
    - |
      worst=0
      mkdir -p mneme-results
      git diff --numstat --no-renames --diff-filter=AM "$CI_MERGE_REQUEST_DIFF_BASE_SHA...HEAD" > changes.txt || exit 2
      tab=$(printf '\t')
      while IFS="$tab" read -r added _ f; do
        [ -n "$f" ] || continue
        if [ "$added" = "-" ]; then echo "skip    $f (binary)"; continue; fi
        out="mneme-results/$(printf '%s' "$f" | tr '/' '_').json"
        rc=0
        mneme check --memory .mneme/project_memory.json \
          --input "$f" --target-path "$f" \
          --query "MR change: $f" --json > "$out" || rc=$?
        echo "exit=$rc  $f"
        if [ "$rc" -gt "$worst" ]; then worst=$rc; fi
      done < changes.txt
      exit "$worst"
  artifacts:
    when: always
    paths:
      - mneme-results/

GitLab runs script lines with errexit on, so the loop records each exit code with || rc=$? instead of stopping at the first violation, and exits with the worst one. Binary files are skipped because mneme check reads text only. We ran this script under sh -e against a local copy of a merge request, not on GitLab runners. The two import lines assume ADRs in docs/adr; drop them if you don’t import ADRs. Check ADRs out with LF line endings (docs/adr/*.md text eol=lf in .gitattributes), because the memory file records a hash of each ADR’s bytes. The GitHub Actions how-to guide walks through the same loop with recorded output.

Status

  • GitLab 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"
  • As of mneme-hq 0.10.0, Mneme doesn’t publish a GitLab CI template. You copy the job above into your .gitlab-ci.yml and own it.

Layered governance model

Layer 1: Generation-time enforcement via Claude Code hooks
Layer 2: Merge request review (human + AI)
Layer 3: CI gate via Mneme in GitLab CI

What the gate actually checks

The job runs mneme check in strict mode, the default, on each changed file against the project’s decision corpus. Each file gets one of four verdicts:

  • PASS: no enforced rule matched the file. Exit code 0.
  • WARN: a constraint matched, for example a FORBID_DEPENDENCY word. Exit code 1 in strict mode, which fails the job. With --mode warn, PASS, WARN and FAIL exit 0 and findings appear only in the job log; INCOMPLETE still exits 2.
  • FAIL: a typed rule or anti-pattern matched. Exit code 2; the job fails until the code or the decision corpus changes.
  • INCOMPLETE: the evaluation couldn’t finish. Exit code 2, never reported as PASS.

Mneme has no override record and no expiry field. An exception is a change to the decision corpus in the same merge request: add a path to a FORBID_LITERAL rule’s exclude_paths (the structured form, alongside its include_paths), or retire the decision with 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), then re-import with --apply --update-existing; 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. Reviewers see the change in the MR diff, so weakening a decision is visible rather than a quiet bypass.

FAQ

Should we run this on every merge request or only on the default branch?
Every merge request. The point of CI enforcement is to surface violations at MR time when the cost of fixing is lowest. Running only on the default branch means failures land after merge, which defeats the gating purpose. The check is deterministic keyword matching with no embedding model or network call, so it adds little to a pipeline.
What if a merge request needs to override a decision intentionally?
Mneme has no override record and no expiry field. Make the exception a change to the decision corpus in the same MR: add the path to a FORBID_LITERAL rule’s exclude_paths, or retire the decision with 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), then re-import with --apply --update-existing. 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 shows in the MR diff, so weakening a decision is visible rather than a silent merge.
How does this compare to running other linters or scanners in GitLab CI?
Different layers. Linters check syntax and style; SAST scanners check for security issues; Mneme enforces architectural decisions against the project's own decision corpus. They compose: the typical pipeline sequence is lint → mneme check → tests → SAST. See the GitHub Actions integration for the equivalent pattern on GitHub.