Skip to content
Back to skills

Arch Health

ASecurity

Proactive whole-codebase architecture scan — find shallow modules, leaky interfaces, misplaced seams, layer violations, and ball-of-mud hotspots; present them as a RANKED report; then grill through the one you pick and route it to /refactor or /prepare. Run it every few days to keep entropy in check, before the codebase becomes hard to change. Distinct from /refactor (cleans the current diff on demand) — this scans the whole tree and surfaces what to clean. TRIGGER when: the user wants an arc...

  • 2 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 5, 2026
ai-agentsrustgobashnoderefactoringcode-reviewgitapi

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned September 24, 2026

npx -y skills add mik2win/foureyes --skill arch-health --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Arch Health?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Arch Health
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/mik2win-arch-health/badge)](https://www.skillsdirectory.com/skills/mik2win-arch-health)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: arch-health
disable-model-invocation: true
description: >-
  Proactive whole-codebase architecture scan — find shallow modules, leaky interfaces, misplaced
  seams, layer violations, and ball-of-mud hotspots; present them as a RANKED report; then grill
  through the one you pick and route it to /refactor or /prepare. Run it every few days to keep
  entropy in check, before the codebase becomes hard to change. Distinct from /refactor (cleans
  the current diff on demand) — this scans the whole tree and surfaces what to clean.
  TRIGGER when: the user wants an architecture health-check, "what's rotting here", "find the
  worst modules", "where's the tech debt", a periodic ball-of-mud scan, or deepening opportunities.
  DO NOT TRIGGER when: the user wants to clean up specific changed files (use /refactor), review a
  diff for bugs (use /code-review), or plan one known change (use /prepare).
allowed-tools: Read, Grep, Glob, Bash, Write, AskUserQuestion, Agent
effort: high
---

# Architecture Health: $ARGUMENTS

Invest in the design of the system *every day*. Agents accelerate coding — and therefore software
entropy — so codebases get complex at an unprecedented rate. This skill is the counterweight: scan
for where the code is becoming a ball of mud, surface the highest-leverage **deepening
opportunities**, and route the chosen one to real cleanup. Run it regularly, not once.

`$ARGUMENTS` optionally scopes the scan to a subtree (a module path) or theme ("the services
layer", "anything touching billing"); empty = the whole codebase per `PROJECT.md` → Architecture.

This skill **proposes** — it does not apply edits. The fix happens in `/refactor` (small) or
`/prepare`→`/implement` (large), so changes go through the kit's normal verification.

---

## Phase 0 — Load profile

**Tooling preflight — one call, before step 1.** Some tools this skill relies on are **deferred**
by the harness: the session lists them by name only and loads their schemas on demand, so calling
one before it is fetched fails. Listing a tool in `allowed-tools` does **not** un-defer it. Issue
a single `ToolSearch` up front covering the whole run — `select:SendMessage,TaskOutput`
(continuing the same finder across loop-until-dry rounds instead of respawning, collecting a
backgrounded batch) — instead of one round-trip per discovery. A name already loaded costs
nothing to include; a schema discovered missing mid-run costs a turn.

1. Read `.claude/PROJECT.md` — **Architecture** (layers, module map, dependency direction, where
   domain logic lives / must not live), **Commands**, and **Plans location** (where the findings
   report is saved). If missing or `TEMPLATE`, fall back to the root `CLAUDE.md` (always in
   context) when it carries the architecture/commands — proceed on it, noting you're running
   without a kit profile. Only if *neither* has those facts, **STOP** and tell the user to run
   `/bootstrap` first.
2. Read `rules/_generic/code-quality.md` and any stack rules whose `paths` cover the scan scope —
   layer/boundary rules are the violations you're hunting.
3. If `CONTEXT.md` exists, read it so findings are named in the project's vocabulary.
4. Read **`skills/codebase-design/SKILL.md`** (+ `DEEPENING.md`; `WHEN-TO-CUT.md` before a finding
   proposes a new seam) for the deep-module vocabulary —
   every finding is phrased in it (shallow module, leaky interface, misplaced seam, information
   leakage…).

---

## Phase 1 — Scan (fan out)

Map the codebase against the smells in `/codebase-design` → DEEPENING.md. For anything beyond a
small tree, **delegate** — don't read everything yourself:

**First, cheap evidence (stack-neutral, no profile facts needed) to steer the fan-out:**
- `git log --format= --name-only --since='6 months ago' | sort | uniq -c | sort -rn | head` —
  churn hotspots: the files that change most are where entropy concentrates; they seed which
  subtrees to scan first.
- For a candidate module, `grep -rn` its public symbols across the tree to count call sites —
  that call-site count is the **leverage** number Phase 2 ranks on.
- **Co-change probe** — file pairs that keep shipping together, which no import graph shows:
  `git log --no-merges --format='%H' --name-only --since='6 months ago' | awk '/^[0-9a-f]{40}$/{if(c>=2&&c<=20)for(i=1;i<c;i++)for(j=i+1;j<=c;j++)print a[i]" + "a[j];c=0;next} NF{a[++c]=$0}' | sort | uniq -c | sort -rn | head`
  Guards are not optional: the 20-file cap drops sweeps and renames, `co(A,B) / min(freq A,
  freq B)` drops hub files, under 5 co-commits is noise, and pairs coupled by construction (a
  source and its own test, manifest↔lockfile, generated or translated files) are excluded. Report
  only pairs crossing a boundary `PROJECT.md` names that no import path explains — an explained
  pair is `arch-tracer`'s. A top count of ≤2, a squash-merged history, or subjectless commits mean
  the probe found nothing, and nothing is the report; `/decompose` runs its confirmatory form
  over a boundary that already exists.
- Regrouped by commit, that same log gives the two history smells of `DEEPENING.md`: one file in
  commits with unrelated subjects is **divergent change**, a cluster that always moves together is
  **shotgun surgery**. And never write that two things are "coupled" without the change that
  couples them — coupled(A, B, Δ) means changing A for Δ forces changing B, Δ read off those same
  commit subjects.
- Where the profile names events or a message bus, list the emitters and the subscribers, draw one
  edge per event name and look for a cycle: a chain nobody can point at in one file is a finding,
  not a redesign.
- Shallow clone or no history? Skip the churn step and rely on the agent fan-out below.

**Profile drift check (cheap, piggybacks on this periodic run).** `PROJECT.md` goes stale
silently as the codebase evolves — and every kit skill trusts it. Verify its load-bearing
facts against reality: the named layer/module directories still exist (`Glob`), the
test/lint commands still resolve (`--version` / `--help`, never a full run here), the listed
integrations still appear in the code. Report each divergence as a **Profile drift** row
(fact → reality, `path` evidence) and suggest the `PROJECT.md` correction — apply only on
user confirmation. Structural drift (renamed layers, new stack) → recommend `/update-kit`
re-adapt instead of piecemeal edits.

- **`arch-tracer`** agent — trace data flow & dependencies through each layer/module; report
  **layer violations** and dependency-direction breaks (imports pointing the wrong way,
  sibling-to-sibling coupling, domain logic reaching into I/O), plus calls between two nodes of
  the outer tree that never pass through the domain — an acyclic graph pointing downward does not
  prove the domain was not bypassed.
- **`deep-analyzer`** agent — for the densest/most-central modules, assess interface depth, leaky
  abstractions, conjoined methods, information leakage, god-objects.

Run several in parallel over disjoint subtrees; each is blind to the others, so collect and dedupe
their findings. Prefer **structured output**: pass the installed Finding Contract schema
(`.claude/schemas/finding.schema.json`) as the `schema` so N agents' findings merge mechanically
instead of by prose-parsing. Finders run in the **background** by default — launch the batch, keep
doing the cheap-evidence work above, and collect on completion notifications rather than idling.
**But no health verdict while a finder is outstanding:** the notification is queued and becomes a
turn only after the current one ends, so a run that keeps working and then writes its report never
receives it (`delegation.md`; measured 4 of 6 reports lost). Before the verdict, every finder's
output is in context or pulled with `TaskOutput` — an uncollected finder means its subtree is
**unscanned**, and the coverage line says so rather than reading as clean.
Ground every finding in `path:line` — an uncited smell is a guess.

**Loop until dry (unknown-size hunt).** Rot has no known count, so a fixed fan-out misses the
tail. At skill effort `high`, iterate: after each round, aim the next round's finders at the
subtrees and smells the previous round did NOT cover (churn list minus scanned, remaining layers),
and stop only when **2 consecutive rounds surface nothing new**. Dedup each round against
**everything already seen** (including candidates later refuted in Phase 2) — deduping only
against confirmed findings makes refuted ones reappear every round and the loop never converges.
At effort `low`, a single round over the highest-churn subtrees is acceptable — say so in the
report ("coverage: 1 round, top-churn only").

---

## Phase 2 — Rank the opportunities

**Verify before ranking — evidence-gate every ball-of-mud call.** A smell is a guess until
proven. Launch the `finding-verifier` agent on each Phase-1 candidate (batches of 3–4 in
parallel; its 3 lenses: real-today · blast-radius · prior-decisions). **Scale the verification
to the skill's effort**: at `low`, one verifier per finding (single vote); at `high`, findings
heading for a **HACK** verdict get the verifier's **panel mode** — 3 instances in parallel, one
lens each, majority decides, ties die (a HACK label sends someone to restructure code, so it
must survive an adversarial panel, not one skeptic's mood). Each candidate must clear
the gate: the churn/call-site evidence shows it's real today, `git log -p` on the module shows no
logged reason that makes the pattern intentional, and it isn't a **sanctioned exception** — an ADR
/ decision record, or a `paths`-matched boundary rule that explicitly blesses the seam. A blessed
pattern is intent, not mud. Drop REFUTED/STALE findings with a one-line reason — only CONFIRMED
findings enter the ranked table, so the user never triages a false positive or a re-litigated
decision.

**Name the `ignore-if` before you rank.** A finding whose fix would remove, move or collapse a
seam carries one line for the condition under which that shape is deliberate — a wrapper breaking
an import cycle, a `switch` that is a factory's dispatch, an unused method that is a test seam —
plus the grep that checked it. Asserted without that check it is UNVERIFIED, not CONFIRMED; local
findings (duplication inside one file, a long method) need none. And an argument about structure
names a direction, not a preference: to call an abstraction speculative, name what it obstructs on
the stated horizon, and where the team is still learning the domain a loose shape is correct —
what it owes is a recorded consolidation, not a finding.

Label each surviving finding with the shared quality verdict — **SHORTCUT** (bounded debt, schedule
it) or **HACK** (violates the architecture, fix before building on top) — as defined by the
`quality-auditor` agent. A **SOUND** module is not a finding; it never reaches this table.

**Classify before you rank.** "Painful to change" is a signal, not a complaint: the indicator that
a part changed type is that its design can no longer carry current business needs. A CRUD-shaped
module accumulating rules and invariants has exactly two readings — the complexity does not move
the business, so it is accidental and the *requirement* is the finding; or it does, so this is
what the product competes on and the pattern must be upgraded. Say which you took, and name the
first only when those rules tie to nothing in `PROJECT.md`/`CONTEXT.md` scope — otherwise it is a
Phase 3 question for the user, not an assertion. Changing pattern is normal, and expensive only
when the first choice was never made consciously. The component everyone hates and nobody has
managed to replace — a reverted rewrite, a dead `v2/`, years of patching — is that second reading
at its strongest: characterization tests first, model it in place. Rough code nobody complains
about and nobody changes is its mirror, and it leaves the table.

**Name the force, and only from what the repo shows** — change (the churn count), scale, or
complexity (essential, or Conway: authorship spread across team directories). "Sloppy" is not a
force; time, cost, experience, skill and visibility are organisational facts no scan reaches, so
name one only if the user states it and never infer a cause about the people who wrote the code.
Then: is the force still acting? If it is, the cleanup re-erodes — either the fix changes the
force, or the row is marked **recurring** and the route says so.

Structural debt has three answers and all three are verdict rows: keep it healthy (alternate
growth with deliberate consolidation), rebuild it, or **accept it, priced** — chosen out loud,
with the price said: the system persists and stops evolving, and the people who can leave, leave.
Accepted is a row, never a silent drop.

Score each finding so the user spends effort where it pays off most. **Leverage** (how many callers
/ how much code gets simpler) against **cost** (how risky/large the change):

| Opportunity | Smell | Verdict | Leverage | Cost | Score |
|-------------|-------|---------|----------|------|-------|
| `path` — <module> | shallow / leaky / misplaced-seam / layer-violation / info-leak | SHORTCUT / HACK | high/med/low | high/med/low | ⭐ rank |

- **Leverage high** = many call sites simplify, a leaked decision gets owned, a hot interface
  shrinks, or a recurring bug class disappears. **Fan-in alone is not pain**: a module hurts when
  it is concrete AND widely depended on AND changing often — cross the call-site count with the
  Phase-1 churn list before scoring. A DB schema or a shared wire format is the archetype; a
  concrete utility with the same fan-in and no churn is harmless.
- **Traffic** is the third axis, and it ranks: how often the code is touched, not how bad it
  looks. A mild smell in the hottest file outranks a severe one in a dead corner, and ugly code
  nobody reads or changes works as an API — leave it. A module with cold history that nothing in
  the run's scope or the plans backlog points at leaves the ⭐ ranking for a `Parked — no traffic`
  line with its date and commit count — labelled so the operator can disagree, never a silent
  drop. A hot module that cannot be cleaned gets isolation proposed, not a rewrite.
- **Cost** = blast radius (callers touched), test coverage present, reversibility.
- Prioritize **high-leverage / low-cost** first; flag **high-cost** ones as "plan via `/prepare`",
  not quick refactors.
- **One structural finding beats N instances.** When a smell recurs across most of a traced
  surface — a service layer that only forwards on 9 of the 12 paths traced — file one
  high-leverage finding stating the fraction with its denominator ("9 of 12 traced", never a
  percentage of a population you did not enumerate), routed to `/prepare`.

Present the ranked table, and **`Write` it as a durable markdown report** to the **Plans location**
from `PROJECT.md` (e.g. `<plans>/<YYYY-MM-DD>-arch-health.md`) so the findings aren't lost when the
session ends and you can re-run and compare later (its git policy follows `PROJECT.md` → Artifact
git policy). For a visual overview the user can scan, *optionally also* render an HTML version per
[REPORT.md](REPORT.md).

---

## Phase 3 — Pick one and grill it

The user chooses an opportunity (via `AskUserQuestion` if it's a real toss-up). Then **`/grill`**
through it: what *exactly* is shallow, what the deeper design looks like (apply
`/codebase-design` → DESIGN-IT-TWICE.md), what the new seam/interface is, the blast radius, and
whether tests exist to make the change safe. Don't restructure on a hunch — resolve the design
first.

If the deepening encodes a lasting decision (a new boundary, a chosen seam), record it via
`/domain-model` as an ADR so it isn't relitigated.

---

## Phase 4 — Route to the fix

| The chosen change is… | Route |
|-----------------------|-------|
| Small, mechanical, behaviour-preserving, in already-changed/local files | **`/refactor`** (applies + verifies via format/test) |
| Larger, cross-layer, or needs new abstractions/migrations | **`/prepare`** (design + impact + decomposition) → `/implement` |
| Missing test coverage that makes any change risky | **`/test`** first to pin behaviour, then refactor |
| The requirement is the finding, not the module (the accidental reading) | back to the **user** — no code change; the report says what to stop carrying |
| Accepted, priced | stays a row in the durable report with its price written down; re-enters next run |

Hand off the grilled design as the input to that skill. Never apply the restructure here, and
never issue a REWRITE verdict: a rewrite is a row in `/prepare`'s alternatives table with its own
preconditions, not an outcome ranked here.

---

## Output

```
## Architecture Health — <scope>

### Ranked opportunities
| Opportunity (path) | Smell | Verdict | Leverage | Cost | Rank |

### Recommended next
- ⭐ <top opportunity> — <one-line why it's highest leverage> → route: /refactor | /prepare

### Layer-violation summary
- `path:line` — <violation> (per PROJECT.md dependency direction)

### Deferred / high-cost · Parked
- <opportunity> — <why it needs /prepare, not a quick refactor>
- <opportunity> — parked: last touched <date>, <N> commits/yr, nothing in scope points at it

### Downtime map
- <moving part> — what it does while it is down *(no line = SPOF; `rules/_generic/resilience.md`)*

### Profile drift
- <PROJECT.md fact> → <reality, with path evidence> — <suggested correction | route: /update-kit>
  *(or "none — profile matches reality")*
```

---

## Hard rules

- **Propose, don't apply.** The fix goes through `/refactor` or `/prepare`→`/implement` — never
  edit code in this skill.
- **Cite `path:line`** for every finding; phrase it in the `/codebase-design` vocabulary.
- **Evidence-gate every verdict.** No SHORTCUT/HACK call without proof: call-site count, `git log`
  history, and a check that it isn't a sanctioned exception (ADR / `paths`-matched boundary rule).
  A blessed pattern is intent, not mud — leave it SOUND.
- **Rank by leverage, not by ease.** The point is the highest-impact deepening, not the easiest.
- **Facts from PROJECT.md.** Layers, dependency direction, and "where domain logic lives" come from
  the profile — don't impose a generic architecture.
- **Don't deepen for its own sake.** A correctly-simple leaf is not a finding (DEEPENING.md → "Not
  deepening").
- **A health report is not a refactoring campaign.** Large simultaneous restructuring destabilises
  a system even when every step has tests. Take the top-ranked opportunity; the rest stay rows in
  the durable report and re-enter on the next run, or when a change lands in that module. Say this
  when the user asks for all of them at once.

## See also

- **`/codebase-design`** — the vocabulary and the smell/move catalog this scan runs on.
- **`/refactor`** — applies a small deepening + verifies. **`/prepare`** — plans a large one.
- **`/domain-model`** — record a structural decision as an ADR.
- **`/decompose`** — when a finding is service-sized ("this module wants to be its own
  deployable"), the split/stay decision lives there, not in a refactor.
- **`/revisit`** — when a smell traces back to a *recorded decision* (the sanctioned-exception
  check hit an ADR you think is stale), test the decision's assumptions there instead of
  relitigating it here.
- **`/distill`** — hunts *conventional* drift (competing patterns) where this skill hunts
  *structural* rot; run both periodically.
- **`quality-auditor`** agent — owns the SOUND / SHORTCUT / HACK verdict rubric these labels use.

Files in this skill

  • REPORT.md1.9 KB
  • SKILL.md12.9 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.