
archy
io.github.hslee16Published by GitHub account hslee16v0.55.0Updated Nov 7, 2026
Architectural sensor for Python codebases.
Overview
An architectural sensor for Python projects that gives an assistant an import graph to query: blast radius, affected tests, cycles, layer violations and…
- What you can do
- Inspect the import graph and find cycles, including self-loops and TYPE_CHECKING edges
- Check declared layer rules and transitive contracts, failing on violations
- Measure blast radius of a file and list impacted modules and tests for changed files
- Compute a composite quality score, record it over time and compare against a baseline
- Report refactor priorities, hotspots, duplicates, change coupling and house conventions
- Snapshot a baseline and diff it after edits to see new cycles or violations
- When to use it
- Useful when an assistant edits or refactors a Python codebase and should know what a change reaches before making it, which tests to run, or whether the architecture still matches declared layers. Also fits CI gating of cycles, layer rules and score regressions.
- Requirements
- Runs as a local stdio process via uvx or a pip/uv/pipx install; requires Python 3.10 or newer. Transitive contracts need the optional contracts extra and an import-linter config. No accounts, API keys or environment variables are declared. The install command writes MCP config for detected coding agents.
Installation
In SourceWeft
- Open archy in the dashboard and add it to a workspace.
- Enable the server for the chats that should use its tools.
Desktop only via STDIO. STDIO servers start a local process, so they need the SourceWeft desktop host.
Other MCP clients
Follow the launch instructions in the repository.
README
[PyPI] [Python] [CI] [Code style: ruff] [License] [Glama]
Your folders show your architecture. Your imports decide it. archy turns a Python import graph into something an agent can use: blast radius before an edit, the tests that edit affects, the modules most at risk. And it fails when the graph disagrees with the layers you declared. Same graph either way, as a CLI and an MCP server, every session and in CI.
[!IMPORTANT] Status, 2026-09-10: active, and pointed at the loop rather than the surface area.
What archy does. One graph, two front ends. It parses a Python project's imports and calls once, keeps the parse cached, and answers the questions a file tree cannot: what does this change reach, which tests does that affect, where are the cycles, and does the graph still agree with the layers you declared. It runs as a CLI in CI and as an MCP server inside an agent session, off the same graph either way, and it fails when the declared architecture and the real one disagree.
Where it is going: deeper into that loop, not wider. The local-model question that reopened this project on 2026-09-02 is closed, as six interventions, none positive, two of them measured as costing MORE than their control. The write-up is Study 8 in
docs/WHAT_DIDNT_WORK.md, and it is the fifth pre-registered null published here. The feature that line produced,conventions --emit-headers, is kept, because it is derived rather than hand-authored, cheap, and--checkstops it rotting. Only the claim attached to it is retired.What those six nulls did establish is where the failure lives: delivery and retrieval, not capability. Across 132 agent transcripts on a pinned tree,
readfired in 132 of 132 runs with a median of 9.5 calls before the first edit; archy fired in 84 of 132, with a median of zero calls before it. Every archy surface is pull. The model reaches for it after it has already decided, or not at all. Two payloads pushed through the one channel archy owns, the file itself, both came out more expensive than their control, which is evidence about the channel and not the content.So the working design rule is: put the answer in the default output of a command that is already being run, rather than adding a surface that has to be discovered first. The live threads follow from it. One is the vacuity family: a check that could not have failed must not report like one that was evaluated and held, which is now explicit on the contracts result and is being finished across the remaining surfaces. The other is two structural refactors of archy's own code: the module its own tools rate as central and fragile, and the two files that have grown large enough to hide things. New tools and new output formats stay gated behind a usage signal.
The discipline does not relax because the project is active. Thresholds are pre-registered before a run, and any result, including another null, gets published in
docs/WHAT_DIDNT_WORK.mdlike the five before it. Nothing archy ships today claims a local-model benefit;archy brief(v0.46) shipped explicitly on judgment ahead of that measurement rather than on one, and the measurement did not later rescue it.What is not changing. The original use case is still supported and still works: layer governance in CI, blast radius and affected-tests for a frontier agent, the MCP server. Nothing is being removed or renamed. Bugs still get fixed, pull requests still get reviewed, and the
good first issuetickets are still deliberately left open.
Read this first: I measured the premise, and it was wrong
I built archy after watching coding agents produce changes that passed review and rotted the import graph underneath. Then I measured whether that happens, and it barely does.
So: the problem is real (I have watched a developer's own architecture rule get broken in the wild), and it is rare, for agents and humans alike. "Agents will rot your import graph" is a claim I made and have retracted. Nobody has measured what one occurrence costs, so I cannot argue "rare but expensive" either.
The last row is the one that says what archy is for. All 25 unaided agents produced the four layer directories correctly. Every failure was an import going the wrong way: entities reaching down into data access. They got the layout right and the direction wrong, and a directional rule caught all three cases at no cost to the API's behaviour.
That is the shape of the whole thing. Layout is visible in a file tree. Direction, transitive reach and cycles are visible nowhere, at any zoom level, in any single file. And a separate study found that once one of these lands it is never repaired: zero violations were resolved across the sampled corpus, and 2 of 14 repositories sat on broken contracts indefinitely. Rare and permanent, not rare and self-healing.
What that means for what gets built. Feature work premised on "agents will wreck your architecture" went off the table when that premise was retracted, and nothing since has put it back. What survives is narrower and has a number behind it: directional rules, transitive contracts and cycle detection, checked every session. That is a real job and archy does it. What the studies argue against is more of it: five of them produced no evidence that another feature is the missing piece, and the honest reading of headroom-limited results is that the next one will not be either.
So the roadmap that was closed in July stays closed, and being active again did not reopen it. Work here goes into the loop that survived rather than into the list that did not: making what archy already computes arrive where an agent will actually read it, and saying honestly what each answer does and does not cover. Bugs get fixed and contributions are welcome, as before.
The full write-up, including the six measurement artifacts that nearly turned a failed study into a success story, is in docs/WHAT_DIDNT_WORK.md. If you only read one thing here, read that.
The failure it catches
Nothing in that picture is derived. Which layer a module belongs to, and which direction is forbidden, are facts you write down in archy.yaml; archy only checks that the source still agrees with them.
Here is the failure it was built for, compressed into one line. This is archy's own source, under archy's own layer rules, with a single import of the kind an agent adds when it needs a helper and the nearest one is upward:
One import, one edge. A forbidden layer edge, a twelve-module cycle, and the score down 0.003. Nothing in the diff itself says any of that, and no amount of reading the file reveals it, because the rule that makes it a violation is not in the source. You supplied it.
Note the size of the score move. 0.003 is small, and that is the honest shape of this problem: no single edit looks alarming on the number. The cycle count going 0 to 1 and check exiting 1 are the signals that matter here, and the score is what catches the version of this that happens forty times over six weeks. Read docs/SCORING.md before treating the composite as a quality gate.
That example is a direct forbidden import, which is the easy case: an agent that reads archy.yaml first can catch it without archy. The harder and more honest case is a transitive violation, where the edit adds no forbidden import at all and reading the config tells you nothing. docs/WALKTHROUGH.md is a one-command reproduction of that, and it states plainly which archy surfaces catch it (one) and which miss it (three).
Reproduce the example above on a checkout: add that import to src/archy/graph.py, then run the three commands with the uvx prefix. It has to be a separate archy, because that one import is a genuine runtime import cycle, and an editable-installed archy can no longer start to report on itself. archy check exits 1, which is what it does in CI and what the MCP server reports to an agent before it commits.
What archy is not: a code-navigation tool. It will not help an agent find and read code faster; that job belongs to symbol-level, multi-language graph tools like codegraph, and they are better at it. archy answers the other question: you declared this codebase should have these layers, no cycles, and this score; is the agent's edit about to break that, and has the trend been sliding for six weeks? Nothing in a navigation graph carries that intent, because intent is not in the source, you supply it.
The sharp version, re-checked against codegraph on 2026-07-27: it ships no cycle detection, no config in which to declare layers or forbidden edges, and no command that exits non-zero on a violation. It will happily show you that models imports repositories if you ask the right question. It cannot tell you that is wrong, because wrongness needs a declaration and there is nowhere to put one. Descriptive tools answer questions; archy makes an assertion that breaks the build. The two are both local MCP servers and compose fine; run them together. See docs/research/CODEGRAPH_COMPETITIVE_ANALYSIS.md for the full comparison, including where archy loses and why this distinction is a choice they made rather than a wall they hit.
Start in one command
Nothing lands on your PATH: the config it writes runs uvx archy mcp on demand. Prefer a real install? pip install archy, uv tool install archy, or pipx install archy. Either way, try it on a project without installing anything:
Free, MIT licensed, no commercial version planned. One maintainer, Python only. Built by Alex Lee.
Status: v0.55.0, working, installed and maintained. Usable today via:
How the score is computed and how to read it: docs/SCORING.md. Benchmarks against pydantic, fastapi, flask, pytest, and archy-on-archy: docs/CASE_STUDIES.md. Design rationale and comparison with sentrux: docs/LEARNINGS.md.
In the wild
ADOPTERS.md is empty and no issue has yet been filed by anyone but me. Outside pull requests are a different story and recent: three landed on 2026-07-25, two merged. Good-first tickets are labelled and deliberately left for others.
If you are running archy on a real codebase I would like to hear what it found, especially if the answer is "nothing useful" - that answer is now supported by measurement rather than merely possible.
Why
The failure at the top of this page is the whole reason archy exists: I wanted a single number per commit that would have caught it.
AI agents generate code at machine speed, and the reasoning went: without a feedback loop on structural health (module coupling, import cycles, layer violations), codebases drift architecturally even when every individual change looks fine in review.
That reasoning is the part I tested and could not support. Twenty-five agent runs produced zero structural regressions, and human commits break their own declared rules on 0.66% of commits. The drift may still be real over long horizons, which is not what a per-edit measurement can see, but I have no evidence for it and I am not going to assert it. The rest of this section is the case as I originally made it, kept because the citations are accurate even where my inference from them was not.
Where a feedback loop did pay is narrower, and it is the moment code is written rather than the patrol afterwards. Building a new backend to a specified architecture, 3 of 25 unaided agents got the dependency direction wrong; with a checker in the loop, none did, and the API behaved just as well. That is one model, one framework, and the mildest of the Constraint Decay paper's conditions, so it is not a general claim. It does say the loop is worth having at generation time, where the mistake is cheap to prevent and, per the decay study, never repaired afterwards.
What that buys you is placement: it runs in CI, in pre-commit, and as an MCP server (archy mcp), so a coding agent can read its own architectural impact before it commits rather than after review.
The agent-feedback framing is empirically supported by 2025-2026 research: the Navigation Paradox paper shows large LLM context windows do not eliminate the need for structural graph navigation, LocAgent's ablation finds graph edges materially improve code-localization accuracy, the Constraint Decay paper (arxiv:2605.06445) finds agents lose ~30 points in pass rate as architectural constraints accumulate (Clean Architecture layering alone costs -9.1 points, on the open and mid-tier models tested) and that its ground-truth layer/dependency-direction verifier is essentially archy check, and the coding-agent failure-mode literature names the specific patterns (scope drift, cross-file reasoning failure) that an architectural feedback loop is built to catch. Citations, a failure-mode-to-archy-capability mapping, and the resulting roadmap priorities are in docs/research/RESEARCH_METRICS.md §14c.
The underlying mechanism
Beneath the empirical case is a structural one. Anthony Hobday, writing about software quality, names it precisely: "as the number of things goes up, the number of relationships goes up even faster. Eventually it's impossible for people to properly consider all of those relationships." Coherence is the state where those relationships still hold together; entropy is its steady loss as a system grows. A single author keeps a codebase coherent by remembering every edge. An agent generating code at machine speed cannot, and neither can a team past a certain size.
That relationship load is exactly what archy reads. Coupling, the DSM, import cycles, and change-coupling are all measures of how far the graph has drifted from "one person can hold it in their head." archy externalizes that memory into a number and a trend, so the growth in relationships stays visible instead of being discovered during a refactor that blows up.
Scope
- Python only. The cross-language story belongs to sentrux; that division is settled. archy goes deep on Python (transitive contracts, SDP, NCCD,
if TYPE_CHECKING:handling inarchy cycles) rather than broad across languages; seedocs/LEARNINGS.md§"Competitive landscape". - Tree-sitter powered. Robust to in-flight edits and partial files; survives syntax errors that would crash
ast. - Score that trends over time. A single number per commit, persisted, plotted. Trend matters more than the absolute value.
- Rules as YAML. "Layer X cannot import Y." No DSL, no plugins (yet).
Non-goals
- Multi-language analysis
- Replacing linters, type checkers, or test runners
- Generating code or auto-fixing violations
Quick start
Covered above in Start in one command; this section is the detail behind it.
Requires Python 3.10+ (archy depends on mcp>=1.28.1 which is 3.10-only). If you only have system Python 3.9 or older, install a newer Python first or use uv, which manages versions for you and is what uvx comes from.
Using archy as an MCP server inside an AI coding agent? Skip the manual config and run uvx archy install, which wires it into Claude Code, Cursor, Codex, opencode, or Continue automatically and writes a config that invokes uvx archy mcp, so archy never needs to be on your PATH. See docs/INSTALL.md.
All examples below use the installed archy command. If you're working from a checkout, prefix them with uv run (e.g. uv run archy graph .).
See docs/SIXTY_SECOND_TOUR.md for the copy-paste path from zero to first score.
Inspect the graph
Find import cycles
Ranking the modules is what makes a cycle visible: every ordinary import points down a rank, so the one edge pointing back up is the entire finding.
Tarjan SCCs of size >= 2, plus self-loops (a module importing itself). Use --strict in CI to fail on any cycle.
The usual way to break an import cycle is to move the back-import under if TYPE_CHECKING:, where it never runs. By default archy still counts that edge, and a cycle that has such edges says so (type_checking_edges in JSON). --exclude-type-checking (or exclude_type_checking_imports: true in archy.yaml; the flag overrides it either way, --include-type-checking turns it back off) leaves them out. It is opt-in so exit codes do not change. With it on, the output reports how many edges it excluded, and --format json becomes {"cycles": [...], "excluded_type_checking_edges": N} so an exclusion that empties the list still shows. Only the body of a bare if TYPE_CHECKING: or if typing.TYPE_CHECKING: counts; an else: branch, a function-local import, and an edge that any unguarded import or call also supports stay in. archy check layer rules and the score's acyclicity axis are unchanged.
Enforce layer rules
Reads archy.yaml from the repo root. Exits 1 on any violation. See Layer rules below.
Transitive contracts (archy contracts)
archy check only sees direct edges. archy contracts wraps import-linter so the same layer story is enforced transitively (A → B → C still counts as A reaching C). It is the strictness upgrade for projects whose layers leak through indirect paths.
archy check --contracts runs this same transitive verdict inline, nested under the check output. It never changes check's own exit code: a flag that can turn a passing check into a failing one because an optional dependency is absent would be unsafe to leave on in CI. Use the standalone archy contracts when the transitive result should itself gate the build.
A kept contract is not automatically protection. A contract whose module expressions match nothing in the graph holds no matter what the code does, so all_kept calls it kept while nothing was ever checked. archy contracts exits non-zero on verified (evaluated and held), not on all_kept, and archy check --contracts reports transitive_checked: false for the same reason: a rule that could not have failed was not evaluated. The text output marks such a contract ?? rather than OK and names the expressions that matched no module; --format json adds top-level verified and unverifiable alongside kept/broken, and per-contract matched_nothing and unmatched_expressions. A .importlinter type = layers contract needs no such flag: import-linter itself refuses to run when a required layer's module is absent, which archy reports as a config error, and an optional layer (written (name)) is absent by design.
Directories the engine does not read. import-linter's engine (grimp) skips a directory with no __init__.py once it is inside a regular package, so a path through it is invisible and the contract is reported as kept. archy contracts and check --contracts list those directories (unscanned_dirs in JSON and the MCP payload; one # N directory not visible to the contracts engine line in text). It is a note on the verdict and does not change verified or the exit code, since a stray scripts or fixtures directory would otherwise fail every existing gate. Add the __init__.py to bring the modules under the contract.
Config resolution. archy contracts reads, in order:
- The
--configargument if passed. .importlinterin the project root: the canonical contracts config.archy.yaml: best-effort fallback. Eachforbid:rule becomes one Forbidden contract checked transitively. Emits aUserWarningbecause this path cannot expressignore_imports, so any legitimate transitive edge (e.g., a service layer reachingpsycopgthrough a sanctionedapp.libs.db.*module) will be reported as a violation with no way to whitelist it.
Two configs, one concern each:
archy.yamlowns layer definitions, direct-edge gating (archy check), required-reach rules (required:),sdp:,exclude:, androots:..importlinterowns transitive contracts: all five contract types (Forbidden, Layers, Independence, Protected, AcyclicSiblings) andignore_importswhitelists.
Reach for .importlinter as soon as you need transitive enforcement at all; the archy.yaml fallback is a zero-config onramp, not a feature target. See .importlinter in this repo for a real-world example, and the import-linter contract types reference for the full grammar.
Common case: forbid services from reaching psycopg but allow the sanctioned db library to do so:
Compute a quality score
Composite of modularity, acyclicity, depth, equality, and complexity (geometric mean of five axes). See docs/SCORING.md for formulas and how to interpret the breakdown. These five axes were chosen after surveying ~15 alternatives from the package-metrics literature (Martin's I/A/D, Lakos's NCCD, MacCormack propagation cost, Structure101 fat/tangle, reflexion models, cognitive complexity, hotspots, logical coupling, dead/duplicate-code detection); Martin's I and the Stable Dependencies Principle check are also shipped as a per-module diagnostic and an archy check rule. See docs/research/RESEARCH_METRICS.md for the full validation, what was shipped, and what was deferred and why.
Track score over time
Persist per-commit scores to .archy/history.jsonl and chart the trend.
Regression gate
Fail if the current score drops more than --strict-tolerance (default 0.02) below the most recent recorded run.
Blast radius
List internal modules that transitively depend on a given file. Useful before refactoring or removing a module.
Affected tests (CI gating)
archy affected is the CI-shaped cousin of archy impact: given changed files, it returns the impacted modules pre-classified into tests and other downstream code, with a depth cap (default 5 hops) so a one-line edit doesn't fan out to thousands of nodes on a monorepo. Pipes naturally from git diff:
Test classification defaults to pytest conventions (test_*.py, *_test.py, anything under a tests/ directory); override with --filter <glob>. A filter that matches no file in the graph is an error (exit 1) rather than an empty selection, and it ends with a did you mean when a directory is close. A tests/ with no __init__.py beside a src/ layout is scanned (test files outside a package are named from the scan root, as in a flat layout), so affected selects them; other .py files outside every package (docs/conf.py, noxfile.py, examples/) are still not in the graph, and a filter that names only those is refused. Internal modules only; vendored or third-party code is not traced.
Design Structure Matrix (archy dsm)
The DSM puts modules on both axes in a chosen ordering, and cell (row=source, col=target) is non-empty when source imports target. Reading positionally exposes properties any single scalar would hide: block-diagonal cohesion under community grouping, above-diagonal back-edges under topological ordering, off-block layer leakage under layer grouping. Visualization-only (docs/research/DSM_EMPIRICS.md for why no scalar joins the score).
archy dsm refuses ASCII rendering for projects larger than --max-nodes (default 80) with an actionable error pointing at --focus, --package, or --format json.
Static HTML export (archy render)
Every other archy surface targets the agent. archy render targets the human reviewing what the agent did: a single self-contained HTML file to attach to a PR, drop in docs, or open offline. No JavaScript, no CDN, no vendored bundle, no server, and byte-stable for a fixed input, so two exports diff cleanly.
What red means follows the ordering you asked for, because only one ordering encodes it: under --group=topological red is a back-edge (a cycle seed), and under --group=community or --group=layer, where block order is not a dependency order, red is an edge crossing a block boundary. The DSM view refuses matrices larger than --max-nodes (default 300) rather than writing an unreadable file.
There is no graph view. A node-link diagram is the one view that needs a vendored layout engine, and it is also the lowest-signal of the three; it stays deferred behind a usage signal (see docs/SPEC_VISUALIZATION.md).
Snapshot and diff (agent feedback loop)
Capture a baseline at the start of an editing session, then diff after edits to see exactly which cycles or layer rules changed. See docs/AGENT_LOOP.md for the full playbook (also available via the MCP server's loop prompt).
Run as an MCP server
Stdio transport, so AI agents can call archy directly. See MCP server below.
MCP server (archy mcp)
The server is backed by a persistent parse cache (.archy/index.db): each tool call re-parses only the files whose content changed since the last call, so warm graph builds stay in the low seconds even on very large repos (benchmarked: 21.5s cold to 2.5s warm on Home Assistant's 17,299 modules). The cache is transparent and disposable; deleting .archy/index.db only costs one cold rebuild. The graph is always re-derived from the current files, so a cached result is never stale. archy index sync warms it explicitly; archy index clear removes it.
archy mcp exposes fourteen tools and one prompt to MCP-aware AI agents (Claude Code, the Anthropic API, etc.):
The server also exposes a loop prompt with the agent feedback-loop playbook (snapshot at start, impact before edit, diff after edit). Discoverable via the standard MCP prompts/list call. See docs/AGENT_LOOP.md for the human-readable version.
The archy mcp server still keeps a debounced filesystem watcher warming .archy/index.db so graph builds stay fast, and every tool syncs on demand so a result is never stale. The index-freshness readout that used to be the archy_status MCP tool is now the CLI archy index status (#267): freshness is diagnostic plumbing an agent rarely needs mid-task, not a per-edit decision.
Tool output contract (structured output)
Every tool declares an outputSchema (JSON Schema, derived from its return model) in tools/list, and every tools/call returns both a structuredContent object (validated against that schema) and a text block with the same JSON, per the 2025-06-18 MCP structured-output spec. Every tool except archy_snapshot is annotated readOnlyHint: true (closed-domain, idempotent, non-destructive), so trusted clients can auto-approve archy's calls instead of prompting on every read. archy_snapshot overwrites .archy/baseline.json, so it declares readOnlyHint: false, destructiveHint: true and clients may ask before running it. Sequence returns (archy_cycles, archy_score(view="history")) and union returns (archy_diff, archy_graph, archy_dsm) are wrapped under a top-level result key since structuredContent must be a JSON object; for unions every branch (including the in-band *ErrorPayload shapes) is a conforming anyOf member.
Error model (recovery contract)
archy maps failures onto MCP's two error mechanisms with one convention, so an agent has a single recovery contract:
- Usage error →
isError: true(a raised exception): an invalid argument value (e.g.response_format="xml",last_n=0), a malformedarchy.yaml, or a project over the scan ceiling. The caller must fix the call or the environment. - Recoverable / advisory → in-band result (
isError: false): an expected precondition that isn't met but is recoverable, or a valid-but-degraded result. These are normal results the agent branches on. Either a union variant when there's no usable result (no baseline →DiffErrorPayload, output too large →*TooLargePayload, no config →CheckErrorPayload, no DSM snapshot →DSMErrorPayload), or an advisory field on an otherwise-valid payload (ContractsPayload.available=false,WhatToRefactorPayload.git_available/WhatToRefactorPayload.note). The marker for a "no usable result" variant: a payload with anerrorfield and no success data. - Protocol error (JSON-RPC): unknown tool or a missing/mistyped required argument, handled by the framework.
Wiring it into your agents
One command detects your installed clients (Claude Code, Cursor, Codex CLI, opencode, Continue) and wires each one up:
This registers the uvx archy mcp server, drops a short rules file so the agent knows when to call the tools, and (on Claude Code) seeds the permissions.allow allowlist. It does not install a binary or the Claude plugin. The full guide, including the per-client path matrix, the manual stanza for unknown clients, plugin-vs-installer guidance, and troubleshooting, is in docs/INSTALL.md.
The lowest-friction path specifically on Claude Code is the bundled plugin at plugins/claude/: /plugin marketplace add hslee16/archy then /plugin install archy@archy from inside Claude Code (or claude --plugin-dir /path/to/archy/plugins/claude from a checkout). See docs/INSTALL.md for when to prefer it over the installer.
Regression-gate semantics
--strict reads the last row from .archy/history.jsonl and compares the current score against it. Drops beyond the tolerance fail with exit code 1. The default tolerance (0.02) matches the threshold sentrux's gate uses. This gives archy parity with sentrux's regression-gate use case while keeping the long-term JSONL history for archy trend.
CI integration
GitHub Action
archy ships a composite action you can drop into any workflow:
Inputs (all optional unless noted):
Pre-commit hook
Add to .pre-commit-config.yaml:
archy-score-strict reads .archy/history.jsonl; commit a baseline first with archy score . --record.
Adopting on a codebase that already has cycles
archy-cycles fails on any cycle, so on most existing codebases it fails on day one, and a gate that always fails gets deleted. Use the ratchet instead: commit the findings you have today, then fail only on new ones.
Then use the archy-no-new-cycles hook, or archy diff . --strict --baseline archy-baseline.json in CI (the Action's command: diff). It exits 1 on a cycle or archy.yaml layer violation the baseline does not carry, names the closing import edges as path:line, and never fails on score. The file is sorted and has no score, line numbers or SDP violations (their instability ratios move with unrelated edits), so regenerating it after paying down debt gives a reviewable diff.
What counts as new: a cycle among modules outside every baseline cycle, an existing module pulled into a cycle, or two baseline cycles merged. Shrinking or splitting a cycle passes, and so do renames, a new file joining an existing cycle, and a new import between two modules already in the same cycle. --strict always needs --baseline and refuses .archy/baseline.json even when named, because archy snapshot and the MCP archy_snapshot tool overwrite it. New findings are rare on a mature codebase (0.66% of commits in archy's own measurement), so treat this as cheap insurance that keeps the gate installed, not as a regression detector.
Layer rules (archy check)
Drop an archy.yaml at the repo root declaring layers and forbidden directions:
Unknown keys are errors. A key archy.yaml does not read, at any level, is refused rather than ignored: forbidden: for forbid: used to print "No layer violations" and exit 0, because a rule that cannot fire looks exactly like a clean codebase. The error names the key, the closest allowed spelling when there is one, and the allowed keys. Top level: layers, forbid, required, exclude, roots, sdp, max_modules, min_layers_present, exclude_type_checking_imports; a layer takes modules; a forbid entry from and to; a required entry source, must_reach, reason and within; sdp takes enabled, tolerance and mode. exclude: matches whole directory names (generated), never paths or globs, so an entry containing /, \, *, ? or [ is refused too.
Pattern syntax. Dotted-name globs: * matches one segment, ** matches zero or more. myapp.domain.** covers the package itself and every descendant, whereas a bare myapp.domain matches that one module exactly and silently leaves its submodules unlayered (import-linter contracts, by contrast, match a named package together with its descendants). archy check flags the bare form when it leaves descendants out; see Coverage below. A layer pattern may start with **. to mean "this package wherever it sits" (**.services.** matches services, conduit.services.user, not myservices.x); archy contracts cannot derive import-linter contracts from such a layer, so it refuses with a message when a forbid rule uses one. Modules must belong to at most one layer.
Layers that name a third-party package. orm: {modules: ["sqlalchemy.**"]} is a real layer: forbid: [{from: domain, to: orm}] fires on a direct import, the layer counts toward layers present and min_layers_present, and archy contracts evaluates it. Edges into such a layer are reported as a separate "internal-to-external" count (external_edges_governed in the JSON and MCP coverage), apart from the internal edge ratio. Only the top-level name exists in the graph, because every external import collapses to its package, so write sqlalchemy.** or sqlalchemy; a deeper pattern such as sqlalchemy.orm.** (in a layer or a required: rule) can never match and is refused with that suggestion. An external layer can be the target of a forbid rule but not its source in archy contracts.
Required reach (required:). The inverse of forbid:. A forbid rule catches an edge that should not exist; a required rule catches one that should exist and does not, which forbidding cannot express:
Every module matching source must transitively reach must_reach, counting the implicit package-__init__ import Python guarantees (importing a.b.c runs a/b/__init__.py first). So one import in app/commands/__init__.py satisfies the rule for every command module, which is usually the correct fix -- a direct-import rule would report all of them as violations after that fix.
This came from a production incident: 34 command modules run standalone, each needing a SQLAlchemy model registry imported before first mapper configuration. 11 imported it, 21 crashed at runtime, and 2 passed only because they happened to reach it through unrelated imports. Those 2 are why the rule is defined over reach rather than imports.
reason is carried into every output surface, because "X does not reach Y" is a fact about the graph and not an explanation, and a rule nobody can justify gets deleted rather than satisfied. A rule whose patterns match nothing is reported as a violation, not passed over. Note pkg.** includes pkg/__init__.py itself; use pkg.* to scope the rule to submodules.
Be honest about what this does: archy cannot derive such a requirement (that is framework semantics, not graph structure). Someone has to know the constraint and write it down. What the rule then does is find every other module that violates it and stop the next edit from undoing the fix -- a ratchet, not a detector.
Derived entrypoints (source: "@entrypoints"). A glob covers the entrypoints its author knew about. A script added later outside it (app/worker.py next to app/commands/) is not governed, and that is exactly the module most likely to miss the bootstrap import. @entrypoints binds the modules archy derives as runnable instead:
An entrypoint is a module with a top-level if __name__ == "__main__": guard (parenthesized or and-combined forms count), a __main__.py, or a target of [project.scripts], [project.gui-scripts] or [tool.poetry.scripts]. Script targets are read from every pyproject.toml between a scanned module and the repository root (the nearest directory holding .git, .hg, .jj, .svn or .sl; with none, nothing above the scanned directory is read), so archy check src sees what the project root declares and each monorepo sub-project contributes its own. Each violation says which of these made the module an entrypoint. Derivation finds every runnable file, guarded test modules and one-off scripts included, so most rules want within: to scope it to the part of the tree that carries the obligation; within is rejected on any other source, where editing the pattern does the same job.
A rule that derives nothing is reported as a violation. So is a pyproject.toml that cannot be parsed, since dropping its script targets would let the rule pass over the modules they name. A script target that matches no scanned module (an exclude:d directory, a re-exported third-party tool) is skipped; under a namespace package without __init__.py, acme.tool.cli is matched to the tool.cli archy scanned. Not read: setup.py and setup.cfg console_scripts, and .py files that sit outside every package, which have no module name in archy's graph (test files are the exception: they are named from the scan root, so affected can see them).
Excluding directories. archy skips two kinds of directory by name.
Always, whatever they hold: .git, __pycache__, .mypy_cache, .pytest_cache, .ruff_cache, .tox, .eggs, .venv, node_modules.
Only when the directory is not a package (no __init__.py): build, dist, venv, site-packages. Build output has no __init__.py at its top level (setuptools writes build/lib/pkg/), so this keeps artifacts out, while a first-party package that happens to be called build (pip keeps one at src/pip/_internal/operations/build/) is scanned instead of vanishing. Before #518 those four names were dropped unconditionally, which made archy impact report zero dependents for code that has them.
Add an optional exclude: list of directory basenames for anything else (codegen output, vendored code) to skip. Each name is matched anywhere in the project tree:
A name you exclude yourself is an explicit choice and is skipped unconditionally, whether or not the directory is a package.
exclude: applies to every analysis (graph, cycles, score, check) and the equivalent MCP tools, and the archy mcp watcher uses the same predicate as a cold scan, so the two cannot disagree about what is in the project.
Scan-size guard (max_modules:). archy refuses to start a scan of a tree with more modules than a ceiling, so a stray vendored, cache, or generated directory that the named exclude: skips do not cover cannot silently wedge a scan for minutes. The default (10,000) sits well above the largest real projects; a scan that trips it stops with a message pointing at exclude: / a narrower path. Override or disable it:
Namespace packages (roots:). archy discovers packages by walking __init__.py files. PEP 420 namespace packages (no __init__.py) are invisible by default. Declare them as roots so descendants get qualified names:
A root names the package directory itself, not the directory that contains it: src/service makes service.db, while src would name src.service.db. This is the opposite of how PYTHONPATH entries work.
Without roots:, a project like app/libs/db.py (no app/__init__.py) is either skipped entirely or shows up as a top-level libs.db, which makes layer rules like app.libs.** match nothing.
Layer presence (min_layers_present:). Forbidding edges between layers says nothing about whether the layers exist. A codebase that collapsed four layers into one module satisfies every forbid rule by having no cross-layer edges at all, and passes silently. Set a floor to catch that:
Empty declared layers are reported either way, because every rule naming one is dead:
Unset by default, so existing configs keep their exit codes. The shape is taken from the Constraint Decay paper (arxiv:2605.06445), whose architecture verifier pairs a dependency-direction rule with exactly this presence floor ("at least 3 of the 4 canonical layers present as distinct directories"). bench/fixtures/conduit_clean/ reproduces its three cases.
It is a backstop, not the main event, and the measurement says so. Across 50 agent-generated backends, every single one produced all four layer directories: this check never fired once, while the direction check caught every failure. Keep it for the collapsed-into-one-module case it is named for, but if you are deciding where to spend effort in a config, spend it on forbid rules.
Discovery. archy check walks PATH upward to find archy.yaml unless --config is given. Exits 1 on violation.
Coverage. Every check reports how much of your code the rules actually reach, on a pass as well as a failure. The example is archy's own config before every module was layered (#686); archy check . on this repository now reports 47 of 48 modules (98%):
That line exists because a rule set that cannot fire is indistinguishable from a clean codebase: without it, a config governing 14% of your import edges prints the same "No layer violations" as one governing all of them. The edge percentage is the one to watch, since a config can put most modules in layers while ruling almost none of the edges between them. Coverage is scoped to the root packages your patterns name (every module is counted if a layer uses a floating **. pattern), so scripts and benchmarks sitting beside your package are counted separately rather than dragging the number down. --show-unlayered lists the modules no layer matches.
The forbid rules declared line is a handoff, not a warning: when the direct pass cannot reach enough of the graph to verify a declared forbid rule, check names the flag that can instead of reporting a pass it did not earn.
When the reach is degenerate, the verdict says so instead of leaving it to the next line. A config whose patterns are bare qualnames, so they match one module each and none of the code beneath it, looks like this (exit code unchanged):
If no module in the tree falls under any root the config names, the verdict says that no rule can fire.
The numbers above are archy's own, and they are not flattering. They are printed here because the alternative is not knowing.
archy enforces its own architecture this way; see archy.yaml at the repo root and the archy check . step in .github/workflows/ci.yml.
Stability check (sdp:). Optionally enable Robert Martin's Stable Dependencies Principle: a module should not import one that is less stable than itself. Stability is I = Ce / (Ce + Ca) where Ce is outgoing internal imports and Ca is incoming, so I = 0 means "depended on, depends on nothing" (most stable) and I = 1 means "depends on lots, nothing depends on this" (least stable).
When enabled, archy check flags every internal import edge whose target's I strictly exceeds the source's (plus tolerance). Per-module I is also surfaced in archy graph --format json whether or not sdp: is enabled, so you can audit before turning enforcement on.
Gradual adoption. Existing codebases will often have SDP violations on day one. Set mode: warn to report violations in the output (and archy_check's sdp_violations payload) without failing the gate, then flip to mode: error once the count is at zero. Layer-rule violations always fail the gate regardless of sdp.mode.
Development
One pytest case (test_pagerank_matches_networkx_when_available) compares archy's hand-rolled _pagerank against nx.pagerank, which needs numpy/scipy. The dependency is intentionally not in the default install (archy stays scientific-stack free); to run that test locally, sync the optional parity group:
Roadmap
This roadmap is closed. Nothing below is planned, and the project being active again does not revive it: current work is the loop described in the status note at the top of this page, not this list. docs/ROADMAP.md and docs/FUTURE.md carry the same closure and the reasoning behind it.
Both phases of the index-and-install work shipped (Phase 1 install-DX in v0.25.0 / v0.26.0, Phase 2 persistent index + watcher in v0.27.0). What follows is kept as a record of what was considered and why, not as a plan. Several items rest on a premise that has since been retracted, so read docs/WHAT_DIDNT_WORK.md before picking one up. Anyone is welcome to.
Considered and never started:
- Per-module score breakdown so an agent can ask "did my edit make this module worse?" rather than "did the project overall regress?". Pairs with
archy_diff. - Opt-in agent hooks (
archy install --hooks): register a lifecycle hook in the agent client (ClaudeStop, CursorafterFileEdit, ...) that runs the archy gate automatically after edits, so the loop fires whether or not the agent remembers to call the tools. Spec:docs/SPEC_INSTALL_HOOKS.md. - Static fragility proxy (high-instability x high-fan-in) as a git-free hotspot stand-in. Advisory, not a score axis. (Duplicate-function detection has shipped as the
archy duplicatesCLI command: a two-tier surfacer with a primary "likely duplicate" list and a demoted "same-class / boilerplate variant" list. A literature review confirmed ~50% refactorability precision is the expected ceiling for any similarity-only detector, so the semantic call is left to the agent; change-history co-change is the precision layer, shipped asdemote_independent(#242) on the change-coupling machinery #131. Exposed on both the CLI (archy duplicates) and MCP (archy_duplicates, the 14th tool).)
The shipped history moved to docs/SHIPPED.md, grouped by the part of the tool
each feature belongs to. It is a record of what was built, not evidence that any of it helped.
Empirically rejected (kept here so they don't get re-proposed): type-hint coverage in any form, calls_per_edge as a 6th axis, HTML output on agent-facing commands, dead-function detection, multi-language analysis. See docs/ROADMAP.md for the evidence behind each.
See docs/FUTURE.md for the longer list and docs/LEARNINGS.md for design notes.
Contributing
See CONTRIBUTING.md for style rules. Notably: no em-dash characters (U+2014) anywhere in the repo.
Security and what archy touches
What archy reads, writes and runs on your machine, with a test behind each claim, is in docs/SAFETY.md.
Reporting security issues
Please report vulnerabilities privately via the Security tab, not as a public issue. See SECURITY.md for scope and response targets.
License
MIT, see LICENSE.
Source: README.md at commit 09fe6b9
Tools
0Version history
14- v0.55.0LatestNov 7, 2026
- v0.54.0Nov 6, 2026
- v0.53.4Nov 5, 2026
- v0.53.3Nov 4, 2026
- v0.53.2Nov 3, 2026
- v0.53.1Nov 1, 2026
- v0.53.0Oct 30, 2026
- v0.52.0Oct 29, 2026
- v0.49.0Oct 26, 2026
- v0.48.2Oct 23, 2026
- v0.48.1Oct 18, 2026
- v0.48.0Oct 16, 2026
- v0.47.0Oct 9, 2026
- v0.46.2Sep 16, 2026
Score
50% of the applicable checks met
How complete this listing is, computed from the registry entry, the endpoint and public sources. Not a rating of the server and not a statement that it is safe or working today: reachability is a dated check, and no installs or reviews go into the score.
- Reached in the last 30 days25 pts
Keep the endpoint answering the MCP handshake; SourceWeft probes it every 30 days.
- Auth declared10 pts
Declare the credential headers in the registry entry, or keep the endpoint answering the probe so its auth can be classified.
- Tools listed15 pts
Answer tools/list without a credential, or ship a package that starts in a sandbox without one.
- DescriptionRequired15 pts
- Logo10 pts
- Documentation15 pts
- License10 pts