atlas
Skill1 starsChanged 9 days agoMITView on GitHub
Map a codebase into a beautiful, interactive, local-only architecture atlas — entry points, services, data flow, integrations, and any AI layer — rendered as a single self-contained HTML file with drill-down and flow tracing. No upload, no account, no network: nothing leaves the machine. Use whenever the user asks to "map this repo", "atlas this codebase", "scan the codebase", "visualize the architecture", "diagram this system", "show me how this project fits together", "draw the agent/model/tool graph", or wants a shareable-but-private architecture diagram.
14 other projects use a version of this.
Adopted by
Most recently taken up by
- CorvidLabs/skillstheir own version, first seen Nov 8, 2026
- anombyte93/prd-taskmastertheir own version, first seen Nov 8, 2026
- Z0CC0/ATLAS-memtheir own version, first seen Nov 8, 2026
- sophonfinance-wq/finance-automation-portfoliotheir own version, first seen Nov 1, 2026
- simota/agent-skillstheir own version, first seen Oct 23, 2026
- nicodiansk/turbochargetheir own version, first seen Oct 13, 2026
Used with
Projects with it also use openai-docs, ship and 1 more.
Other files people use alongside this one.
Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
OpenAISkillopenai-docs
Use when the user asks how to build with OpenAI products or APIs, asks about Codex itself or choosing Codex surfaces, needs up-to-date official documentation with citations, help choosing the latest model for a use case, or model upgrade and prompt-upgrade guidance; use OpenAI docs MCP tools for non-Codex docs questions, use the Codex manual helper first for broad Codex self-knowledge, and restrict fallback browsing to official OpenAI domains.
Also in 3 projects that use this
AWSSkillship
Finalize dev work on an issue branch — update docs, push, and open a PR.
Also in 3 projects that use this
VercelSkillsetup
Set up Vercel CLI and project configuration. Use when the user says "set up Vercel", "configure Vercel", "link to Vercel", or "vercel init".
Also in 3 projects that use this
Other versions14 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 14
- ariga/atlas★ 8.8ksame name
- anombyte93/prd-taskmaster★ 608same name
- simota/agent-skills★ 96same name
- sophonfinance-wq/finance-automation-portfolio★ 16same name
- jscraik/Agent-Skills★ 9same name
- nicodiansk/turbocharge★ 3same name
- nicodiansk/turboflow★ 3same name
- ParkerRex/Skills★ 2same name
- Rynaro/magicite★ 2same name
- Abbasi-Alain/atlas★ 1same name
4 more not listed
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository7 other files it gives its agents.
- babysit-prSkill
- client-feedbackSkill
- demo-pageSkill
- design-promptSkill
- loom-watchSkill
- migration-safetySkill
- walk-me-throughSkill
The file
We split this skill into the rules it gives an agent
6 rules. 0 are common, 2 are distinctive, and 4 are very specific to this project.
0
Common
Most of the 11 edited versions we read keep these.
2
Distinctive
This author's own choices. Worth a look.
4
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 13
Write no HTML or CSS. Nothing is uploaded: the output is a single self-contained HTML file on disk.
- Very specific: line 36
Use targeted edits, not full rewrites, and re-run until it is clean.
- Very specific: line 91
Never read the viewer sources (templates/*) or the rendered atlas.html: all are fixed, and the CLI prints everything you need.
- Distinctive: line 196
When the tests disagree — auth middleware and rate limiters are hub-shaped but can *end* any flow with a 401 or 429 — the flow trace wins: …
- Very specific: line 262
sourceRefrules: Use a path you actually saw — don't infer one from a naming pattern (.ts when the file is .tsx, an index route that … - Very specific: line 276
Before writing the map, assemble .atlas/inventory.md from .atlas/survey.txt, which the survey already wrote.
“Common” is measured: kept by at least 6 in 10 of the 11 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Atlas (local codebase map)
Analyze a repository and produce a local, interactive atlas — entry points, services, data flow, integrations, plus the AI layer when the repo has one. You produce only the data (a small JSON object); a fixed renderer draws the map. Write no HTML or CSS. Nothing is uploaded: the output is a single self-contained HTML file on disk.
Steps
- Resolve the target repo: an explicit path or repo name in the request
(confirm it exists), a git URL (shallow-clone it,
--depth 1, to a temp directory), or the current working directory. All paths below are relative to the target repo, not to this skill. - Measure and investigate — see "Investigating" below.
- Write the coverage inventory to
.atlas/inventory.md— one line per item, no prose. A working file, not a deliverable. - Write the map to
.atlas/atlas.jsonin the contract shape below, reconciling it against every inventory line. Leave the repo's.gitignorealone — whether the map gets committed is the user's call; note in your summary that.atlas/is untracked so they can decide deliberately. Write anytourslast, once the edges are settled — a tour is prose over the graph, so the graph has to be right first. - Check it, and fix what comes back. Instant, writes nothing:
Use targeted edits, not full rewrites, and re-run until it is clean.python3 /abs/path/to/skill/scripts/render.py /abs/repo/.atlas/atlas.json \ --repo /abs/repo --check --edges --inventory/abs/path/to/skill/evals/legibility/stats.pyadditionally measures the opening view (hub fan-out, drawn-edge density, label ratio after containers collapse) — worth running on anything large. - Verify the sentences the checks can't — see "Details" below. A green
--checksays the map is well-formed, not that it is true: in one audited map ten of twelve load-bearingdetailclaims were wrong with every check passing, one asserting the opposite of the code. Take the counted-claims worklist--checkprints, plus any sentence asserting "only", "no X", or a security posture, and refute each against the code. This is the step that decides whether the map is trustworthy. - Render once, at the end:
Droppython3 /abs/path/to/skill/scripts/render.py /abs/repo/.atlas/atlas.json \ --repo /abs/repo --open--openwhen there's no browser (headless, CI, a sandboxed run); the file is written either way. - Report the file paths and a few summary lines — counts, main flows,
anything surprising. Don't echo the JSON back.
atlas.htmlis renderer output, always safe to delete and regenerate;atlas.jsonis yours.
Re-running does not overwrite atlas.json for you. Default to a clean scan:
rm .atlas/atlas.json .atlas/atlas.html first. Take the incremental path — read
the existing map and check git log/git diff since project.date rather than
re-scanning — only when that date is recent, you wrote the map yourself, and
its project.rules matches the version: in this skill's frontmatter.
That last condition is the one that bites, because nothing about a stale map
looks stale. The rules change what a kind means without moving a field, so an
older map stays structurally valid while its kinds are now wrong — and the
incremental path re-reads those kinds instead of re-deriving them, propagating
them with every check green. When --check reports a project.rules mismatch,
read CHANGELOG.md, and re-scan whatever the intervening entries touched rather
than diffing on top of it.
On the incremental path, three things repay the minute they cost:
- Run
--checkbefore you edit anything. It turns the diff into a worklist: a deleted file shows up as a deadsourceRef, and any warning it reports that the diff didn't cause is one the previous run left you. - A commit message is a doc, and the code-wins rule applies to it too — with more force here, because on this path the log is your primary input rather than a footnote. One tested commit announced a subsystem's removal and deleted none of it; the service, its scheduled task, its migrations and ~17 call sites were all still live. Confirm each claim in the diff before believing it.
- Grep for the name of anything deleted. A removed package leaves references no check can see — a CI job still requiring it, a test config still pointing at the missing directory, docs still telling users to import it. That debris is a finding worth reporting even though the map itself is now correct.
evidence refs are path:line pins, so an insertion above the call silently
moves the line out from under them and --check still passes. One more reason
to attest only the few edges that truly need it.
Never read the viewer sources (templates/*) or the rendered atlas.html: all
are fixed, and the CLI prints everything you need.
Output contract — write EXACTLY this shape to .atlas/atlas.json
{
"version": 2,
"project": {
"name": "string (<=48)",
"slug": "lowercase-dashed (<=48)",
"tagline": "one line (<=80, optional)",
"iconDomain": "favicon domain for the project, e.g. acme.com (optional)",
"date": "YYYY-MM-DD",
"rules": 3
},
"graph": {
"nodes": [
{ "id": "chat", "label": "Dashboard chat", "kind": "entry", "sub": "/api/chat",
"sourceRef": "src/app/api/chat/route.ts:18",
"detail": "Streams support answers to the dashboard; the only authenticated chat entry point" },
{ "id": "agent", "label": "Support agent", "kind": "agent", "sub": "streamText",
"sourceRef": "src/agents/support.ts:42",
"detail": "Answers tickets with order lookups against Postgres, streaming replies back to chat" },
{ "id": "gpt4o", "label": "GPT-4o", "kind": "model", "domain": "openai.com" },
{ "id": "billing", "label": "Billing service", "kind": "service",
"sourceRef": "src/services/billing.ts",
"detail": "Owns plan changes and invoice generation for every paid workspace" },
{ "id": "billing-plans", "label": "Plans & quotas", "kind": "service",
"parent": "billing", "sourceRef": "src/services/billing/plans.ts",
"detail": "Tracks each workspace's plan tier and enforces its seat and usage quotas" },
{ "id": "pg", "label": "Postgres", "kind": "store", "domain": "postgresql.org",
"sourceRef": "src/db/schema.sql",
"detail": "Primary datastore for billing records, plans and support ticket history" }
],
"edges": [
{ "from": "chat", "to": "agent", "kind": "triggers" },
{ "from": "agent", "to": "gpt4o", "kind": "calls" },
{ "from": "billing", "to": "pg", "kind": "writes", "label": "charges on trial end" },
{ "from": "agent", "to": "pg", "kind": "writes",
"ghost": true, "claimedBy": "README.md:31" }
]
},
"tours": [
{ "title": "The life of a chat request",
"steps": [
{ "node": "chat", "text": "A question arrives at the dashboard chat endpoint." },
{ "node": "agent", "text": "The support agent answers it, pulling order lookups as it goes." },
{ "node": "gpt4o", "text": "The reply itself is drafted by GPT-4o." }
] }
]
}The viewer derives the whole header from the graph — no summary fields to keep
in sync. Legacy stats/topModels/topTools/topIntegrations are ignored;
don't write them. project.date = today. project.rules = the version: in
this skill's frontmatter — it records which ruleset the map was authored
against, and --check prints the number to use.
| field | rule |
kind | one of entry (route/page/CLI/webhook), cron (a job: work triggered from outside any request — a schedule or a queue, so a celery task, a job runner handler and a pub/sub consumer are cron alongside a schedule: block, with what triggers it in sub), service (a subsystem that performs the product's own work at runtime — not a catch-all for "internal", see Kind discipline), agent (an LLM call the product makes), model (the model behind it — an actual model id), tool (a function a model can call: a tool({...}) definition, an MCP server tool), store (DB/cache/index/config/disk), external (3rd-party API). Kinds drive the layout lanes (Entry points → Services & agents → Models & tools → Data & external), so choose accurately. A single-shot LLM call with no loop and no tools is still an agent, so its model edge reads correctly — say what it really is in sub ("single-shot classify"). |
parent | optional, max depth 2: children render inside their container, collapsed with a +N badge and expanding in place. A container with a single child usually wants to be one node. When the real hierarchy runs deeper — app → feature → route in a monorepo — keep the two levels a teammate names out loud (usually feature as container, routes as children) and compress the rest: the outer level becomes a group, the inner detail a child's sub. Don't invent a middle tier just to have one. |
group | optional, <=24: nodes sharing a group render as one labeled stack. Group by feature/domain the way a team talks ("Billing"), never by file layout; hub nodes stay ungrouped. One carve-out: when a backend has two parallel service layers — an execution engine and the request-path CRUD that authors and drives it, which both pass the runtime test and so are both service — the layer is what a teammate names out loud ("core" vs "services"), and group is the only thing that can tell them apart. Use it there, and nowhere else that a directory name is the answer. The viewer puts a group in its members' median lane, so one spanning kinds pulls nodes out of their semantic column — a real cost, worth paying when the team genuinely names the thing as one unit. Entry points are the exception: a group never moves an entry or cron out of the Entry points lane. A feature group is usually one command plus the services it drives, and letting the median drag the command rightwards put the reader's starting point in among the things it starts. Such a node keeps its group (it stays searchable by group name and the "Ask agent…" prompt still reports it) but sits in lane 0, and the group's box forms around the rest — so group a command with its services freely. No groups at all beats a forced one. |
edge kind | optional, prefer setting it: calls/reads/writes/triggers/enqueues, revealed on flow trace. Two nodes can have more than one edge — a service that both reads and writes a store gets both, not a compromise label. Use enqueues for a hand-off that returns before the work happens (a task queue, a job runner, a pub/sub publish): it is where the flow stops being synchronous, which is the first thing a reader needs when the far end never ran. |
edge label | optional, <=24, always visible — spend it on the few relationships that would surprise a reader ("charges on trial end"). See the label budget below. |
edge evidence | optional path:line — the call site you read that proves this edge. Records a verification the text heuristic can't repeat (DI, barrel re-exports, generated registries), so --edges stops re-flagging it. Use it for the few edges that need it, not as a default: attesting everything switches the check off, and --edges warns past 80% attested, once there are 20 or more edges. |
tours | optional top-level array (cap 6), a sibling of graph not a field inside it: {title (<=60), steps: [{node, text (<=280)}]} — authored, narrated walkthroughs the viewer plays as stories. Each step names a node id, and consecutive stops must be reachable along real edges (from any earlier stop, so forks are fine). Genuinely optional: a map without tours is complete, not deficient. See Tours below. |
edge ghost + claimedBy | ghost: true marks a doc-claimed edge the code does not implement; claimedBy (required with it) is a path:line ref to the doc making the claim, and is verified against the repo. Renders dashed and translucent, captioned "doc-claimed · <ref>"; excluded from flow tracing, blast radius, particles and --edges, and annotated in the "Ask agent…" prompt. Never both ghost and evidence — finding the performing line makes it a real edge. See Ghost edges below. |
domain | optional favicon domain, no scheme (openai.com) — only for things a recognizable company owns; use the product domain for models (claude.ai). |
detail | <=200: one sentence shown on click. Treat it as required for internal nodes, like sourceRef — it is what a reader who has never seen the repo actually reads. In a controlled pair of maps of one repo, the one with detail on 92% of nodes answered 16/16 comprehension questions and the one with 19% answered 10/16, and every missed answer was a missing sentence rather than a missing box. --check warns below 80%. |
sourceRef | repo path plus :line (<=120). Treat it as required for internal nodes — it feeds jump-to-code and the viewer's "Ask agent…" prompt, and a node without one produces a vague prompt. Rules below. |
| caps | top-level nodes <= 40, children per container <= 20, nodes <= 300, edges <= 500. Labels <= 28 chars, sub <= 40. Ids unique; every edge endpoint an existing id. |
Kind discipline. service is the only kind with no natural boundary, so it
quietly becomes the bucket everything internal lands in — in the two largest
maps this skill has produced it took 69% and 70% of all nodes, absorbing UI
trees, bundler scripts, type definitions and the third-party editors an
installer writes into. It is not "internal code"; the test is runtime work on
the product's own data — something calls it, and it moves or transforms
something. When nothing else seems to fit, one of these does:
- A UI surface — screen, panel, component tree, client-side state — belongs
to the entry point that mounts it, as a child of that
entry, and the child carries kindentrytoo: the header pills count children (every kind butstore, where a nested node is part of the same engine), so a UI tree filed asserviceinflates the service count into a lie about the product. The pill tooltip carries the top-level count beside the total, which is the fastest way to see whether a kind is being used as a bucket. A kit shared by several entries is one node, not a container of its components. - Build, bundling, release and CI hang off whatever triggers them: an
entryfor push/dispatch, acronfor aschedule:block. - A third-party product you read or write is
external, even though the code doing it is yours — an installer that writes VS Code'ssettings.jsonis the service; VS Code is not. - A shared library passes the runtime test — a retrying HTTP client is
called and transforms things — so the line between it and a service is drawn
by the flow trace, not the call stack: a service is a stop in some flow's
story; a library is how a stop does its work. If every edge it would get is
the same
callsfrom half the map and none would earn a label, it's a library: at most the single hub node below, otherwise a line in its callers'sub/detail. The swap test settles arguments — replace its implementation and no flow redraws → no node. Judge against this product's story: secret masking is a helper in a web app and a product promise in a security CLI. When the tests disagree — auth middleware and rate limiters are hub-shaped but can end any flow with a 401 or 429 — the flow trace wins: a gatekeeper is a stop, and the hub rule prunes its edges, not its node. - Types, constants and vocabulary perform nothing and are never nodes; fold them into the node that uses them.
- Boot wiring — an
init_appthat calls someone'sinit()behind an env check, an instrumentation hook, a middleware registration — moves none of the product's own data and is never a node. Map the thing it wires up (theexternalit ships to) and drop the wiring. A 40-line file whose whole body isif DSN: sdk.init(...)is the giveaway.
What survives is what a teammate names when asked what the product does: an eligibility engine, an export pipeline, a scan pipeline.
Stages are not services. A step that only ever runs as part of one parent
flow — extract → split → embed → load, or generator → runner → task pipeline —
is a stage, not an independently callable subsystem. Stages are fine as
children, but say what they are in sub ("step 2 of 5", "indexing only") so the
reader does not read a phase of one pipeline as a service another caller could
reach. If nothing outside the parent ever calls it, it is a stage.
Granularity. A kind says what a node is; it doesn't say how finely to slice, and left unstated the same repo maps three different ways. Two rules settle the cases that actually diverged — three runs over one monorepo produced 11, 2 and 2 stores and 23, 4 and 5 crons, all of them defensible:
- One
storeper datastore engine, not per table. Postgres is one node; tables, collections and indexes arechildrenof it when they earn their own line. Two Postgres databases with separate connection strings are two engines and two nodes; two schemas in one database are not. Nesting them is safe: the header counts a store inside a store as part of that engine, not as another place data lives, so seven table groups under one database still read as one store. Every other kind nests real instances and is counted in full. - One
cronper job — the thing that runs — with the trigger insub("Vercel · every 5m", "celery · queue: dataset"). Not one per cadence, and not one node for all of them. Jobs that share a trigger and a handler merge with a count ("6 reminder crons"); jobs that merely run at the same time do not. The two halves of one mechanism take the same kind: a poller that finds due work and the handler that executes it are bothcron, and splitting them across kinds — pollercron, executorservice— hides that they are one thing. If anenqueuesedge points at it, it is a job. - One node per client, not per endpoint. N classes that differ only in which
path they call on the same target are one node with the count in
sub, the same way 20 vector backends are onestore. Five typed clients over one daemon read as five subsystems and are one channel; the swap test catches it — replace any one and the flow still runs from your code to that daemon.
The same instinct generalises: slice by what the system has one of, not by what
happens to be convenient — and when merging, say so in sub so the reader knows
a count was collapsed rather than a subsystem dropped.
Sizing. The top level is the picture a staff engineer draws on a whiteboard:
as many nodes as the system genuinely has things worth naming out loud, up to
the ceiling of 40. That is the only number — don't aim at a count, and don't pad
a small repo or flatten a large one to look thorough. Completeness lives in
children, which enumerate a container's full contents; children only have to
exist in the repo, while every top-level node has to earn its place.
Label budget. Labels never fade, so they compete with each other and with
the edges underneath. Count the budget at the opening view, not over the raw
edge list: collapsed containers merge every child edge into one drawn edge per
top-level pair, so labels concentrate — a map that passes one-in-four raw has
opened at more than half its visible edges labeled. Collapse the map in your
head to top-level pairs and count again, or run /abs/path/to/skill/evals/legibility/stats.py.
Let kind carry the ordinary relationships.
sourceRef rules. Use a path you actually saw — don't infer one from a
naming pattern (.ts when the file is .tsx, an index route that doesn't
exist). Point at the definition the node names — the type, the func, the route —
not the doc comment above it, not a bare const (, and never the same line as
the node's parent. A container whose subject is a directory points at the
directory itself (src/stores, app/api/admin) with no :line; don't borrow
one member's file, and give every member its own child with its own ref.
Refs are repo-relative: an absolute path or one containing .. is rejected
rather than resolved, because an atlas is a file people send each other and a
ref is only ever meant to point inside the repo it describes.
--check verifies all of this against the repo.
Coverage — the map must account for everything
Before writing the map, assemble .atlas/inventory.md from .atlas/survey.txt,
which the survey already wrote. references/coverage-checklist.md covers the
part no sweep reaches — flag-unlocked modes, the machine-facing contract,
dev-time AI — and how to read the largest-files list. Keeping the list in your
head is how a scheduled job or a whole flag-driven mode goes missing without
anything registering that it did.
Reconcile every line: each item appears in the map — as a node, a child, or
named in a node's sub/detail ("+ Google Maps geocoding") — or is omitted in
one sentence. Record the disposition on the line itself, naming the node id in
backticks, so the claim can be checked rather than believed:
- `scan repo [path]` — child `cmd-scan-repo`
- `ARMIS_LOCAL_S3_ENDPOINT` relaxes the SSRF check — detail on `api-ssrf`
- `completion`, `help` — omitted: cobra boilerplate, no architectural consequenceNot — node (route trees) or — mapped: a disposition that doesn't name an id
reads as reconciliation without being it, and it is how a whole subsystem goes
missing from a map whose inventory looked complete. --inventory reads these
back, verifies every id on the line exists, and counts what was never
dispositioned. A line that names several ids is where this bites: merge two
nodes into one and the dispositions still name both, so the ids that no longer
exist read as reconciled. Re-run --inventory after any pass that deletes or
merges nodes, not just after writing the map:
# inventory: 83 items — 73 mapped, 10 omitted, 0 unreconciledOmitted-with-a-reason is a finished state, not a failure. A large repo
should have a healthy omitted count; inflating nodes to drive it to zero is the
worse error, and it is what pushes maps to hug the 40 ceiling. Only
unreconciled must reach 0. Done means: a new engineer clicking through every
container sees nothing missing.
Edges — where a map misleads
A confidently-labelled edge that isn't real is the one error a reader cannot
detect: it looks exactly like the true ones. --check cannot fault one on its
own — an edge is structurally valid as long as both ends are node ids.
--edges looks for the line that performs each claim in the from node's file
and lists the edges where it found nothing. For each flag, either point at the
line or move the edge to the node whose file performs it. A clean run isn't a
proof: it finds edges with no evidence, not edges with the wrong evidence.
A flag is not a verdict. The heuristic reads text, so it cannot see through a DI
container, a barrel re-export or a generated registry — architectures where
every true edge is invisible by design. When you have found the call site
yourself, record it as the edge's evidence:
{ "from": "bookings", "to": "tasker", "kind": "triggers",
"evidence": "packages/features/bookings/lib/handleNewBooking.ts:412" }That ref is itself checked — a path that doesn't exist or a line past the end of
the file is reported, so evidence can't excuse an edge by being vague — and
the edge stops being re-litigated on every run. Flags marked inconclusive
rather than absent mean the container's directory was too big to scan whole;
those need the same treatment, and reading them as "no evidence" is how a
container passes vacuously.
Then verify the rest yourself, as one pass over the finished list — not while
drafting, so a habit of mind can't carry across ten edges. For each edge open
the file the from node's sourceRef names and find the line that performs the
claim: an import of the exact symbol plus a call to it, a query naming the
exact table, a fetch or spawn of the exact path. If the performing line lives in
another file, the edge belongs to that file's node. Set kind from the call you
found rather than from what the two nodes are for — a screen that loads a record
to prefill a form reads; the write is in the hook it hands the form to.
Three quarters of the wrong edges this skill has produced were off by exactly one hop, and every one looked correct from the import block. Four shapes:
- The wrapper credited with the work. A composite action, CLI front-end,
facade or page shell gets credited with what its caller or callee does — the
config load that happens in the command rather than the engine, the query that
happens in the driver script rather than the pure function it calls. If the
fromfile cannot reach the thing at all (no db client, no import), the edge belongs elsewhere. When a node'sdetaildescribes a step, confirm that step is in the file the node'ssourceRefpoints at. - Import ≠ call, package ≠ symbol. A utility package routinely exports both
the thing you assumed and a far smaller thing everyone actually uses (a
retrying client and a bare transport; a path sanitiser and a plain join). Get
it wrong once and you write the same wrong edge five times, because every
importer looks alike from the import block. Read the imported name, not the
path. Two mechanisms recur: a barrel file re-exporting both a trivial helper
and the heavyweight writer you meant, and two files with the same name in
different packages (
features/coupon/actions/general.tsvsfeatures/subscription/...). - Arrows out of a diagram. A funnel described in a docstring, README or
CLAUDE.md that no module implements. When docs and code disagree, map the code
— and keep the doc's claim as a
ghostedge (below) rather than discarding it: the delta between the system's self-image and its reality is often the most valuable thing on the map. This holds inside a sentence too: if a doc is your only source for a mechanism ("streams uploads through io.Pipe"), grep for it before repeating it in adetail— a stale detail inherits the doc's authority and outlives the doc. - Hub nodes. Shared infrastructure (a db connection module, a logger, a
config loader) touches everything; drawing every true edge buries the story
the map exists to tell. Give it one node and only load-bearing edges: past a
dozen edges on one node in the opening view its fan stops being followable,
which is the signal to prune the ones drawn from theme ("everything reads
config"). The direction tells you which hubs to prune: a node with many
edges in and none out is a pure sink — "everything persists", "everything
logs" — and that fan is theme, not story, so keep only the writes that shape
the product. One map had a Postgres node with 17 inbound and 0 outbound;
cutting it to 6 removed a whole tier of visual noise and lost nothing a reader
wanted. A hub with traffic in both directions is usually a real dispatcher and
its fan is the point — leave it, and say so in your summary rather than
pruning a true story to satisfy a threshold.
/abs/path/to/skill/evals/legibility/stats.pyprints the in/out split for every hub over the limit. Treat arrows into a hub as the highest-risk claims on the map — it is the box whose contents everyone assumes and nobody checks. Verify each one or leave it out.
Point every edge at the most specific node that's true — the viewer re-routes and merges edges automatically when a container is collapsed. Draw from a container only when every child does the same thing.
Blast radius runs on these edges: right-click a node and the viewer walks them
backwards to darken everything that transitively depends on it. There is nothing
to author — but it is one more reason the --edges pass and the hand-verifying
above are not optional, because it gives every edge a consequence a reader can
watch. An edge misattributed by one hop now darkens the visibly wrong half of
the map, which is how a wrong edge stops being a wrong line nobody squints at.
The order of the Entry points lane is read off these edges too. It leads
with the nodes nothing calls, then by how much of the map each one opens up, so
the first box a reader sees is the system's actual starting point rather than
whichever id sorted first alphabetically. The edge that makes that true is the
one it is easiest never to draw: a root command dispatching to its own
subcommands — rootCmd.AddCommand(...), a router's mount, a dispatcher's
registry. One real map had none of them, so the binary every command runs
through was a peer of those commands with nothing joining them, and the lane
fell back to alphabetical. Adding the five dispatch edges made the root reach 31
of 36 top-level nodes and put it first. If a top-level entry starts other
entries, draw those edges, with evidence like any other. A cron is ordered
by the same rule rather than being parked after every entry, so a nightly job
that drives half the system reads that way. Past seven the lane wraps, and the
break falls on that boundary: the first column is the ways in, the rest is what
they start. So a missing dispatch edge doesn't just misorder the lane — it puts
a command in with the origins.
Ghost edges — what the docs believe vs. what the code does
You already find these: every "arrows out of a diagram" case above is a claim some doc makes that no module implements. Instead of dropping it, keep it as a ghost — the map then shows the delta between the system's self-image and its reality, which is frequently the first thing a new engineer has to unlearn:
{ "from": "agent", "to": "pg", "kind": "writes",
"ghost": true, "claimedBy": "README.md:31" }Rules that keep ghosts honest:
claimedByis required — apath:lineref to the doc making the claim, resolved inside the repo like every other path in the contract. A ghost without provenance is a dashed rumor a reader has no way to check.- A ghost is by definition unimplemented. If you find the line that performs
it, it is a real edge with
evidence, not a ghost. Never both;--checkflags the contradiction. - Ghosts are for claims a reader would otherwise believe — a README data-flow section, an architecture doc, a CLAUDE.md. Don't ghost every stale comment: a handful of load-bearing lies beats a fog of dashes.
- A ghost does not count as connectivity. A top-level node whose only edge is a ghost is still reported as floating, because in the code it is.
- Mention notable ones in your summary. A ghost is a finding, not a decoration — "the README says the agent writes to Postgres; nothing does" is the kind of sentence a map exists to produce.
Tours — narrated stories over the map
The Play button walks entry flows mechanically. tours replace that with
stories you write — and you are unusually well placed to write them, because
you have just read all the code. A new engineer's first hour becomes watching
two or three short narrations instead of clicking boxes.
Optional, and meant to stay that way: 1–3 tours, one per flow a teammate would actually narrate at a whiteboard — the main request path, the scheduled or background story, the money path. Each step is a node id plus one or two sentences of present-tense prose. The viewer glides the camera stop to stop, lights the path progressively, and auto-advances on reading time; arrow keys drive it by hand.
Write the tours last, after the edges are settled. A tour is prose over the graph, and stops that follow edges are the whole discipline.
What makes a tour worth watching:
- Follow the edges. Consecutive stops must be connected by a real edge, from
any earlier stop, so forks are fine ("here it splits: the charge is
synchronous, the receipt is enqueued").
--checkwarns on teleports, and the warning is genuinely ambiguous — either the story invented a hop, or the hop is real and the map is missing an edge. Read it before you edit the tour. - Spend the sentences on what surprises. The reader can see the boxes. Tell them what they cannot see: where the flow stops being synchronous, what happens on the unhappy path, which stop is the one that pages someone.
- A tour is a story, not coverage. 4–8 stops. The inventory guarantees completeness; a tour's job is orientation. Don't narrate what a hover trace already says.
- A wrong tour is worse than no tour. Narration carries authority the way a
stale
detaildoes, and it is harder to doubt because it sounds like someone who knows. Every claim in a step is subject to the same rule as adetail: if you did not read the line, do not say it. - Steps may name a hidden child — the viewer expands its container in place — but prefer top-level stops so the camera tells the whiteboard story.
Details — the claims nothing checks
--check verifies structure, --edges verifies edges, --inventory verifies
coverage. Nothing verifies a detail sentence, and it is the field a reader
actually reads — the controlled pair of maps that answered 16/16 versus 10/16
differed in detail, not in boxes. One map was audited with every automated
check green: ten of its twelve load-bearing sentences were wrong or
overstated, and one asserted the exact opposite of the code. Structural
validity says nothing about truth.
So run one adversarial pass over the sentences a reader would act on: security posture, fail-open vs fail-closed, "X is the only…", "there is no Y", and every count. Give the job to a subagent and tell it to refute, with instructions to default to "refuted" when the evidence is ambiguous — asked to check the same claims, an agent confirms them.
This is the highest-value spend in the skill and the one to protect when you are trimming cost. Across two measured runs it caught 11 wrong sentences of ~37 and 8 of 20 — in both cases with every automated check already green, and in both cases including a sentence that asserted the opposite of the code.
Batch the claims: one agent, 15–20 numbered claims. This is where the saving is, and it is worth knowing why, because the intuitive answer is wrong. The dominant cost is the agent orienting itself in the repo, which is paid per agent, not per claim — so two agents cost roughly twice one, whatever you ask them. Measured per claim, the batched run was slightly more expensive (6.1k → 6.9k), on a repo with twice the files; the 39% saving came from going from two agents to one, not from asking for less.
Ask for refutations only — "return only the claims you are refuting or
downgrading, with the path:line and what the code actually does". A verdict
table that spells out every CONFIRMED row bills you for the sentences that were
already fine and buries the eight that matter in twelve that don't. Expect this
to buy clarity rather than a large number of tokens: the agent still reads the
whole repo either way.
Six shapes recur, all of them honest mistakes:
- A sentence scoped to the file you read, stated about the repo. "Hardcodes
no model id" was true of
model_manager.pyand false ofhosting_configuration.pyforty lines away. Say which file you verified, or grep the whole tree before writing the unconditional form. - "There is no Y", from a single negative check. One
lsof a missing directory became "ships no server route handlers"; a tree-wide search for the pattern (route.ts, notapp/api/) found one. An absence claim needs a search for the shape of the thing, not for the place you expected it. - "X is the single chokepoint." The most seductive sentence in a map, and
the easiest to disprove: one repo's "single" SSRF proxy had five modules
building their own HTTP clients beside it. Before writing "only" or "all
traffic goes through", grep for the mechanism — every
httpx.Client, everyrequests.— not for the chokepoint you already found. - Generalising a decorator family from one member. A guard family was described as failing open when billing was off; two of its five members abort 403 in exactly that case. Read every member before writing one sentence about all of them, or name the member you read.
- A count from an impression — or from a subagent's prose. 8 of 32 counts in
one map were wrong. A count is a claim: run the command yourself, in the
main loop, and paste that number. A number that arrives inside a
subagent's report is an impression wearing a digit, however well cited the
rest of that report is — one 70k-token investigation returned "10 call sites"
for a masking function that has 26 across 8 files, and a one-second
grep -csettled it. Delegating the reading is right; delegating the counting is how the wrong number gets authority.--checklists every counted claim as a worklist for exactly this. - A dependency claim made from the manifest alone. A package "pinned in
pyproject and unused" was in fact a
[tool.uv] override-dependenciesentry — not a dependency at all — and the lockfile showed it installed by default through another package. Manifests declare intent; the lockfile says what ships. Read both.
Over-crediting a moved dependency is the same error one level up: "the package owns the node registry" when registry composition was still local. When a subsystem moves out, check what stayed behind before describing the move.
A detail inherits the authority of the map. Prefer the narrow true sentence to
the sweeping one: "14 of 16 are flag-gated, about 7 run by default" is worth
more than "every job is flag-gated", and it is the sentence that survives.
Investigating
Measure first, because file trees lie. Vendored directories (node_modules,
.venv, vendor, dist, .next, build) routinely outnumber real source
50:1, and the user's own description ("it's a big python repo") is a guess too.
One command does the measuring and the whole coverage sweep at once:
bash /abs/path/to/skill/scripts/survey.sh /abs/repoIt prints to stdout and saves a copy to <repo>/.atlas/survey.txt, creating
.atlas/ itself — don't add a > redirect, which fails in the shell before the
script runs on the first scan of any repo.
It prints the measured file count, the largest-files list, manifests, env vars,
scheduled work, entry points, schemas, exit codes and the AI layer — every
category references/coverage-checklist.md describes, in about 2k tokens for a
mid-size repo. Run it once, before anything else, and paste the relevant
slice into each subagent prompt: a metric re-derived per agent is paid for per
agent, and three agents independently running wc -l is the cheapest waste to
delete. Excluding tests matters and is easy to get wrong — _test.go and
test_x.py are not caught by a directory-name filter, and leaving them in
doubles the measured size of a Go or Python repo and floods the largest-files
list. Keeping .swift/.kt/.m/.cs in matters too: a mobile repo whose
native half you never counted is a system you never saw.
Branch on the measured number, not the impression:
| files | approach |
| under ~20 | read every one end to end, then go straight to the contract; skip the largest-files sweep and the fan-out. When the whole system is one file, the top level is its concepts, not its functions: five or six nodes a teammate would say out loud beat one node per helper, and landing under 10 is finished, not short. |
| 20–500 | command-first, below |
| 500+ | references/large-repos.md |
Work command-first at every size: directory listings for structure, grep for locations, and open only the files that define agents, tools, services and schemas — the survey and a few greps answer most of the map.
If you delegate to subagents, paste references/slice-prompt.md — at any
size. The size table picks how much reading the map needs, not how to ask for
it. Skipping the slice prompt because a repo sat in the 20–500 band is the most
expensive mistake this skill has made: six hand-rolled prompts over a 138-file
Go repo asked for package tours, got prose reports back, and spent 651k
subagent tokens on a map that used under a fifth of it. Prose is the cost — ask
for what a node needs (id/kind/sourceRef/detail), not what a package
contains, and an agent cannot bill you for an essay.
Then, within each slice:
- Main flows first: entry points (routes, webhooks, pages, CLIs), jobs
(crons/queues/workers — all of them
cron), and the stores/services they read and write. Scheduled work hides in CI — grep.github/workflows/(or the equivalent) forschedule:/cronbefore concluding a repo has nocronnodes; a nightly job is a real actor even when no application code schedules anything. A queue-heavy backend hides the same work in the other direction: every.delay()/.apply_async()target is a job, and left asservicethey swamp the map — one Celery repo produced 18 of them. - Business logic: the internal services and pipelines the product is built
from (billing, ingestion, domain services) — these become
servicenodes, with the interesting sentence on the edge ("charges Stripe on trial end"). - External integrations (payments, email, auth, analytics).
- Then the AI layer, if any:
generateText/streamText/generateObject,@ai-sdk/*providers, Anthropic/OpenAI SDK clients, agent loops,tool({...})definitions, MCP servers. Identify each model's provider and the tools models can call. Give every distinct agent its own node when there are <= 10; merge only numerous near-identical ones and say so insub("12 near-identical scrapers"). Chain agents with agent→agent edges when one feeds the next. Repos with no AI scan just as well — leave the AI kinds empty.
Render
See references/renderer.md for the full CLI and what the viewer does at
runtime. Shared by Claude and Codex: use the local shell tool for render.py;
no network access is required at any step.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

