enolaBlogHow to Use Enola Day to Day: Hooks, Baselines, and the Architecture Check

Blog

How to use enola day to day: hooks, baselines and the check.

Dejan Menges26 min read

The most common piece of feedback Enola gets is not a bug report. It is a version of this: I like the check, but right now I am spiking, and I do not want anything grading my architecture for the next hour.

That is a completely reasonable thing to want, and it is one command. The reason it keeps coming up is that people meet Enola as something their agent does automatically, so it reads like a property of the tool rather than a setting they own. It is a setting. This post is the day-to-day version of the manual: how to turn the loop off and back on, where to put the check instead, how to read --explain, how to decide what fails — including what to do when your language forbids the one thing that fails by default — how baselines work and why one can stop being valid, and what enola log is for.

Every output below is real, produced by enola 0.3.19. Most of them come from a small Express and TypeScript service in /tmp/orders with six modules — api, billing, invoice, pricing, notify, storage — plus a Go port of the same service, and three public repositories: Excalidraw, Gitea and Tokio. Nothing is hand-written; the only edits are trimming, marked with ⋮ or called out in the text, and dropping the engine's progress lines, which Enola writes to stderr.

The three places the check runs, and none of them is mandatory

Enola is one binary that does two things: it serves your agent a graph of the codebase over MCP, and it grades what a change did to that graph. The grading half can run in three places, and each is independent of the others.

WhereWhat runs itTurn it off with
In your agent Session hooks: one pins a baseline when a session starts, one grades when the agent finishes a turn and speaks only if the change regressed something. enola uninstall, or enola install without --hooks
In your shell enola baseline pin, then enola check. Exits 1 on a structural regression. Do not type it
In CI The same command, or the GitHub Action, on every pull request. Remove the workflow

Most people who tell me the hook is in the way actually want the third row and not the first. That combination is worth naming: no automatic grading while you work, one gate before anything merges. It is a normal way to run this tool, not a degraded one.

Turning the hook off, and back on

enola install --hooks is what wired the loop into your agent. It writes an instruction file for each agent you have, and two hooks. Every run previews exactly what it will touch before it writes anything, and --dry-run stops after the preview:

enola install --hooks --dry-run
Install
Target:     /tmp/orders   (current directory)
            Pass a path to configure a different repository.
Hooks:      yes
            · at the start of a session the architecture is frozen as a
              baseline, in the background — session start is never delayed,
              and a baseline you pinned yourself is never replaced
            · at the end of a session, the architectural delta is reported only
              if the change introduced a regression — or, once per cause, that
              the baseline was not comparable so nothing could be graded at all
            Never blocks, and never interrupts on failure.

  created   /tmp/orders/.claude/rules/enola.md
  created   /tmp/orders/.claude/settings.json
  created   /tmp/orders/.cursor/rules/enola.mdc
  skipped   /tmp/orders/AGENTS.md
            does not exist; enola will not create it. Creating it would configure Codex, Copilot, Pi and other AGENTS.md-aware agents at once — worth doing deliberately, so create it yourself and re-run
  skipped   (codex)
            locally, codex reads the repository's AGENTS.md, which the `agents` target already writes
  created   /tmp/orders/.codex/hooks.json
  created   /tmp/orders/.github/instructions/enola.instructions.md
  skipped   (pi)
            locally, pi reads the repository's AGENTS.md, which the `agents` target already writes

Dry run — nothing was written.

Two things in that preview matter. Enola never creates an AGENTS.md that was not already there, because that one file configures several agents at once and is your decision to make. And every file it does write is either owned by Enola or edited inside explicit markers.

Which is what makes the reverse safe. enola uninstall takes it all out, and it also has a --dry-run:

enola uninstall --dry-run
Uninstall
Target:     /tmp/orders   (current directory)
            Pass a path to configure a different repository.

  removed   /tmp/orders/.claude/rules/enola.md
  removed   /tmp/orders/.claude/settings.json
  removed   /tmp/orders/.cursor/rules/enola.mdc
  unchanged /tmp/orders/AGENTS.md
  skipped   (codex)
            locally, codex reads the repository's AGENTS.md, which the `agents` target already writes
  removed   /tmp/orders/.codex/hooks.json
  removed   /tmp/orders/.github/instructions/enola.instructions.md
  skipped   (pi)
            locally, pi reads the repository's AGENTS.md, which the `agents` target already writes

Dry run — nothing was written.

A shared file that Enola only added a block to comes back byte for byte; directories it created itself are removed. What uninstall does not touch is the MCP server registration, which you made with claude mcp add enola enola or the equivalent for your client. So after uninstalling, your agent still has the graph and can still answer "what depends on this" — it just stops grading you unasked. That is usually the setting people are looking for.

There are three settings here, not two:

  • enola install --hooks — instructions plus automatic grading. The full loop.
  • enola install — instructions only. Your agent knows the graph exists and uses it when it helps; nothing grades automatically.
  • enola uninstall — neither. MCP still works if you registered it.

Restricting the run to one agent is --targets=claude,cursor, and --global does the same thing for every project of yours instead of one repository.

Check whether the hook is actually running

A hook configuration is a contract with your agent, and one the agent silently ignores looks exactly like one it honours. So the hooks record every time they fire, including the runs where they deliberately say nothing, and enola doctor reports it. Here it is right after an install, before any session has started:

enola doctor
enola doctor: /tmp/orders

Runtime
  soft memory limit 117964 MiB (90% of 131072 MiB system RAM; override with GOMEMLIMIT)
  enola 0.3.19 (extractors v197)

Updates
  up to date, as far as the last check knows

Baseline
  comparable — the gate can grade against it

Session hooks
  configured in .claude/settings.json
  installed        2026-08-14T14:57:38Z (less than a minute ago)
  hook command     /Users/dejan/.local/bin/enola

  session-start   pins the baseline your change is graded against
                  NEVER FIRED
  stop            grades the change when the session ends
                  NEVER FIRED

  NEITHER HOOK HAS EVER RUN.
  The configuration exists but nothing is invoking it. Start a session in this
  repository and re-run; if it still says this, the agent is not reading the
  hooks — a shape enola writes that your agent version ignores would look
  exactly like this. Please report it.

doctor is a report, never a gate: it always exits 0. It is also the fastest way to answer the two questions that quietly break this tool — whether the hooks are firing, and whether your baseline still counts.

Without the hook, you say when

Turning off automatic grading does not mean going back to guessing. It moves the trigger to you. With the MCP server registered, both halves of the loop are things you can ask for in plain English, at the moment they are worth spending time on:

Ask your agent
"Pin the current architecture as a baseline before we start."

"What depends on billing? Show me the impact analysis before you touch it."

"Re-snapshot and diff against the baseline. Did we add coupling we did not intend?"

That is the same loop the hooks automate, run deliberately: pin before a refactor you actually care about, skip it entirely for a spike. If you left the instruction file in place with plain enola install, your agent already knows these tools exist and what they are for — the file it writes for Claude Code is 23 lines and says, among other things, to prefer impact_analysis over re-deriving structure by grepping, and to treat a cycle it introduced as something to fix before presenting the work rather than mention afterwards.

And with no agent at all, it is two commands:

shell
enola baseline pin      # freeze the architecture before you edit
#   …make your change…
enola check             # grade it — exit 1 on a structural regression

Put the gate where it cannot slow you down

The argument for a local hook is speed of feedback: the agent gets the verdict while it still has the context to fix it. The argument against is that it is feedback you did not ask for, on work that is not finished. If the second argument wins for you, move the gate to the pull request and it costs you nothing during the day.

.github/workflows/architecture.yml
name: Architecture
on:
  pull_request:
  merge_group:

permissions:
  contents: read

jobs:
  enola:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: enola-labs/enola-action@v2

That is the whole thing. The Enola Architecture Check action compares the exact pull-request base with the checked-out commit, so there is no baseline artifact to publish or restore, and it reports new findings as source annotations plus an architecture delta in the job summary. It takes the same policy inputs as the CLI: fail-on, min-confidence, target, max-spillover, warn-only. Everything runs on the runner; no source leaves it.

If you would rather manage the baseline yourself — publish it once when the default branch moves, restore it on every pull request, index the tree once instead of twice — the complete workflow in the repository does that. It works because a baseline is portable: Enola identifies a repository by its normalized git remote, not by the absolute path it was pinned at, so one pinned on a runner grades a checkout anywhere else.

Four exit codes, and the two unusual ones are the interesting part:

ExitMeaning
0Clean. No structural regression under the policy that ran.
1Regression. The policy was violated. This is the one you want to block on.
2Error. The gate could not run: no baseline pinned, a bad argument, an inverted snapshot pair.
3Declined. The baseline is not comparable with the current snapshot, so Enola refused to grade rather than blame your change.

In CI, 2 and 3 should fail the job. Both mean the check did not actually run, and a gate that silently passes when it could not run is worse than no gate at all.

Reading enola --explain

enola --explain is the one command with no setup behind it: no baseline, no config, no MCP server, nothing written to disk. Point it at any repository — including one you have never seen — and it prints the architecture on one screen. Here is Excalidraw, 8,740 facts in 0.81 seconds on a laptop, trimmed to the four sections people ask about:

enola --explain ~/development/ts/excalidraw
Architecture
  Pattern:             (none detected)
  cyclic dependencies         2
  layer violations            0

Impact analysis (hotspots)
  coupled modules            60
    high criticality         49
    medium criticality       11
  Top hotspots (by coupling):
    module                            fan-in  fan-out crit     blast radius
    packages/element/src                 835      480 high     68
    packages/excalidraw/components       478      624 high     68
    packages/excalidraw                  483      147 high     70
    packages/excalidraw/actions          127      407 high     62
    packages/common/src                  343       45 high     68

Code health
  god classes (high fan-in)     25
    packages/math/src.pointFrom                  184 dependents
    packages/excalidraw.t                        115 dependents
    packages/math/src.pointRotateRads            74 dependents
  complexity outliers        15
    packages/excalidraw/components.App.onPointe… complexity 175
    packages/excalidraw/components.App.onPointe… complexity 143
    packages/excalidraw/components.App.onKeyDown complexity 141

How to read it:

  • fan-in is how many imports point at a module, fan-out how many point out of it. Ten files in one module importing yours is ten imports but one module, which is why fan-in is usually the larger number.
  • blast radius is how many distinct modules a change there could reach, following imports backwards up to three hops. packages/excalidraw at 70 means most of the codebase is downstream of it.
  • criticality is a tier over that coupling, not a separate measurement — it is the fast way to sort 60 modules into the ones worth looking at.
  • god classes are symbols with unusually many dependents for this repository: 184 of them for one pointFrom function. That is not automatically wrong. It is where a signature change hurts.
  • complexity outliers sit two standard deviations above your own average. A 175 next to a repository whose median is single digits is a real answer to "where do I not want to be at 11pm".

The number that governs all of this is confidence, and it means something exact. 1.00 is proven: a dependency cycle either exists in the import graph or it does not, and Tarjan's algorithm settles it. Anything below 1.00 is measured against your own codebase and is a candidate to review, not a verdict. A god class caps below 1.00 however extreme it gets, because "unusually many dependents for this repository" is a statistical statement and cannot be more than that.

Because it is fast, read-only and needs no state, --explain is also a loop you can hand to an agent: run it, take the top item, fix it, run it again and watch the number move. Nothing is stored between runs, so there is nothing to reset if you abandon the attempt.

enola check is a policy you write

Here is the failure everyone sees first. In the demo service, one plausible change: billing.refund logs the invoice it is refunding, and invoice gains a helper that asks billing what has been paid. Two files, four lines, both perfectly sensible on their own.

enola check
FAIL — 1 structural regression introduced.

Regressions (fail):
  - [cycles] 1.00 — Cyclic dependency detected (2 modules)
      module "src/billing" is part of the cycle

Policy: fail on new findings from [cycles] at confidence >= 1.00.

New findings (advisory — below the failure policy):
  - [dependency-depth] 0.70 — Deep dependency chain: src (depth 5)

Confidence < 1.00 is a candidate to verify, not a verdict.

What changed
  symbols      +1
  dependencies +2
  edges        +8  (imports +2, calls +5, declares +1)

Added (3):
  symbol     src/invoice.outstanding                      src/invoice/invoice.ts:25
  dependency src/billing -> src/invoice/invoice.js        src/billing/billing.ts:1
  dependency src/invoice -> src/billing/billing.js        src/invoice/invoice.ts:1

New coupling (8):
  src/billing                                  --imports--> src/invoice/invoice.js
  src/invoice                                  --imports--> src/billing/billing.js
  src/billing.refund                           --calls--> src/invoice.describe
  src/invoice.outstanding                      --calls--> src/billing.total
  ⋮

tsc --noEmit exits 0 on that change, and it would in your repository too: circular imports between ES modules are legal, and nothing in the toolchain objects. Nothing in the diff looks wrong either, because nothing in the diff is wrong — the problem is a property of the pair, and the pair is not in any one file. That is the class of thing this gate exists for.

Note what else is in that output. A second finding was reported and did not fail anything, and the line under it says why. Enola runs all eleven of its explainers on every single run. The policy decides which of their findings set the exit code, and the default promotes exactly one:

You wantRun
DefaultFail only on a new dependency cycle — enola check
Declared rulesAlso fail on an undeclared cross-repo seam and a declared layer order crossed the wrong way — enola check --fail-on=cycles,intent,layers
EverythingAll eleven, including the eight Enola infers rather than proves — --fail-on=cycles,intent,layers,crossrepo,coverage,unused-routes,god-class,hotspots,dependency-depth,exported-surface,complexity-outliers --min-confidence=0.8
NothingReport everything, fail nothing — enola check --warn-only
ScopeFail if the change spread outside the area you named — enola check --target=src/notify --max-spillover=0

The reason the default is narrow is not modesty about the other ten. It is that an estimate which breaks the build by default is an estimate people learn to switch off. Cycles are proven, so they gate; the rest are argued, so they report until you say otherwise.

The same change, two policies

Declaring a layer order takes a handful of lines, and it is the cheapest way to make the gate enforce something specific to your codebase:

enola-intent.yaml
service:
  name: orders
  description: order, invoice and payment API

layers:
  - {name: api, paths: ["src/api/**", "src/server.ts"]}
  - {name: domain, paths: ["src/billing/**", "src/invoice/**", "src/pricing/**", "src/notify/**"]}
  - {name: storage, paths: ["src/storage/**"]}

Now a second change: the store calls notify.refundIssued directly when a payment is zeroed. Storage reaching up into the domain. Under the default policy:

enola check
PASS — no structural regression.

New findings (advisory — below the failure policy):
  - [layers] 1.00 — Layer violation: storage -> domain
      import of src/notify

These met the 1.00 confidence floor. They did not fail the build because their
explainer is outside [cycles] — add it to --fail-on to enforce them.

Exit 0. The same repository, the same change, one flag different:

enola check --fail-on=cycles,layers
FAIL — 1 structural regression introduced.

Regressions (fail):
  - [layers] 1.00 — Layer violation: storage -> domain
      import of src/notify

Policy: fail on new findings from [cycles, layers] at confidence >= 1.00.

Exit 1, with no cycle anywhere in the change. That violation scores 1.00 because the layer order was declared. A layer order Enola merely recognised from your directory names caps at 0.80, which is below the default floor — so on a repository with no enola-intent.yaml, adding layers to --fail-on changes nothing until you also lower --min-confidence.

Three ways to configure it and get silence

Each of these has cost someone an afternoon:

  • --fail-on replaces the default, it does not extend it. --fail-on=layers stops failing on cycles. Write --fail-on=cycles,layers.
  • --min-confidence lowers the bar. The default is 1.00, already the strictest value there is. --min-confidence=0.8 makes the gate fail on more, not less.
  • A misspelled explainer name is not an error. It matches nothing, so the gate goes quiet instead of complaining. --json echoes the policy that actually ran, which is how you catch it:
enola check --fail-on=layer --json
{
  "status": "clean",
  "policy": {
    "fail_explainers": [
      "layer"
    ],
    "min_confidence": 1,
    "warn_only": false
  }
}

"layer", singular, matches no explainer. Compare that block against what you typed.

Failing on scope, with no finding at all

--target is a different question from everything above. The flags so far grade the delta; --target grades it against your intent. Enola runs reverse-dependency analysis on the pre-change graph to work out which packages a change to that area should have reached, then reports any package you touched outside it. Claiming the storage change above was about src/notify:

enola check --target=src/notify --max-spillover=0
## Scope

**Reached beyond the declared scope.** 0 of 1 package(s) touched were predicted or declared, match ratio 0.

Spillover — touched but neither predicted nor declared:
  - src/storage

A package here was changed by something the declaration did not describe.
That is worth reading even when every finding is clean.

Predicted but not touched (usually fine — the change was narrower than its blast radius):
  - src
  - src/api
  - src/notify

FAIL — 1 structural regression introduced.

Measurements over threshold:
  - [fail] 1 package(s) reached outside the declared scope

Spillover is reported and never fails until you ask it to with --max-spillover=N. A scope check that broke the build the first time somebody passed --target would only teach people not to pass --target. Two snapshots can say what changed; only you can say what you meant to change.

If your language cannot have cycles

Go is the case that makes this concrete. The compiler rejects an import cycle between packages, and Enola's Go module is the package directory — the same unit. So a Go repository that builds has zero cycles by construction, and the default policy is a gate that can never fire. Here is Gitea, 35,492 facts across Go, TypeScript and gRPC:

enola --explain ~/development/go/gitea
Architecture
  Pattern:             go-standard (38% confidence)
  cyclic dependencies         0
  layer violations            0

Impact analysis (hotspots)
  coupled modules           319
    high criticality        244
    medium criticality       75
  Top hotspots (by coupling):
    module                            fan-in  fan-out crit     blast radius
    routers/web/repo                       6      676 high     5
    modules/setting                      590       61 high     306
    modules/log                          579        9 high     321
    modules/util                         530        1 high     337

Code health
  god classes (high fan-in)     25
    models/db.GetEngine                          922 dependents
    modules/log.Error                            856 dependents
    modules/web.GetForm                          240 dependents
  deep dependency chains     10
    .                                            depth 39
    cmd                                          depth 38
    routers                                      depth 37
  large public surfaces      20
    models/issues                                656/770 (85%)
    models/repo                                  519/563 (92%)
    modules/structs                              412/412 (100%)
  complexity outliers        15
    services/migrations.migrateRepository        complexity 85
    routers/api/v1/repo.EditBranchProtection     complexity 77

Zero cycles, and 319 coupled modules with 244 of them high criticality, a function 922 things depend on, an import chain 39 deep, and a package exporting all 412 of its symbols. One of the eleven checks does not apply to Go. The other ten do, and they are where the answers are.

The practical consequence is that on Go, --fail-on is not a tuning knob, it is the gate. The two that reach 1.00 and are therefore enforceable at the default floor are layers — when you declare the order in enola-intent.yaml — and intent, for a cross-repo seam nobody declared. Everything else needs --min-confidence=0.8 as well.

The same demo service as before, ported to Go: internal/storage imports internal/notify to send a refund message. No cycle, and the toolchain is perfectly happy:

shell
$ go build ./... && go vet ./... && echo "toolchain: clean"
toolchain: clean

$ enola check
PASS — no structural regression.

New findings (advisory — below the failure policy):
  - [dependency-depth] 0.70 — Deep dependency chain: cmd/server (depth 5)
  - [layers] 1.00 — Layer violation: storage -> domain
      import of internal/notify

$ enola check --fail-on=cycles,layers
FAIL — 1 structural regression introduced.

Regressions (fail):
  - [layers] 1.00 — Layer violation: storage -> domain
      import of internal/notify

Policy: fail on new findings from [cycles, layers] at confidence >= 1.00.

Eight lines of declared layer order turn a check that could never fire into one that catches what Go's compiler was never going to catch for you.

Rust is the opposite case, and it is worth knowing why

Cargo forbids cycles between crates. It says nothing about modules inside one crate, and Enola's modules are finer than a crate — so the default gate does fire on Rust. Tokio has three of them at confidence 1.00, all legal, all compiling. Asking the graph over MCP — the view your agent gets, from the query_insights tool — Enola says so in the finding itself:

MCP · query_insights(explainer='cycles')
### Cyclic dependency detected (3 modules)
- explainer: cycles · confidence: 1.00
- The following modules form a dependency cycle:
  tokio/src/runtime/scheduler/multi_thread -> …/handle -> …/worker -> …/multi_thread.
  All of them compile into "tokio", so this is NOT a build-order problem — mutual
  references between namespaces or submodules inside one assembly/crate are legal and
  ordinary, and the build system forbids cycles between units anyway. Read it as a
  coupling signal: the cycle means these directories cannot be understood, moved or
  split apart independently.
- suggested actions:
    - Treat as a coupling signal rather than a build defect
    - Extract the shared types the cycle turns on into their own module

The general rule behind both cases: compare Enola's module granularity with the unit your build system refuses to loop. Where they are the same thing — Go packages — cycles are the compiler's job and you should be enforcing something else. Where Enola is finer — Rust modules within a crate, and the same story for namespaces inside one .NET assembly — a cycle is a real finding about coupling that nothing else in your toolchain reports.

You decide what is acceptable

This is the part that surprises people most, and it is the whole design: Enola grades deltas, so accepting a tradeoff is a first-class move, not a suppression file.

Suppose the cycle between billing and invoice is one you have looked at and decided to keep for now. You pin a new baseline while it is there:

enola baseline pin && enola check
Baseline pinned for /tmp/orders
  Generated: 2026-08-14T14:56:17Z
  Repo:      /tmp/orders
  Git:       main @ 8df86a699a2e — clean
  Facts:     56 · Insights: 2
  Snapshot:  sha256:812b277ae339df7ea7013c0516278dab6ae0576c59490942783e6c6148f19e56

PASS — no architectural change.

The gate is green and stays green. The cycle has not been hidden, deleted or downgraded — it is simply not something this change did, and the check only ever speaks about what changed since the baseline. It is still there in plain sight for anyone who asks the question directly:

enola --explain
Architecture
  Pattern:             (none detected)
  cyclic dependencies         1
  layer violations            0

That is the trade in one screen. Accepting a finding costs one command and hides nothing; the state you accepted becomes the floor, and the next change is graded from there.

And when you do fix it — here, by moving the helper into billing so invoice stops importing back — the same command says so:

enola check
PASS — no structural regression.

Resolved by this change (1):
  - [cycles] 1.00 — Cyclic dependency detected (2 modules)
      module "src/billing" is part of the cycle

What changed
  symbols      +1 / -1
  dependencies -1
  edges        +4 / -5  (imports -1, calls +3 / -3, declares +1 / -1)

Removed (2):
  symbol     src/invoice.outstanding                      src/invoice/invoice.ts:25
  dependency src/invoice -> src/billing/billing.js        src/invoice/invoice.ts:1
  ⋮

Findings resolved are reported as deliberately as findings introduced. This is what makes --explain and check usable as an agent loop: the agent gets a specific, checkable statement that the thing it was asked to fix is gone.

Baselines: the "before" and its expiry

A baseline is a frozen snapshot in that repository's .enola/baseline directory. Every repository keeps its own, and three subcommands manage it.

enola baseline show
Baseline at /tmp/orders/.enola/baseline
  Generated: 2026-08-14T14:56:40Z
  Repo:      /tmp/orders
  Git:       main @ 1c500d31ef71 — clean
  Facts:     60 · Insights: 3
  Snapshot:  sha256:f4b6c14e566b93454b9116a1ee670d95ea81648777f2892f6514b7c523e4a311

show answers "is this still the right thing to compare against?" rather than only "does one exist". enola baseline clear removes it, and enola baseline pin takes a fresh snapshot and freezes it — pinning always re-snapshots rather than freezing whatever happened to be on disk, so a baseline is never accidentally three days old.

With no baseline at all, the gate does not guess. It exits 2 and tells you what is missing:

enola check
enola check: no pinned baseline at /tmp/orders/.enola/baseline

The gate needs a "before" to compare against, pinned BEFORE you edit:
    enola baseline pin /tmp/orders
    …make your change…
    enola check /tmp/orders

Or compare against the immediately-preceding snapshot with --baseline=previous.

Why a baseline from another Enola version is not accepted

This is the one that looks like a bug and is not. Upgrade Enola, run the check against a baseline pinned by the previous version, and you get:

enola check
DECLINED — refusing to grade: the baseline is not comparable to the current snapshot.
This is NOT a statement about your change. The delta below would describe how the two
snapshots were produced, not what you edited.

Baseline comparability:
  - baseline was generated by a different enola version (0.3.14 vs 0.3.19) — extractor changes between versions can appear as spurious churn

  Blocking (the gate declined to grade):
    version_mismatch — different enola versions extract differently, so unchanged code can appear as churn

Exit 3, deliberately not 1. A delta is only a description of your change when both sides were produced the same way. Enola releases teach extractors to see new things — a route composition it could not follow before, a call edge it used to miss — so the same commit indexed by two versions genuinely produces different facts. Grading against that would report a hundred "added dependencies" for a change that touched one file, and it would look exactly like a regression you caused.

Four things make a baseline incomparable, and they all block: a different Enola version, a local build whose extractors changed, a different extractor set, and different ignore globs. Each one changes what got parsed rather than what was written. The fix is always the same one command:

shell
enola baseline pin      # re-freeze the "before" with the current build

One case that looks similar and behaves differently: an old baseline. Past three days, Enola tells you exactly how stale it is and what that implies — the delta now also contains whatever the repository itself did in the meantime — and then grades anyway. A long-lived baseline is a legitimate way to measure a week-long refactor, and only you know whether that is what you meant. Staleness warns; incomparability declines.

enola doctor reports the state of your baseline in one line, which is why it is the first thing to run when the gate starts behaving unexpectedly.

enola log: when did this happen?

Everything above describes the tree as it is now. enola log is the one surface that describes the past — every snapshot Enola has taken, recorded as a revision, with what changed since the one before it.

Recording is on by default, so a repository you have been snapshotting already has a timeline. A repository Enola has never seen has none, and the honest fix is to build one from the commits themselves:

enola log --backfill --sample=all
3 commit(s) selected, 0 already recorded, 3 to snapshot.
[1/3] 14097a04c38d
[2/3] 8df86a699a2e
[3/3] 1c500d31ef71
Backfilled 3 revision(s) in 0s.
Run `enola log` to read it.

Backfill only reads your repository — each commit's tree is extracted to a temporary directory — so a read-only or borrowed checkout is a valid target, and re-running resumes rather than repeating. --sample=all|merges|tags|daily, --since and -n choose how much of the history to walk.

enola log --stat
c8fcdad  2026-08-14 16:55  (14097a04c38d, backfill)  initial: 53 facts, 0 findings
812b277  2026-08-14 16:56  (8df86a699a2e, backfill)  2 new findings · +3 facts · +8 edges
         dependency   +2
         symbol       +1
f4b6c14  2026-08-14 16:56  (1c500d31ef71, backfill)  +4 facts
         intent       +4

Oldest first, which is the opposite of git log and deliberate: a changelog answers "what landed recently", and this answers "how did it get like this", which runs forward. --graph draws the branch topology beside it.

The timeline is the index; the useful questions are the ones you ask of it. enola blame takes a module, a symbol, a file path or both ends of an edge, and reports when it appeared and when it went:

enola blame "src/invoice -> src/billing"
812b277  2026-08-14 16:56  (8df86a699a2e, backfill)
    + dependency  src/invoice -> src/billing/billing.js  (src/invoice/invoice.ts:1)

1 event across 3 searched revisions.

--findings searches recorded findings instead, which is how you answer "which change introduced this cycle?" rather than "which change added this import?". Revisions whose contents have aged out are reported as unsearched rather than as absent — "not found" and "not found in what I could read" are different answers.

From there, enola show <rev> opens one revision in full (the finding, its evidence and its suggested actions), and enola diff <a>..<b> answers what happened between two points. A revision is a snapshot id or prefix, a git commit, HEAD~3, @7, a ref name or latest.

It costs little and it is not in your repository. A revision header is about 600 bytes; with contents stored it is roughly 4 KB on a small repository and 34 KB on a 23,000-fact one, all under ~/.enola/graphs/, so clearing .enola/ does not take the history with it. enola gc reports what is there and removes what is not needed:

enola gc
/Users/dejan/.enola/graphs/orders-7be5533c/history

  revisions      3
    with contents 3  (replayable)
  segments       1
  stored         5 KB

Nothing to remove.

--thin-older-than=90d drops old contents while keeping the timeline, and --prune-working discards snapshots of uncommitted trees. The longer version of how the history works, and the three properties it is bounded by, is in How did the codebase get like this?.

Pick the setup that fits the week you are having

If you wantDo thisWhat it costs you
The full loop enola install --hooks The agent grades itself and fixes regressions before telling you it is done. Nothing to remember.
Speed now, safety later enola uninstall plus the GitHub Action Nothing runs while you work. One gate before anything merges.
Manual control enola install without --hooks The agent knows the graph is there and uses it; you say when to pin and when to grade.
Just to look enola --explain /path/to/repo No baseline, no config, nothing written to disk.

All four are the same binary, reading local files, writing nothing outside .enola/ and ~/.enola/. Switching between them is one command in either direction, and that is the point: the check is a tool you pick up, not a thing that happens to you.

shell
curl -fsSL https://raw.githubusercontent.com/enola-labs/enola/main/install.sh | sh

Every command and flag is in the CLI reference, and the change-control loop has its own page in the docs.

FAQ

How do I stop Enola from checking my architecture on every agent session?

Run enola uninstall in the repository. It removes the session hooks and the agent instruction files, restores shared files byte for byte, and deletes only the directories it created itself. If you want the instructions without the automatic grading, run enola install without --hooks. Neither touches your MCP server registration, so the agent can still query the graph when you ask it to.

Does enola check only fail on dependency cycles?

Only by default. All eleven explainers run on every check; the policy picks which of their findings set the exit code. --fail-on accepts any of the eleven names, and --max-spillover can fail a run with no failing finding at all. Two traps: --fail-on replaces the default rather than adding to it, and --min-confidence lowers the failure floor from 1.00 rather than raising it.

Why does enola check say it declined to grade, with exit code 3?

The baseline is not comparable with the current snapshot — usually a different Enola version, a local build whose extractors changed, a different extractor set, or different ignore globs. Different parsers produce different facts for identical code, so grading across that seam would report the parser's changes as yours. Exit 3 means the comparison was unsound, not that your change was bad. Re-pin with enola baseline pin.

How do I see, clear or re-create an Enola baseline?

enola baseline show reports what it describes: when it was generated, the repository, the git ref and dirty flag, fact and insight counts, and the snapshot ID. enola baseline clear removes it. enola baseline pin re-snapshots and freezes the result. It lives in that repository's .enola/baseline, so every repository keeps its own.

Can I accept a finding instead of fixing it?

Yes, in one command. Enola grades deltas against the pinned baseline, so re-pinning while the finding is present makes it pre-existing state and the gate goes green. It is not hidden: enola --explain and the insights still report it. The state you accept becomes the floor for the next change.

What does enola log do?

It reads the architecture history: one line per recorded snapshot, oldest first, with what changed since the previous one. It is strictly read-only and never snapshots to fill a gap. --backfill builds a timeline from your commit history, enola show and enola diff open a revision or a range, enola blame finds when a dependency or finding appeared, and enola gc reports and prunes what is stored.

Is Enola useful for Go or Rust, where circular dependencies are not allowed?

Yes, and the two languages differ. Go's compiler forbids import cycles between packages, which is exactly Enola's module unit, so a Go repository that builds always reports zero cycles — one of the eleven checks does not apply, and the other ten do. Gitea reports 0 cycles alongside 319 coupled modules, 25 god classes and import chains 39 deep. On Go, declare a layer order in enola-intent.yaml and run --fail-on=cycles,layers, which scores 1.00 and fails at the default floor. Rust is different: Cargo forbids cycles between crates but not between modules inside one, and Enola's modules are finer than a crate, so it does find them — Tokio has three. Enola labels those as coupling signals rather than build defects.

Can I run the check in CI instead of locally?

Yes, and for many teams that is the right home for it. enola check behaves identically on a runner, and enola-labs/enola-action pins the pull-request base itself, so there is no baseline artifact to manage. Let exit codes 2 and 3 fail the job: both mean the gate did not run.