AI Readiness Assessment + Complexity Hotspot
SkillAI & modelsAssess a codebase's readiness for AI agent contributors using the layered contract model, and generate a complexity hotspot SVG treemap (size = LOC, hue = cyclomatic complexity, saturation = recent git churn). TRIGGER when the user types /assess, asks for an AI-readiness review, wants a complexity heatmap or hotspot map, asks 'how complex is this code?', wants migration risk triage, or asks for a codebase snapshot/report. Produces an MD report + SVG that can be opened as a PR in the target repo.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the AI Readiness Assessment + Complexity Hotspot skill
What this skill tells your AI
The instructions your AI receives, as published by bjcoombs/ai-native-toolkit in skills/assess/SKILL.md and read by ahel’s review.
Three artefacts in one pass against a target repo:
- Layered contract assessment - 0-8 score across navigability, runtime liveness, code design, linters, architecture tests, CI, coverage, review bots, and AI project management.
- Complexity hotspot SVG - Codecov-style treemap of the code. Size = LOC. Colour = cyclomatic complexity. Saturation = recent git churn. Vivid red = complex AND active = riskiest to change.
- Doc navigability SVG - a node-graph of the docs. Structure = connectivity (centre = entry, rim = unreachable, dashed ring = orphan); colour = staleness (vivid red = a frozen doc beside churning code = a lying map); size = file length. Folds navigability and the decaying-map signal into one artifact.
Both SVGs are colour-blind-safe by default (OrRd ramp, no red-green).
All land as files inside the target repo. The skill always writes them locally; after writing, ask the user whether to open a PR in the target repo with the artefacts.
The model: truth-pressure, not presence
Read this before scoring - it changes how you score. Across every layer, the real signal is never presence. It is whether a thing is under active pressure to stay true:
- Tests keep behaviour honest (CI fails when it's wrong).
- Retros / feedback loops keep the process honest (Layer 8 scores whether retros are carried out, not merely present).
- Maintenance keeps docs honest (a wiki tracked against code churn).
- Telemetry / liveness keeps relevance honest (is this code actually exercised).
So AI-readiness is the degree to which a codebase's self-descriptions are kept honest, not the degree to which scaffolding exists. Score artefacts on maintenance pressure, not existence. A stale-but-present doc scores at or below absent: missing makes the agent go look; confidently-stale makes it navigate fast to a wrong, current-looking conclusion.
The 9 layers (0-8) fall into three bands, ordered by dependency - what must hold for the next band to mean anything:
- Read-side foundation (L0 navigability, L1 liveness) - can the agent form a true picture before it acts?
- Write-side enforcement (L2-L7) - can the agent be trusted to produce good output? Only means something once you can trust that what you're reading is real and current.
- Meta (L8 feedback) - does the system keep itself honest over time? Depends on a working enforced system to improve, so it stays last.
The three write-side tendencies the layers guard against
The write-side scores aren't abstract good practice - each traces to a known tendency of an AI contributor, observed across models. All three are the same defect: a self-description (the file's shape, a comment's promise, a gate's verdict) under no pressure to stay true. The deterministic core turns each into a cross-layer finding so the report names the specific files, not just the category:
- Accretion - an agent does what is asked, and what is asked is feature after feature; nothing in that loop asks for a refactor, so files only grow. Now fully instrumented via the
accretion_ratchetfinding: a file whose accumulated line count ratcheted monotonically upward across multiple commits with almost no deletion pressure (deletions below ~15% of total churn). Only top complexity/size-band files are flagged, so growth-but-simple is never noise. It surfaces on three surfaces - theaccretion_ratchetblock inrun-context.json, theaccretion_ratchetcross-layer finding (with its files in the attention list), and a growth-profile line on each flagged hotspot page (hotspots/*.md). The signal disclaims itself (rather than dropping the result) when the git history is degenerate - a shallow clone or squashed import has no meaningful net-delta sequence, so the block carriesreliable: falseand the hotspot line is marked as possibly incomplete. - Unactioned intent - an agent records promises it never returns to keep (
TODO/FIXME/ "remove after migration"). Instrumented via theunactioned_intentfinding: markers aged by the edits they survived without being kept - a lying map of intent. - Guardrail erosion - under pressure to make red go green, an agent loosens the check instead of fixing the root (a suppression, a skipped test, a widened threshold), hollowing out the layers meant to protect it while they still read as Present.
Repository archetype (not every repo is software)
The 0-8 model assumes a software repo. A knowledge / document base - markdown sources, an LLM-maintained wiki, a CLAUDE.md schema, and no application code or runtime - has no code surface for the write-side layers (L2-L7). Scoring them Missing is itself a lying score: a well-run KB reads ~2.5/8 ("Not Ready") when it is in fact well-run, penalised for not testing code it doesn't contain.
The deterministic core (lib/archetype.py) classifies the repo and writes an archetype block to run-context.json:
- Detection is a heuristic - the code-file ratio (code vs markdown) and the absence of a runtime surface (
package.json,pyproject.toml,go.mod,Dockerfile, ...). A documentation-heavy application (lots of markdown but a real build) stays software because of the runtime-surface gate. - Override marker. An
assess-archetype: knowledge-base(orsoftware) marker in any instruction file (CLAUDE.md/AGENTS.md/...) forces or suppresses detection, so a maintainer is never trapped by a misfire. Write it as an HTML comment, e.g.<!-- assess-archetype: knowledge-base -->. - Scoring. For a detected knowledge base the write-side layers (2-7) are scored N/A (not Missing) and excluded from the denominator; the headline renormalises over the applicable layers (L0, L1, L8 → denominator 3) and the maturity label names the archetype and the applicable-layer count (e.g.
Knowledge Base · Solid (3 applicable layers)). A software repo is unaffected - all 0-8 layers, denominator 8. - KB-maintenance signal.
archetype.kb_maintenanceflags whether the repo documents how the AI maintains the KB - the Karpathy LLM-wiki pattern (immutable raw sources, the schema file as the product, an ingest workflow, query-as-filing, periodic lint/consolidation). It is both a detection signal and a scored read-side (Layer 0) quality signal; the gist is cited in the report as the best-practice pointer whether or not the workflow is documented.
This is intentionally one archetype (knowledge base), structured as an extensible dispatch so more are cheap to add later - not a general archetype framework (YAGNI). The assess-layer-scorer agent reads the block (its Step 0) and the assess-findings skill renders N/A layers and the renormalised headline.
$ARGUMENTS
Step 1: Determine Repo Root and Output Directory
git rev-parse --show-toplevel # from the arg path if given, else pwd
Set $REPO_ROOT to the result. All scanning happens from here.
Scoping a subtree (/assess <path>). When the argument is a directory under the repo root, scope the whole run to it - metrics, score, badge, wiki, and gate all computed for and labelled with the scope, artifacts under .assess/<scope-slug>/, no signal from a sibling. Pass --scope "$SCOPE" to complexity-treemap.py and assess_core.py and swap .assess/ for .assess/<slug>/ throughout. Full recipe: references/monorepo-scoping.md (relative to this skill dir). A no-path run is whole-repo, unchanged.
Decide the output directory (default: $REPO_ROOT/.assess/). Create it if needed:
mkdir -p "$REPO_ROOT/.assess"
Artefacts will land at:
$REPO_ROOT/.assess/complexity-heatmap.svg$REPO_ROOT/.assess/complexity-stats.json$REPO_ROOT/.assess/doc-graph.svg$REPO_ROOT/.assess/assess-report.md
Write-protected repo root?
/assesswrites.assess/into$REPO_ROOT, and the treemap/core run asuvsubprocesses that write there too. If your workflow keeps the repo root pristine and read-only (e.g. a<repo>-mainclone that teammates branch from, with a hook blocking direct edits), a guard on your writes won't stop the subprocess - it just makes the run write into the directory you meant to protect. Create a worktree first and run/assessfrom there.
Step 2: Generate the Code Heatmap + Doc Graph
This step produces two views of the codebase, both colour-blind-safe (OrRd ramp, no red-green):
- Complexity heatmap (
complexity-heatmap.svg) - a treemap of the code. Size = LOC, colour = cyclomatic complexity, saturation = recent churn. Vivid red = complex AND active = "hard to change safely". - Doc navigability graph (
doc-graph.svg) - a node-graph of the docs. Structure shows connectivity (centre = entry point, rings = link-distance, rim = unreachable; orphans carry a dashed ring); colour shows staleness in the same grammar as the code heatmap (vivid red = a frozen doc beside churning code = a lying map); size = file length. It folds both Layer 0 doc signals - navigability and the decaying-map - into one artifact. Beyond static wikilinks and CommonMark links, it recognises Obsidian vault-native navigation -.baseview hubs anddataviewquery blocks - as edges (resolved statically by folder / tag / frontmatter predicate), so a vault navigated by dynamic queries isn't mis-scored as orphaned. The SVG and the scored signal compute over the identical doc set: both honour the same excludes (.assess/config.toml).
Feed the complexity stats into the linter/complexity layer (Layer 3) and the doc_graph / doc_staleness blocks of run-context.json into Layer 0 (the graph SVG is the visual; the score reads the structured blocks).
The consent lifecycle (read references/consent-lifecycle.md)
Steps 2a/2b/2d and the assess-pr end-of-run offers share one consent model, specified in full in references/consent-lifecycle.md (relative to this skill dir) - load it before running the offers. Load-bearing hooks the steps rely on: decline markers carry provenance (write .no-<tool> JSON via the reference's write_decline_marker helper, never a bare touch; a mutation decline under an older plugin major sets reoffer_mutation: true so Step 2d re-asks once); three phases each a single batched question (Phase 1 tool installs 2a+2b, Phase 3 the separate mutation pass 2d, Phase 2 the assess-pr write-back offers); and the non-interactive contract - a headless/CI run makes no AskUserQuestion calls in any phase, Phase 1 as orchestration from your runtime context and Phases 2/3 from the core's run-context.json .interactive flag, which pre-records every skipped offer in .offers.
2a: Detect scc need (feeds Phase 1)
The bundled treemap uses lizard (Python, Go, JS, Java, C/C++, etc.) by default. Optional scc extends coverage to 200+ languages including markdown, JSON, YAML, SQL, and shell - useful when the repo's surface is more than just traditional source code.
Before scanning, check three signals:
# 1. Is scc already on PATH?
command -v scc >/dev/null 2>&1 && SCC_PRESENT=1 || SCC_PRESENT=0
# 2. Has the user previously declined for this repo?
[ -f "$REPO_ROOT/.assess/.no-scc" ] && SCC_DECLINED=1 || SCC_DECLINED=0
# 3. Is the repo mostly markdown/data/config (where lizard alone will be sparse)?
# Cheap heuristic: count non-code files vs code files. The `.` argument is
# the regex pattern (matches every path) and "$REPO_ROOT" is the search
# path - without `.`, fd treats $REPO_ROOT as the pattern itself, matches
# nothing, and silently returns 0.
CODE_FILES=$(fd -t f -e py -e js -e ts -e tsx -e jsx -e go -e java -e kt -e rs -e rb -e cs -e swift -e dart -e cpp -e c -e h -e php . "$REPO_ROOT" 2>/dev/null | wc -l | tr -d ' ')
NONCODE_FILES=$(fd -t f -e md -e json -e yaml -e yml -e toml -e sh -e sql . "$REPO_ROOT" 2>/dev/null | wc -l | tr -d ' ')
Add scc to the Phase 1 offer list only if all three are true: SCC_PRESENT=0, SCC_DECLINED=0, and the repo looks lizard-sparse (CODE_FILES < NONCODE_FILES or CODE_FILES < 10). Otherwise it contributes nothing to Phase 1. Do not ask here - scc is batched with the dead-code tools into the single Phase 1 question in Step 2b. Its trade-off phrasing, for when the batched question is presented:
"This repo has code files and non-code files (markdown/JSON/YAML).
sccwould include the non-code files in the treemap; without it the treemap may be sparse. Installscc?"
The three options are the shared Phase 1 shape (Install / Skip for now / Skip permanently via write_decline_marker scc). If the user accepts, run the platform-appropriate command (do not auto-install - brew install is a system mutation):
# macOS (Homebrew)
[ "$(uname)" = "Darwin" ] && command -v brew >/dev/null && brew install scc
# Linux (try common package managers, fall back to go install or manual)
[ "$(uname)" = "Linux" ] && {
command -v apt >/dev/null && sudo apt install -y scc \
|| command -v dnf >/dev/null && sudo dnf install -y scc \
|| command -v go >/dev/null && go install github.com/boyter/scc/v3@latest \
|| echo "Install scc manually: https://github.com/boyter/scc#installation"
}
If the install fails or the platform isn't covered, fall back to lizard-only and continue - don't block the assessment.
2b: Phase 1 - batched analysis-tool install offer (capability-driven, detect-or-propose)
/assess maps each Layer 1/Layer 3 analysis capability (liveness/dead-code, static module graph, linting, modernization) to a serving tool. Historically that map was a hardcoded per-language allowlist - vulture for Python, ts-prune/knip for TS/JS, staticcheck/deadcode for Go. The defect that allowlist created: when a repo's language isn't enumerated, every capability silently degraded to "unavailable" - the report read "this layer is absent here" rather than "a tool could serve this - install one?". A non-enumerated language was locked out with no resolution path inside the run.
The flow is now capability-driven detect-or-propose, in three moves per capability:
- Detect whether a serving tool already exists (on PATH, or configured in build/lint config). If it does, use it - and if it's configured in the build, credit it; never re-offer.
- Propose an ecosystem-appropriate candidate when none exists. For an enumerated language this is the table below; for a non-enumerated one you propose a fitting tool at runtime (reasoned latitude - you are not locked out because the language isn't in a hardcoded list). Ask the user with the same AskUserQuestion pattern.
- Honest-degrade anything you can detect-but-not-serve: name the capability and a candidate tool in the report. This is a deliverable state distinct from both "Present" and a silent "Missing" - never let a capability vanish without naming what would serve it.
The per-language dead-code offer below is the simplest instance (one capability, install-consent). When the tool is absent, the scan degrades to tool_absent and the user has no resolution path inside the skill - they'd have to know which tool fits the language, which package manager to use, and run the install themselves. The same install-offer pattern as Step 2a closes the loop without leaving them to figure it out.
Detect languages with cheap fd counts (mirroring Step 2a's heuristic - the treemap script's own classification isn't exposed in the stats sidecar, and shelling out is fine here):
PY_FILES=$(fd -t f -e py . "$REPO_ROOT" 2>/dev/null | wc -l | tr -d ' ')
TS_FILES=$(fd -t f -e ts -e tsx . "$REPO_ROOT" 2>/dev/null | wc -l | tr -d ' ')
GO_FILES=$(fd -t f -e go . "$REPO_ROOT" 2>/dev/null | wc -l | tr -d ' ')
# Per-language candidate tool. Prefer the read-only tool first - `ts-prune` over
# `knip` for TS, `staticcheck` over `deadcode` for Go - so the user isn't asked
# twice for the same job and the chosen tool doesn't need to build the project.
needs_offer() {
# $1 = tool; $2 = file count for the language; returns 0 if we should ask.
local tool="$1" count="$2" min="${3:-5}"
[ "$count" -ge "$min" ] || return 1
command -v "$tool" >/dev/null 2>&1 && return 1 # already installed
[ -f "$REPO_ROOT/.assess/.no-$tool" ] && return 1 # user declined permanently
return 0
}
OFFERS=() # each entry: "language|tool|install_cmd"
# Seed with scc first when Step 2a flagged it (the treemap-coverage tool shares
# this one batched question with the per-language dead-code tools).
[ "${SCC_PRESENT:-1}" = 0 ] && [ "${SCC_DECLINED:-0}" = 0 ] \
&& { [ "${CODE_FILES:-0}" -lt "${NONCODE_FILES:-0}" ] || [ "${CODE_FILES:-0}" -lt 10 ]; } \
&& OFFERS+=("coverage|scc|brew install scc (or apt/dnf/go install - see Step 2a)")
needs_offer vulture "$PY_FILES" && OFFERS+=("python|vulture|pip install vulture (or 'uv tool install vulture')")
needs_offer ts-prune "$TS_FILES" && OFFERS+=("typescript|ts-prune|npm install -g ts-prune")
needs_offer staticcheck "$GO_FILES" && OFFERS+=("go|staticcheck|go install honnef.co/go/tools/cmd/staticcheck@latest (or 'brew install staticcheck')")
If OFFERS is empty (no language hits the threshold, or every tool is already installed/declined), skip straight to 2c. Non-interactive short-circuit: Phase 1 precedes the core, so in a headless/CI run make no AskUserQuestion call and install nothing - proceed to 2c with lizard-only plus whatever is already on PATH (the core records the skip in offers at 2c).
Otherwise, in an interactive run, batch all of Phase 1 into a single AskUserQuestion call - one question per entry in OFFERS (scc and each dead-code tool together), three options per question. This is the one tool-install decision surface; the user never faces scc and the linters as separate modals:
- Install - run the cited install command and continue.
- Skip for now - proceed without the tool. Don't write a marker; ask again next run.
- Skip permanently for this repo -
write_decline_marker <tool>so future runs don't ask. Recommended when the language only appears in scripts/configs that don't warrant symbol-level reachability.
Phrase each question so the gain is concrete, e.g.:
"This repo has 47 Go files.
staticcheck -checks U1000would let/assessflag unreachable Go funcs as Layer 1 candidates. Install? (go install honnef.co/go/tools/cmd/staticcheck@latestorbrew install staticcheck)"
When the user picks Install , run the platform-appropriate command from the offer. Surface any install failure as a chat message and continue - dead-code tools are degrade-don't-block (same contract as scc); a missing tool reduces Layer 1's precision but never gates the assessment. When they pick Skip permanently, write_decline_marker <tool> (e.g. write_decline_marker staticcheck). The user answers this one batched question once and the run proceeds with whichever tools they accepted.
JVM / Maven capability offers (v1)
When the deterministic core detects a Maven or Gradle project it emits a capability_offers block in run-context.json - the first proof of the capability-driven flow on a non-enumerated ecosystem. Read it after Step 2c's core run, before scoring, and act on each capability's state:
jq '.capability_offers' "$REPO_ROOT/.assess/run-context.json"
liveness→state: "offer"- Maven was detected butmvn dependency:analyze(coarse module-level dead-dependency detection) has not run. Theconsentfield names the shape:run(mvnis on PATH - offer to run it against the project;dependency:analyzeneeds a compiling build, so this is a run-consent, heavier than a static scan) orinstall(mvnabsent - offer to install Maven first). Use AskUserQuestion exactly as Step 2b, phrasing the trade-off (a build that resolves dependencies and may hit the network) - but honour the non-interactive contract: whenrun-context.json .interactiveisfalse, skip this offer and honest-degrade the capability instead of prompting. On accept and arunconsent, runmvn dependency:analyze, capture its output, and re-run the core with the served result so the candidates feed Layer 1. On decline, the capability stays honestly named, not silently dropped.linting/modernization→state: "credited"- an already-configured pom.xml plugin serves it (served_bylists which: Checkstyle, SpotBugs, PMD, error-prone, OpenRewrite, Modernizer). Credit it in the report; do not re-offer.- Any capability →
state: "honest_degrade"- nothing serves it yet (module graph, linting/modernization without a configured plugin, and all capabilities under Gradle in v1). The block carries acandidate_toolandgloss. Name both in the report's Layer 1/Layer 3 prose ("module-graph analysis is unserved here;jdepswould provide it"). Honest-degrade is a deliverable - surfacing the candidate is the point.
Boundary (v1). Only Maven liveness is served. Module graph (jdeps), linting, and modernization honest-degrade; Gradle honest-degrades entirely. The candidate_tool values are deterministic defaults - you may propose a better-fitting ecosystem tool at runtime (the detect-or-propose latitude above); that choice is human-judged, not CI-tested. CI tests only signal consumption: given a tool's output, the scorecard feeds correctly.
2c: Run the treemap
Run the bundled treemap script alongside the deterministic core - see the chained block below.
The script prints a one-line summary (file count, lizard vs scc coverage, churn window chosen, top 5 biggest files). The stats sidecar contains percentiles (p50/p95/max for LOC, CCN, churn) and ranked lists of the top 10 files by hotspot score, raw CCN, and raw LOC. Both feed the report.
Dependencies: the script uses PEP 723 inline metadata (lizard, squarify, matplotlib, numpy). uv resolves them on first run.
Build artifacts and generated code are filtered by default. The script excludes two classes of files:
- Build artifacts:
main.dart.js, Flutter canvaskit/skwasm runtime bundles (canvaskit.js,skwasm*.js),*.min.js,*.bundle.js,*.chunk.js,*.map, sourcemaps, service workers, and files undernode_modules/,dist/,build/,.next/,.nuxt/,.output/,coverage/, etc. - Generated code: protobuf bindings (
*.pb.go,*_grpc.pb.go,*.pb.gw.go,*.connect.go,*_pb.ts,*_pb.d.ts,*_pb2.py,*.pb.cc,*.pb.h), Go generators (*.gen.go,wire_gen.go,zz_generated_*.go,bindata.go), .NET source generators (*.designer.cs,*.g.cs), Dart/Flutter codegen (*.freezed.dart,*.g.dart,*.gr.dart).
Full list in complexity-treemap.py's EXCLUDE_DIRS and EXCLUDE_FILE_PATTERNS. If you specifically want to score these (e.g., to visualise how much of the repo is generated), pass --include-artifacts.
Dominance warning. If a single file still holds >30% of total scoreable LOC after filtering (the threshold compiled bundles typically cross), the script prints a warning to stderr identifying the file. When you see this, the right next step depends on why the file is large:
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 30
- Forks
- 5
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
assess- Source
- github.com/bjcoombs/ai-native-toolkit