pi-fovea

Token-budgeted repo mapping for agent sessions: foveated heat diffusion over a cross-language code graph, with progressive disclosure.

Packages

Package details

extensionskill

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.

npm version checks pi extension license

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 status for loaded versions, graph coverage, and active modes
  • /fovea settings for a TUI configuration overlay
  • /fovea reset for a fresh focus and sync baseline
  • /fovea reload to 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.