context-router · diff
git:20260929.a9af0ab to git:20260930.dd7591d
153 added, 173 removed. Audit A to A.
---
name: context-router
- description: 'Resolve 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.'
+ description: 'Resolve 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.'
id: skill.context-router
tier: 2
kind: skill
origin: seed
title: context-router — the knowledge rule and the traversal that resolves the minimal node set for a task
owns:
- rule.knowledge
- context-router.method
- context-router.declaration
- context-router.residency
- context-router.menu
- context-router.graph-over-harness
requires:
- skill.knowledge-graph
peers:
- skill.validate-knowledge
load_when:
- "what should I load for this task"
- "resolve the minimal node set before working"
- "route a task through the knowledge graph"
- "declare loaded and skipped nodes"
- "orient in a large codebase without bulk-reading"
- "context budget for a change"
- "which leaves or children of a node to open"
- "harness default working style conflicts with graph doctrine"
artifacts:
- templates/prompts/graph-session-bootstrap.md
- templates/knowledge-graph/_schema.md
- templates/knowledge-graph/index.md
prevents: A session that opens source files before deciding which few facts the task needs, and never declares what it skipped, so nobody downstream can tell an informed omission from an unread node.
- est_tokens: 3626
+ est_tokens: 3590
---
# context-router
- 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.
+ A large codebase does not fit in a context window, and loading all of it
+ makes an agent worse: 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
+ 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.
+ runbooks, specs, decisions, tools). It is the project's only
+ documentation system; every maintained document is a layer of it.
- **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 is
+ 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: 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 is
`docs/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.
+ - **One home per fact.** Every fact lives in exactly one node's `owns:`;
+ everything else links. Duplicated facts rot asymmetrically.
`graph-lint.py` enforces 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`.
+ 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 new need extends
+ that owner, 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`.
- **A fact the graph states is settled.** Use it; never re-derive or
re-check it. Grow, `ingest-library` and canonize establish facts so
that no later session has to: the behaviour of a language or a
library, a best practice, a known bug, a decision, an owner rule. Only
a fact about the plant's own code can go stale, and only when that
- code moved. Canonize records
- a code anchor (`docs/graph/code-anchor.py --record`), and one
- comparison at session start prints one line. A quiet line: code facts
- are current. A line naming paths: facts about those paths may be
- stale, the code wins there, and the node is fixed in the same change.
- A not-recorded or not-checked line: check the code facts you rely on
- against the code. Settled facts stay settled either way. A worker sees
- no session-start line, so its code facts are current only where its
- brief carries that line saying no code changed. Paths under
- `docs/graph/` and `.cypress/` are the graph and its state, not code.
+ code moved. Canonize records a code anchor
+ (`docs/graph/code-anchor.py --record`), and one comparison at session
+ start prints one line. A quiet line: code facts are current. A line
+ naming paths: facts about those paths may be stale, the code wins
+ there, and the node is fixed in the same change. A not-recorded or
+ not-checked line: check the code facts you rely on against the code.
+ Settled facts stay settled either way. A worker sees no session-start
+ line, so its code facts are current only where its brief carries that
+ line saying no code changed. Paths under `docs/graph/` and `.cypress/`
+ are the graph and its state, not code.
- **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".
+ edges when they bite, `load_when:` triggers when routing missed. Write
+ "not recorded" for any fact, version, or URL you do not have
+ (`knowledge-graph`, rule 3).
- **The graph outranks the harness's working style**
(`context-router.graph-over-harness`). Where the graph's doctrine and
a harness's default working style differ, follow the graph. A
- harness's safety and permission policy is not working style, and the
- graph never overrides it.
+ harness's safety and permission policy is a boundary, not working
+ style, and the graph never overrides it.
- 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.
+ 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:
+ 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. |
+ | **Question** | "how does auth work?" | The node that *owns* the fact. Answer with citations. Read code only when the node proves 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.
+ 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.
+ 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
+ **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 and stays settled until its `ends_when` says 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).
+ 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 see every 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
+ by construction; if it is not, the graph is mis-modelled and should be
fixed rather than worked around.
- **Whatever else a node lists is a menu** (`context-router.menu`). The
- leaves, children, links, neighbours and index rows a loaded node names
- are not a load list. Open an item only when its one-line "load when"
- serves your task, one item at a time, and list each one you pass over
- in NOT LOADED (step 5). Only the `requires:` closure loads in full. A
- thin parent saves context only if its reader does not go on to open
- every leaf it names.
+ **Whatever else a node lists is a menu** (`context-router.menu`). Open a
+ leaf, child, link, neighbour or index row a loaded node names only when
+ its one-line "load when" serves your task, one item at a time, and list
+ each one you pass over in NOT LOADED (step 5). Only the `requires:`
+ closure loads in full. A thin parent saves context only if its reader
+ does not go on to open every leaf it names.
- 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).
+ 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`
+ ### 4. Cross a peer only on purpose
`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.
+ only when the task explicitly crosses into it, and 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.
+ Print the resolved set, however small the task. It is the artifact that
+ lets a reviewer catch a bad load before it becomes a bad change, and the
+ only record of what you did not read.
```
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.
+ 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.
+ Then, and only then, open source files: 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 <node> — 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."
+ say so ("Widening: loading <node>, because the change is not local: …").
+ 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:
+ The graph router is executable, and every spawned worker session runs
+ it: the canonical block every delegation brief embeds
+ (`docs/graph/templates/prompts/graph-session-bootstrap.md`) carries that
+ across the spawn, and this skill owns only the traversal *algorithm*
+ above. Dry-run your own hand-resolved set against the tool:
```sh
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
+ 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:
+ false premise. Trust it for a single-subject change or question, and as
+ a floor everywhere. On four kinds of task, trust your own reasoning over
+ its output:
- - **Traces** — the right nodes are the hops on the path, which keyword
+ - **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; `--plan` may pad
- the set. Prefer the single owner.
- - **Compound / multi-topic tasks** — a task description that bundles
+ - **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; `--plan` may 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
+ and specialist set entirely, beyond a merely 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:
+ Stop loading ONLY when one of these is true, because the closure is the
+ rule:
- 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 `peer` the 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/decision-economy.md` §6).
- - 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.
+ - **Within the loaded scope, retrieve progressively.** The closure names
+ the files; read within them from indexes, headings, symbols, and diffs
+ to regions, and from excerpts to full files, taking the complete
+ source only when exactness demands it. Query order and when one source
+ decides a question are the retrieval posture in
+ `docs/graph/method/decision-economy.md` ("Every operation serves a
+ decision").
+ - Confirming a path with `ls` or `grep` is fine; reading twenty files to
+ build a mental model is the bulk read the knowledge rule replaces with
+ the graph.
+ - 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, ONLY along its one path.
+ - **Read a shared convention from the shared node.** When two sibling
+ subsystems look near-identical, what they share belongs in a shared
+ node. A convention you can infer only by comparing siblings is missing
+ from its home, so add it there.
- ### Residency
+ ## Residency
Loading minimally decides *which* text enters a session; residency
decides how long it stays. Every text that can enter a session belongs
to exactly one of four classes, placed by the test, not by file type:
| Class | Test | How it is held |
|---|---|---|
| **1. Resident, full** | Every task needs it. | Loaded once at session start, in full. Nothing restates it, per-prompt hooks included. Only the kernel. |
| **2. Resident, pointer** | The model must know it exists before it can know it needs it. | Always visible as one line naming *when* to reach for it; the explanation lives in the body. |
| **3. Once per session** | Some tasks need it, and may need it again. | Protocol and skill bodies. Surfaced when first routed, then named by id as surfaced earlier, not re-suggested in full, until a reset or refresh. |
| **4. Per lookup** | It is consulted for a fact, not followed as a procedure. | Never resident. Reference corpora: read the section, cite, move on. |
An item that fits two classes takes the smaller resident footprint, and
- you say why. **Spawn-boundary exception:** repetition across a subagent
- spawn is not duplication. A spawned agent starts empty, so the brief's
- canonical block stays verbatim; dedup applies within one context, never
- across a spawn. A hook can know what it *surfaced*, never what the
- model *read*. How the per-prompt hooks hold to this is the CYPRESS
- seed's own spec `SPEC-0003-per-prompt-injection`.
-
- ## Anti-patterns
-
- - **Bulk-reading a subsystem to get oriented.** The graph is the
- orientation. Confirming a path with `ls`/`grep` is 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_when` as 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.
+ you say why. Dedup applies within one context. A spawned agent starts
+ empty, so repetition across a subagent spawn is how the brief's
+ canonical block arrives verbatim. A hook can know what it *surfaced*,
+ never what the model *read*. How the per-prompt hooks hold to this is
+ the CYPRESS seed's own spec `SPEC-0003-per-prompt-injection`.
## 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.md` is 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.
+ - `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.md` is 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.