pi-fovea
Token-budgeted repo mapping for agent sessions: foveated heat diffusion over a cross-language code graph, with progressive disclosure.
Package details
Install pi-fovea from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:pi-fovea- Package
pi-fovea- Version
0.14.3- Published
- Aug 11, 2026
- Downloads
- 4,489/mo · 4,489/wk
- Author
- monotykamary
- License
- MIT
- Types
- extension, skill
- Size
- 438 KB
- Dependencies
- 0 dependencies · 3 peers
Pi manifest JSON
{
"extensions": [
"./src/index.ts"
],
"skills": [
"./skills"
],
"image": "https://raw.githubusercontent.com/monotykamary/pi-fovea/main/media/cover.svg"
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
👁️ pi-fovea
A foveated repo-mapping extension for Pi
See the whole repo on every prompt, sharp where you work and cheap everywhere else.
pi-fovea gives the model a map of your repo on every prompt. The repo compiles once into a cross-language graph of code. Symbols, files, and route anchors share one network. Your question becomes an interest vector that diffuses through the graph as heat. The renderer caps the field inside a token budget. Near the question you get exact source locations and full signatures. One hop out you get typed relationships. Past that the repo collapses to a skeleton.
When a session starts, Fovea records a baseline of the repo. If files changed while Pi was idle, those changes enter context before the first model call. Fovea checks the repo again after each assistant turn. Detection uses file content hashes. An edit from a Pi tool, fabric_exec, bash, a subagent, or an editor looks the same to Fovea. Edits that touch only comments or formatting stay silent. A meaningful change arrives as a steer. When the agent is about to stop, Fovea starts the next turn itself.
Where fovea fits in shipping a feature
Shipping a feature in a large codebase costs time before the first edit. First you find where the feature lives. Then the change needs a map of everything it touches. Other branches keep landing while you work. Reviewers want the blast radius. In long-lived enterprise repos, these steps cost more time than the change itself. Fovea handles these steps. Each step becomes a cheap call against the code graph. Tests, review gates, CI, and rollout keep their own tools.
When not to reach for fovea
Skip Fovea on small repos. A repo of a few dozen files reads faster than it sketches. Fovea earns its cost when the working set outgrows the context window. Cross-language monorepos hit that wall early. A long-lived codebase you have never opened hits it too. Fovea narrows your reading to suggested windows. Open those windows yourself, and keep the project's format, lint, typecheck, and test commands in your loop. CI has the final say.
What the model gets
| Command | Ask | Answer |
|---|---|---|
fovea_sketch |
where is everything? | production-first silhouette; test and fixture architecture stays collapsed |
fovea_focus |
what is this? | exact matches, typed relationships, suggested reads, optional source scopes, and deterministic fresh views |
fovea_dwell |
what else? | widens the current focus and returns newly relevant neighbors |
fovea_impact |
what does this touch? | warms everything a file, symbol, or PR base reaches across languages |
grep (default hybrid) |
graph or text? | bare identifiers, qualified symbols, repo paths, and routes use Fovea; search options and obvious regex retain native grep |
Focus normalizes camelCase and common inflections. An approximate name such as
switchServer can still resolve switchingServers. A query with no certain
match returns the nearest symbols plus their locations. Direct graph edges
carry labels such as caller, callee, route, shared literal, and co-change.
Symbols that merely share a file stay collapsed.
The Hybrid grep toggle is on by default. grep({ pattern: "CreateUser" }),
grep({ pattern: "Controller.create" }), and route paths travel through the
graph. Calls that carry text-search options or obvious regexes go to Pi's
native grep. A graph miss falls back to native text. A graph error, such as a
broken ast-grep, falls back the same way and adds a one-line note marking the
result as native. Turn the toggle off to recover a purely native grep. A toggle
change reloads extensions, so Pi and pi-fabric capture the same behavior.
pi-fabric
Captured extension tools live under Fabric's extensions provider. Use the direct proxy when the action is known:
const result = await extensions.fovea_focus({ query: "CreateUserHandler", maxTokens: 512 });
return result.text;
For dynamic discovery, pass an object to tools.search and keep the returned namespaced ref:
const [action] = await tools.search({ query: "fovea_focus", limit: 5 });
if (!action) return "Fovea is not captured";
return tools.call({ ref: action.ref, args: { query: "CreateUserHandler" } });
The stable explicit ref is extensions.fovea_focus. The bare forms
fovea_focus and fovea.fovea_focus will miss.
Runtime slash controls:
/fovea statusfor loaded versions, graph coverage, and active modes/fovea settingsfor a TUI configuration overlay/fovea resetfor a fresh focus and sync baseline/fovea reloadto activate updated extension source
Install
Requires Node.js 20+ and ast-grep on PATH (brew install ast-grep, npm i -g @ast-grep/cli, or set FOVEA_AST_GREP=/path/to/sg).
pi install npm:pi-fovea
From GitHub:
pi install git:github.com/monotykamary/pi-fovea
From a local checkout:
pnpm install
pi install /absolute/path/to/pi-fovea
There is also a package for any agent shell or CI:
fovea sketch /path/to/repo 900
fovea focus /path/to/repo "/v1/messages" 800
fovea impact /path/to/repo --base main 1200
fovea rules /path/to/repo
fovea status /path/to/repo
Install the CLI globally — the published bin is a single self-contained bundle, so it runs on plain Node.js (no tsx, no node_modules):
npm i -g pi-fovea # or: pnpm add -g pi-fovea, bun add -g pi-fovea
From a checkout, pnpm fovea runs the live source via tsx, and pnpm run build:cli rebuilds dist/cli.mjs (the prepack hook keeps the published bundle in sync).
Large workspaces and startup
Indexing runs in the background at session_start. Your first prompt never
waits for ast-grep, hashing, or graph assembly. A cold sync hook reports the
progress. Later calls reuse the same shared build.
A non-Git umbrella directory treats each nested .git directory or worktree
marker as a project boundary. A folder of clones becomes many separate graphs.
Cold runs stay bounded through streamed JSONL cache I/O, 64-file extraction
batches, adaptive ast-grep chunk splitting, and a two-root resident LRU. The
limits accept environment overrides:
| Variable | Default | Meaning |
|---|---|---|
FOVEA_MAX_FILES |
8000 |
maximum indexed files in one graph |
FOVEA_MAX_FILE_BYTES |
1048576 |
maximum bytes extracted from one source file |
FOVEA_MAX_ROOTS |
2 |
resident graph, fact, session, sync, and root-metadata caches |
FOVEA_SPAWN_CONCURRENCY |
3 |
concurrent ast-grep/git child processes |
FOVEA_MEMORY_HALF_LIFE_HOURS |
48 |
wall-clock half-life of the per-node sync memory (charged cascade warmth) |
FOVEA_IO_CONCURRENCY |
32 |
concurrent file stat/read operations |
Files over the size cap keep their place in the model's view of the repo.
Failed extractions do the same. You find both in /fovea status and in tool
details.
Turn sync
Continuous sync is enabled and visible by default. Before an agent starts, Fovea establishes its baseline or injects any outside drift ahead of the first model call. After every assistant turn it compares symbols, calls, imports, literals, and anchors again. Content hashes keep the unchanged fast path cheap. Edits that touch only comments or formatting raise no signal.
A meaningful change found before agent start lands inside that run's context. A
post-turn route or dependency change ships with deliverAs: "steer". When the
agent is about to settle, triggerTurn starts the continuation. The compact
update names the changed files, the route deltas, and the newly relevant files.
It lists the causal channels behind each link: calls, imports, shared literals,
tests, or co-change history. By default it also embeds the refreshed focus
context of the top drift target (push). With sync.pushFocus off, the update
ends with a suggested focus probe for the next call (pull). Switching
branches re-baselines silently instead of steering: a git checkout
re-materializes the worktree, but the branch diff is not authored drift —
commits, pulls, and rebases still report. Clean turns stay silent. Enable sync.ackClean if you want an ack for those. Set sync.mode to
"hidden" to keep red sync context working behind the scenes without rendering it
in the transcript, or to "disabled" to turn continuous sync off.
Surprise is measured per graph node, not per file: a disclosed cascade
charges the symbols, literals, and anchors it warmed, and a later verdict
only counts mass exceeding that ledger. Re-editing the same spot re-seeds
the same charged nodes and stays silent — flip-flopped work cannot wake the
model twice, no matter how many times it flips within a session. A novel
hunk still fires, damped only by the charged nodes it overlaps. The ledger
cools by wall clock (48h half-life, FOVEA_MEMORY_HALF_LIFE_HOURS), so a
structurally re-heated neighborhood can earn a fresh verdict on a later
day. Anchor deltas follow the same evidence rule: a route add/remove
escalates only when its carrier file drifted, which makes transient
extraction artifacts quiet by construction.
Runtime controls:
/fovea status: loaded package and ast-grep versions, indexed coverage, anchor scopes, sync and grep modes/fovea reset: clear focus disclosure and depth, then establish a fresh sync baseline/fovea reload: hot-reload extensions and activate newly installed source; sync baselines (the verdict ledger) ride through on a global slot, so a reload no longer replays charged cascades as first disclosures/fovea settings: configure sync, budgets, and hybrid grep
Choose enabled, hidden, or disabled per repo or globally through settings. The environment override still turns sync off with:
FOVEA_TURN_SYNC=off pi
Configuration
Global settings live in ~/.pi/agent/fovea.json. A trusted repo-level override
sits in <repo>/.pi/fovea.json. These are the same two scopes pi-fabric uses
with fabric.json. In /fovea settings, the configured external-editor key
(Ctrl+G by default) switches both the displayed values and save destination
between project overrides and global defaults. A project override can remain
effective while its global default is being edited.
| Key | Default | Meaning |
|---|---|---|
sync.mode |
"enabled" |
"enabled" shows model-visible sync messages, "hidden" keeps them model-visible but out of the transcript, and "disabled" turns sync off. Legacy sync.enabled booleans still parse. |
sync.budget |
512 |
token cap for proactive steering context |
sync.ackClean |
false |
toast after clean structural turns |
sync.steerThreshold |
0.15 |
total surprise (channel-weighted heat above the session sync memory) that justifies proactive model steering |
sync.pushFocus |
true |
embed a budgeted focus preview of the top drift target in red syncs |
tools.defaultBudget |
512 |
fallback maxTokens for the fovea_* tools |
tools.grepMode |
"augment" |
"augment"\u0020keeps native grep and appends a Fovea graph section to symbol-query results (works with pi.grepinside fabric_exec too);"replace"keeps the legacy takeover where bare symbol queries navigate the graph instead of returning lines;"off"is native grep only. The legacy booleantools.replaceGrep still parses (true\u2192"replace", false\u2192"off") and loses to an explicit grepMode`. |
tools.grepAugmentBudget |
512 |
token cap for the appended graph section |
Budgets cap the rendered view, not the map: whenever sketch, focus, dwell, or
impact truncate results for budget, the full list spills to
$TMPDIR/pi-fovea-<op>-<hash>.txt and the footer names the path — read or grep
the file for the remainder. fovea_dwell remains the semantic widen.
How routes are found
Route anchors come from five port shapes. Together they cover most of the ecosystem:
| Port shape | Examples |
|---|---|
recv.verb("path", handlers…) |
express, koa, fastify, hono, gin, echo, chi, net/http |
| annotation + optional class prefix | NestJS @Controller + @Get, Flask and FastAPI decorators, Spring @RequestMapping + @GetMapping |
| verb embedded in the path | Go 1.22 mux.HandleFunc("GET /x", h) |
| verb as first string argument | chi r.Method("GET", path, h), aiohttp router.add_route("GET", path, h) |
| receiver-less DSL macros | Rails routes.rb, Phoenix router.ex, Django path(), Ktor routing { get("/x") {} } |
File-convention routers keep their paths in the file tree. Next.js App Router, Pages Router, SvelteKit, Nuxt, and Astro work this way. Fovea derives their anchors from file paths. The verb comes from exported handler names or filename suffixes.
Discovery mode
A repo may write routes in a shape fovea has never seen. The literal pass
harvests every call shape in it. Shapes with solid statistics get promoted into
implicit rules. A discovered anchor carries half the conductance of a declared
one and shows a △ sigil. Turn sync reports the churn. An unconfirmed
hypothesis cannot turn the verdict red. Once a known rule matches any site of a
hub, that hub upgrades to first-class.
fovea anchors <root> --discovered # the △ hypothesis hubs only
fovea rules <root> # promoted rules with evidence
fovea rules <root> --sigs # every path-touching signature, by precision
fovea rules <root> --adopt # persist promotions into .fovea/rules.json
.fovea/rules.json pins community or project rules in the repo:
{
"rules": [
{ "id": "fiber", "langs": ["Go"], "pattern": "$R.$M(\"$P\", $$H)", "methods": "^(get|post)$", "kind": "route" }
]
}
A rule may declare prefixPattern. A class-level prefix such as
@Controller('api/airports') then composes with per-method paths. A change to
the rules file invalidates the anchor extraction cache. The parsed facts above
the cache carry over.
Blind spots are logged in src/core/anchors.ts. The current list covers
Rust proc-macro attributes (actix #[get("/x")]), constructor-assigned
prefixes (Flask Blueprint, FastAPI APIRouter(prefix=…), chi Mount, Express
Router mounts), scope and namespace nesting in Phoenix, Rails, or Django
include(), and tRPC, GraphQL, and gRPC, whose call sites carry no path token
to anchor on.
How it works
The repo compiles to a typed graph. Your question becomes a source vector $s$ over the nodes. The field the model receives is the heat kernel at time $t$ over the Laplacian $L$:
$$ v(t) = e^{-tL} \cdot s \quad \text{with} \quad L = I - D^{-1/2} W D^{-1/2} $$
The four tools run one operator at four timescales. Sketch runs at $t=16$ with production hubs and anchors as seeds. Focus drops to $t=2$, seeded by your query. Dwell doubles $t$ inside the current focus. Impact takes changed files as its seed. A change of focus resets the sharp timescale and the disclosure scope.
A Chebyshev expansion evaluates the kernel. Rescale $M = L - I$ so the spectrum sits in $[-1,1]$. With $T_k$ as the Chebyshev polynomials and $I_k$ as the modified Bessel functions:
$$ e^{-tL} = e^{-t} \left[ I_0(t) T_0(M) + 2 \sum_{k\ge 1} (-1)^k I_k(t) T_k(M) \right] $$
The vectors $T_k(M) s$ stay cached in the session. A new timescale reuses those vectors and pays only for fresh coefficients. The graph walk happens once.
Discovery measures how often the argument at one slot of a call shape carries a route path. Shapes earn promotion past a Jeffreys-smoothed posterior:
$$ \hat{p} = \frac{\mathrm{pathN} + \frac{1}{2}}{\mathrm{n} + 1} \ge 0.55 \quad \text{with} \quad \mathrm{n} \ge 4 \text{ sites and} \ge 2 \text{ files} $$
Tests on eight cloned projects put corpus junk below $\hat{p} \approx 0.27$. Real route shapes land above $\hat{p} \approx 0.75$. The cutoff sits mid-cliff at any repo size.
The method draws on spectral-graph wavelets evaluated by shared Chebyshev recurrence. Progressive image coding contributes the budget-as-bitrate view over significance-ordered coefficients. Foveated rendering supplies the sharp center and the coarse rim. Aider's PageRank repo map is the fixed-timescale special case of this field. docs/heat-diffusion.md walks through conductance tiers, specificity bridges, hub gravity, and inferred regions.
Languages
Full symbol and call extraction: TypeScript, TSX, JavaScript, Python, Go, and Rust. Outline-based symbols: Elixir, Ruby, C, C++, Java, Kotlin, Lua, PHP, Swift, Scala, Haskell, and Bash. Config joins through literals: YAML, JSON, TOML, env, Markdown, and OpenAPI.
Development
pnpm install
pnpm run check # typecheck + full vitest suite
pnpm run bench # rate–distortion bench against ../pi-fabric
pi loads the extension straight from src/ through jiti, so nothing needs
building. Per-repo JSONL caches live in $TMPDIR, guarded by per-file content
sha1 values and stat manifests. Cache I/O streams. Only dirty files re-run
ast-grep. Failed extractions keep fact-free hash markers that stay visible
across launches. Those files skip the retry on each start. Bump CACHE_VERSION
in src/core/build.ts whenever extractor semantics change.
Acknowledgments
Thanks to Alp, the original user whose request for a better LSP extension started this project.
MIT.