context-router
SkillFiles & storageResolve the minimum set of knowledge-graph nodes needed for a task before reading any source file. Use at the START of every non-trivial task once a project has a knowledge graph — when changing a subsystem, tracing a bug, planning work, answering a question about how something works, or onboarding. This is the mechanism that keeps a large or multi-repo codebase inside a context window: it decides what to load, what to deliberately skip, and forces the agent to declare both before working. Pairs with the knowledge-graph skill, which builds and lints the graph this one traverses.
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 context-router skill
What this skill tells your AI
The instructions your AI receives, as published by llopresto87/cypress in skills/context-router/SKILL.md and read by ahel’s review.
A large codebase does not fit in a context window, and loading all of it makes an agent worse, not better — a model that has read everything has no signal about what matters. This skill replaces "read around until it feels familiar" with a traversal that terminates and that you can defend.
This skill owns the knowledge rule — the graph is the source of truth for structure and capability, loaded minimally — and the traversal algorithm that makes the rule executable.
The knowledge rule
The project keeps one LLM-maintained knowledge system at
docs/graph/: Tier 1 routes, Tier 2 nodes own concise facts, Tier 3
leaf collections hold source-backed depth (libraries, provenance,
product, architecture, APIs, data, prompts, evaluations, plans,
runbooks, specs, decisions, tools). Never parallel doc systems.
- Load minimally, and declare it. Resolve the minimal node set from
the router — entry nodes, their
requires:closure, and only the composed depth the task names specifically — and declare what you loaded and deliberately skipped. Never bulk-read to get oriented; the graph is the orientation. The boundary you chose not to cross is part of the work's record, not a courtesy: a reader who cannot see it cannot tell an unread node from a read one. The algorithm below is the full form of this obligation; the delegation-boundary form isdocs/graph/templates/prompts/graph-session-bootstrap.md. - One home per fact. Every fact lives in exactly one node's
owns:; everything else links. Duplicated facts rot asymmetrically.graph-lint.pyenforces unique fact-keys, resolvable acyclic edges, and no version pin outside its owning library page. The code-side twin is one owner per concern: a cross-cutting behaviour — session and auth, policy emission, tolerant parsing, resource creation, a datastore — has exactly one owning component, and a second "convenience" mechanism for a concern that already has an owner is refused, because two mechanisms multiply precedence questions nobody can answer. - Graph before code, ahead of memory. Memory of APIs and versions
is unreliable; the graph is local and source-grounded. No wiki page
for a library you're about to use → run
ingest-library. - The graph compounds. Record facts when code gains them, sharp
edges when they bite,
load_when:triggers when routing missed. Never fabricate a fact, version, or URL — write "not recorded".
Authoring and maintaining what this rule loads is skill.knowledge-graph
(docs/graph/skills/knowledge-graph.md) — read its node contract once
(the _schema.md the graph was built from); dependency leaves are
skill.library-wiki. Prefer a configured Context7 / DeepWiki /
llms.txt MCP server for fetching upstream content. If the project
has no graph yet, build one via adopt-existing / knowledge-graph
first; until then, fall back to reading the README and the
plan-of-record, and say you did.
The algorithm
1. Classify the task in one sentence
Say what kind of work it is before deciding what to read. The four kinds route differently:
| Kind | Example | Entry |
|---|---|---|
| Question | "how does auth work?" | The node that owns the fact. Answer with citations. Read no code unless the node is wrong. |
| Change | "add a field to X" | The owning subsystem node + its required closure. |
| Trace | "why is this endpoint 401-ing?" | Every node on the request/data path. Follow peers deliberately — this is the one kind that legitimately crosses them. |
| Plan | "rebuild the deploy pipeline" | The plan-of-record + the relevant platform/infra nodes. |
2. Resolve entry nodes
Open the graph's router index (Tier 1) and match the task against each
node's load_when: triggers. Prefer the most specific match. A task
naming a path resolves to that subsystem's node; a task naming a
concept resolves to the node that owns it.
If nothing matches, you have found a gap in the graph. Say so, fall back to the root node, and note it for the graph's maintainer to fix.
A standard's match surfaces its standing exceptions. A
deviation.* node (kind deviation, status: standing — the schema's
"Node kinds") carries the departed-from standard's own name in its
load_when, so a task that matches the standard's topic also matches
every deliberate departure from it. Load it with the standard: a
standing deviation is part of the answer, never a lapse to fix and
never a decision to re-argue; its ends_when says when it stops
applying.
Watch for aliased names across layers. When a subsystem answers to
more than one name — a repo or folder name that differs from its
product name, its package/artifact name, and its internal code name — a
task that types one alias can silently fail to match a node keyed to
another, and neither load_when matching nor a grep sees the miss.
The Tier-1 router index must carry an explicit naming-divergence
note listing the aliases for each such subsystem, so routing and
search are not blind to any one of them. When you hit an unlisted alias,
add it to that note in the same change (like sharpening a load_when
trigger).
3. Take the closure
Load each entry node, then transitively load every node in its
requires: list. That much you cannot be correct without. It is small
by construction — if it is not, the graph is mis-modelled and should be
fixed rather than worked around.
Then, from every loaded expertise node, take the composed children
the task names specifically: descend into a child when the task
uses, exactly, a term in that child's own vocabulary — its load_when:
triggers plus its whole slug — that the parent does not already carry.
So family words sitting on the parent descend nobody, and descent never
folds a prefix the way the router's own entry matching does. A child
you take becomes the parent for its own children, which is the whole of
the recursion. composes: is a menu, not a closure: the specialisations
the task is not about stay unread, and you say so (step 5).
A child you turn out to need but that descent did not reach is the same
signal as a load_when: that should have matched and didn't. Load it,
say you widened, and sharpen that child's triggers in the same change,
so the next task routes there without you.
4. Do not take peers
peers: are the boundaries you are choosing not to cross. Load a peer
only when the task explicitly crosses into it — and when you do, say
why. The one exception is a trace: following a request or a message
across subsystems is exactly what peers edges are for.
5. Declare before you work
Print the resolved set. This is not ceremony — it is the artifact that lets a reviewer catch a bad load before it becomes a bad change.
Task: add field <F> to <entity> [change]
LOAD (N nodes, ~T tokens)
<entry node> (entry)
<required node> (requires of <entry node>)
<expertise node> (requires of <required node>)
<composed child> (composed by <expertise node> on "<term>")
NOT LOADED (with the reason)
<peer node> peer of <entry> — owns X; not touched
<peer node> peer of <entry> — holds a copy of Y; cross only if
the change must reach it
<sibling child> composed by <expertise>; no task term specific to it
Tier-3 to open on demand
<library page / spec / ADR> — if the detail is needed
<expertise node>'s "depth" map names which leaf your identity needs
One NOT LOADED section, whatever kept a node out. A peer you chose not to cross and a specialisation the task never named are the same kind of record — the boundary, and the reason it held — and a set that lists only one of them hides the other.
Then, and only then, open source files — and only the ones the loaded nodes name.
6. Widen honestly, never silently
If mid-task you discover you need a node you did not load, load it and say so ("Widening: loading — the change is not local because …"). Silent widening is the failure this skill prevents. So is stubbornly working without a node you need in order to look disciplined. Both are worse than "I was wrong about the boundary."
Dry-run it
The graph router is executable and must run inside every spawned worker
session — that requirement travels as the canonical block every
delegation brief embeds (docs/graph/templates/prompts/graph-session-bootstrap.md:
run --plan with the exact delegated task, load the closure, declare,
return the output as route evidence); this skill owns the traversal
algorithm above, not a second copy of that block. Dry-run your own
hand-resolved set against the tool:
python3 <graph-tools>/graph-lint.py --plan "add a field to X"
If the two differ, one of you is wrong — usually a load_when: trigger
needs sharpening, a cheap permanent fix.
--plan is a keyword heuristic, not an oracle. It ranks nodes by
weighted term overlap; it does not reason about a request path or a
false premise. Trust it for a single-subject change or question, and
as a floor everywhere. But on four kinds of task, trust your own
reasoning over its output:
- Traces — the right nodes are the hops on the path, which keyword overlap cannot infer.
- False-premise questions ("confirm we use X") — the correcting node may share no words with the wrong assumption; ask which node would own the truth.
- Policy questions — these have one owning node;
--planmay pad the set. Prefer the single owner. - Compound / multi-topic tasks — a task description that bundles several distinct topics dilutes each topic's distinctive terms below the keyword threshold, so the ranking can resolve to the wrong node and specialist set entirely, not merely a partial one. Probe each sub-topic separately, or explicitly discount the output for a task you know is compound.
Stopping rules
Stop loading when any of these is true:
- The closure is exhausted:
requires:transitively, plus every composed child the task named specifically. - You can state the change you are about to make and name the contract it must not break.
- The next node you would open is a
peerthe task does not cross into, or a composed child the task never named.
Do not stop merely because you have loaded "enough" files. The closure is the rule, not your comfort.
Cost discipline
- Within the loaded scope, retrieve progressively. The closure
names the files; it does not license reading them whole. Indexes,
headings, symbols, and diffs before regions; excerpts before full
files; the complete source only when exactness demands it. Query in
order of precision — exact identifier, exact phrase, constrained
keyword, scoped filters, semantic search, broad exploration last —
and let one authoritative source decide a question unless evidence
conflicts or the consequence of error justifies corroboration
(the retrieval posture:
docs/graph/method/engineering-posture.md). - A change task should load a handful of nodes. If it needs many, it is really several tasks; split it and say so.
- A trace may legitimately load many nodes along one path — but never a node off that path.
- Never load two sibling subsystem nodes "for comparison." If they are near-identical, what they share belongs in a shared node; read that instead. Needing a second sibling to infer a convention means the convention is missing from where it should live — add it there.
Anti-patterns
- Bulk-reading a subsystem to get oriented. The graph is the
orientation. Confirming a path with
ls/grepis fine; reading twenty files to build a mental model is the thing this skill stops. - Loading the whole graph "to be safe." Full load is a summary that displaces the code you actually need — the most expensive way to know the least.
- Treating
load_whenas documentation. It is an index. When a task should have matched a node and didn't, fix the trigger in the same change. - Skipping the declaration because the task is small. The declaration costs one paragraph and is the only record of what you did not read.
Reference files
docs/graph/skills/knowledge-graph.md— builds and lints the graph.docs/graph/_schema.md— the node contract (the adopted copy beside the graph;docs/graph/templates/knowledge-graph/_schema.mdis the pristine seed template, fast-forwarded on graft).docs/graph/templates/knowledge-graph/index.md— the router-index template.- the kernel (
AGENTS.md) — the context-budget rule this skill implements.
Signals
- GitHub stars
- 31
- Forks
- 1
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
context-router- Source
- github.com/llopresto87/cypress