pi-fovea 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,10 +4,10 @@
4
4
 
5
5
  **A foveated repo-mapping extension for [Pi](https://github.com/earendil-works/pi-coding-agent)**
6
6
 
7
- _Survey, focus, dwell, impact a budget-capped field of view instead of a folder dump._
7
+ _See the whole repo on every prompt, sharp where you work and cheap everywhere else._
8
8
 
9
9
  <p>
10
- <img src="https://raw.githubusercontent.com/monotykamary/pi-fovea/main/media/cover.svg" alt="pi-fovea: a code graph seen through a fovea hot at the center, collapsed at the rim" width="1100">
10
+ <img src="https://raw.githubusercontent.com/monotykamary/pi-fovea/main/media/cover.svg" alt="pi-fovea: a code graph seen through a fovea, hot at the center and collapsed at the rim" width="1100">
11
11
  </p>
12
12
 
13
13
  [![npm version](https://img.shields.io/npm/v/pi-fovea?style=for-the-badge&logo=npm&color=cb3837)](https://www.npmjs.com/package/pi-fovea)
@@ -17,35 +17,23 @@ _Survey, focus, dwell, impact — a budget-capped field of view instead of a fol
17
17
 
18
18
  </div>
19
19
 
20
- ---
20
+ pi-fovea hands the model a map of your repo on every prompt. The repo compiles once into a code graph across languages, where symbols, files, and route anchors join into one network. Each question becomes an interest vector that diffuses over the graph as heat. The renderer converts the field into a token-capped view: full signatures near your task, one-liners a hop away, a skeleton of the rest.
21
21
 
22
- Large models have small context. pi-fovea turns a repository into a **heat field** over a cross-language code graph symbols, files, route anchors and hands the model exactly `maxTokens` of it at a time, sharp where you look and whole-picture-but-cheap everywhere else. After every edit it silently re-syncs, and only speaks up when the change didn't stay local.
22
+ After each assistant turn the map re-syncs incrementally. Detection reads content hashes instead of tool events, so edits made by pi's edit/write tools, a fabric_exec inner `pi.edit`, a bash heredoc, a subagent, or an editor save outside the session all land identically. A clean turn stays silent. A turn that moves route anchors or warms files you have not looked at says so.
23
23
 
24
- ## Why Fovea?
24
+ ## What the model gets
25
25
 
26
- | | Capability | What it unlocks |
27
- | :-: | ---------- | --------------- |
28
- | 🔭 | **Survey** | `fovea_sketch` renders the whole repo as a low-acuity silhouette feature anchors and basins by mass, never raw file lists. |
29
- | 🎯 | **Focus** | `fovea_focus` centers on a symbol, route, or env key: hot nodes as signatures, warm nodes as one-liners, periphery collapsed. |
30
- | ⏱️ | **Dwell** | `fovea_dwell` diffuses longer and returns only the delta. Chebyshev vectors are cached a new timescale is coefficient recombination, not a re-walk. |
31
- | 🌡️ | **Impact** | `fovea_impact` predicts the co-change cascade across languages what a file, symbol, or PR base warms up. |
32
- | 🩸 | **Turn sync** | After every edit turn the graph re-syncs for free. Anchor shifts and unwatched warmings surface as red flags; stable turns stay silent. |
33
- | 🪙 | **Token truth** | Budgets are hard caps, not hopes: the renderer fits a monotonic prefix and never exceeds `maxTokens`. |
26
+ | Command | Ask | Answer |
27
+ |---|---|---|
28
+ | `fovea_sketch` | where is everything? | the repo as a silhouette, with feature anchors and inferred regions ranked by mass |
29
+ | `fovea_focus` | what is this? | centered on a symbol, route path, or env key: hot nodes as signatures, neighbors as one-liners |
30
+ | `fovea_dwell` | what else? | diffuses the field one step further and returns the delta |
31
+ | `fovea_impact` | what does this touch? | warms everything a file, symbol, or PR base reaches across languages |
34
32
 
35
- ## How it works
36
-
37
- The repo compiles to a typed graph whose edges carry **conductance**: imports are bridges, calls are solid, and shared literals — route paths, env keys — are the cross-language welds, weighted by specificity. Your interest is a heat source `s`; the map the model receives is the heat kernel run for time `t` over the graph Laplacian:
38
-
39
- ```text
40
- v(t) = e^{−tL} · s
41
- ```
42
-
43
- - **sketch** — large `t`, hub + anchor seeds: the whole repo in one blurry-but-valid silhouette.
44
- - **focus** — small `t`, your query as the seed: the fovea on exactly that feature.
45
- - **dwell** — `t` ×2 per call; only newly-luminous nodes are returned.
46
- - **impact** — changed files as the seed; warmth = predicted blast radius.
33
+ Two slash commands on top:
47
34
 
48
- Lineage: spectral-graph heat kernels (SGWT evaluated by shared Chebyshev recurrence), progressive image coding (budget as bitrate over significance-sorted coefficients), foveated rendering. Nanobridge: aider's PageRank repo map is the fixed-timescale special case.
35
+ - `/fovea status` for graph stats and sync state
36
+ - `/fovea settings` for an overlay in your TUI, styled after pi-fabric's `/fabric settings`
49
37
 
50
38
  ## Install
51
39
 
@@ -73,34 +61,28 @@ pi install /absolute/path/to/pi-fovea
73
61
 
74
62
  </details>
75
63
 
76
- Then, in any repo session, the model gets the four `fovea_*` tools; you get:
77
-
78
- - `/fovea status` — graph stats, sync on/off
79
- - `/fovea settings` — an overlay built from the same SettingsList idiom as pi-fabric's `/fabric settings`
80
-
81
- ### CLI
82
-
83
- The same ops, stateless and pipe-friendly — for agent shells, CI, and `llmc`-style uses:
64
+ There is also a package for any agent shell or CI:
84
65
 
85
66
  ```sh
86
67
  fovea sketch /path/to/repo 900
87
68
  fovea focus /path/to/repo "/v1/messages" 800
88
69
  fovea impact /path/to/repo --base main 1200
70
+ fovea rules /path/to/repo
89
71
  fovea status /path/to/repo
90
72
  ```
91
73
 
92
- (`fovea` bins to `cli.ts` via `tsx`; install `tsx` globally or use `pnpm fovea` from a checkout.)
74
+ `fovea` runs `cli.ts` via `tsx`. Install `tsx` globally, or use `pnpm fovea` from a checkout.
93
75
 
94
- ## Turn-sync (default on)
76
+ ## Turn sync
95
77
 
96
- After every assistant turn, fovea re-syncs the graph guaranteed incremental by content hash no edits means zero work. The verdict:
78
+ Turn sync is on by default. After every assistant turn the graph is re-synced against your edits: unchanged files cost nothing because every parsed fact sits behind its content hash. The verdict is **green** or **red**:
97
79
 
98
- - **green** silent in the model's context (a UI toast only if `sync.ackClean` is on).
99
- - **red** a capped custom message: route anchors that appeared/disappeared, plus files the edit cascade warmed that the model hasn't focused yet.
80
+ - **green**: silence in the model's context. A clean-toast shows only if you enable `sync.ackClean`.
81
+ - **red**: a capped custom message naming route anchors that appeared or disappeared, plus files warmed by the edit cascade that the model has not focused on yet.
100
82
 
101
- The first sync establishes the baseline; the first drift after it calibrates the warm neighborhood rather than alarming, so a steady feature cone doesn't page the model every edit.
83
+ The first sync seeds the baseline. The first drift after it calibrates the warm neighborhood, so a steady feature cone stays quiet. Sub sequent drift turns red.
102
84
 
103
- Opt out per-repo or globally: `/fovea settings` → "Turn sync → false", or
85
+ Turn it off per repo or globally: `/fovea settings` → Turn sync, or
104
86
 
105
87
  ```sh
106
88
  FOVEA_TURN_SYNC=off pi
@@ -108,46 +90,42 @@ FOVEA_TURN_SYNC=off pi
108
90
 
109
91
  ## Configuration
110
92
 
111
- Global `~/.pi/agent/fovea.json`; project override `<repo>/.pi/fovea.json` when trusted the same two-scope model as pi-fabric's `fabric.json`.
93
+ Global settings live in `~/.pi/agent/fovea.json`. A trusted repo-level override sits in `<repo>/.pi/fovea.json`. Two scopes, the same model pi-fabric uses with `fabric.json`.
112
94
 
113
95
  | Key | Default | Meaning |
114
96
  | --- | :-----: | ------- |
115
97
  | `sync.enabled` | `true` | the turn-sync loop |
116
- | `sync.budget` | `1024` | tokens for the red model-visible report |
117
- | `sync.ackClean` | `false` | toast on clean structural turns (no model tokens either way) |
118
- | `sync.warmFileThreshold` | `2` | newly-warm undisclosed files that justify red |
119
- | `tools.defaultBudget` | `2000` | fallback maxTokens for fovea_* tool calls |
98
+ | `sync.budget` | `1024` | token cap for the red report seen by the model |
99
+ | `sync.ackClean` | `false` | toast after clean structural turns |
100
+ | `sync.warmFileThreshold` | `2` | warmed files unseen by the model that justify turning red |
101
+ | `tools.defaultBudget` | `2000` | fallback maxTokens for the fovea_* tools |
120
102
 
121
- ## Repo rule packs
103
+ ## How routes are found
122
104
 
123
- The built-in pack catches route declarations by **port shape**, not framework name — five shapes cover almost the whole ecosystem:
105
+ Route anchors come from port shapes, and five shapes cover almost the whole ecosystem:
124
106
 
125
107
  | Port shape | Examples |
126
108
  |---|---|
127
- | `recv.verb("path", handlers…)` | express, koa, fastify, hono, gin, echo, chi, net/http (any quotes, incl. TS template literals and Python f-strings) |
128
- | verb-annotation + optional class prefix | NestJS `@Controller + @Get`, Flask/FastAPI decorators, Spring `@RequestMapping + @GetMapping` |
109
+ | `recv.verb("path", handlers…)` | express, koa, fastify, hono, gin, echo, chi, net/http |
110
+ | annotation + optional class prefix | NestJS `@Controller + @Get`, Flask and FastAPI decorators, Spring `@RequestMapping + @GetMapping` |
129
111
  | verb embedded in the path | Go 1.22 `mux.HandleFunc("GET /x", h)` |
130
112
  | verb as first string argument | chi `r.Method("GET", path, h)`, aiohttp `router.add_route("GET", path, h)` |
131
113
  | receiver-less DSL macros | Rails `routes.rb`, Phoenix `router.ex`, Django `path()`, Ktor `routing { get("/x") {} }` |
132
114
 
133
- File-convention routers never write a route string at all — those anchors are derived from **file paths** (Next.js App Router `app/**/route.ts` + `page.tsx`, Pages Router `pages/api/**`, SvelteKit `+server.ts` / `+page.svelte`, Nuxt `server/api/**.get.ts`, Astro endpoints), with verbs pulled from exported handler names or file-name suffixes.
134
-
135
- **Known blind spots** (deliberate, logged in `src/core/anchors.ts`): Rust proc-macro attribute routers (actix `#[get("/x")]`, rocket) — ast-grep patterns can't parameterize attribute paths; frameworks with constructor-assigned prefixes (Flask Blueprint, FastAPI `APIRouter(prefix=…)`, chi `Mount`, Express `Router` mounts) — variable binding tracking is out of band; `scope`/`namespace` nesting in Phoenix/Rails/Django `include()` — prefixes across blocks aren't composed; tRPC/GraphQL/gRPC — no path token exists to anchor on.
136
-
137
- ### Tier 3: discovery mode
115
+ File-convention routers declare paths nowhere in code. Next.js App Router, Pages Router, SvelteKit, Nuxt, and Astro anchors therefore derive from file paths, with the verb read off exported handler names or filename suffixes.
138
116
 
139
- Unknown shapes self-heal. During the literal pass fovea harvests a per-repo histogram of *call-shape signatures* — `(lang · shape · callee · argIdx)` with a path-precision — and promotes statistically significant unknown ones into **implicit rules**: exact-arity ast-grep patterns, synthesized automatically, wired into the graph at **half hub gravity** with a `△` sigil in `fovea anchors` output. Sync reports discovered churn but never lets an unconfirmed hypothesis escalate to red; a hub upgrades to first-class the moment any site matches a non-implicit rule.
117
+ ### Discovery mode
140
118
 
141
- Promotion needs: ≥4 path-carrying sites, spread over ≥2 files, and a Jeffreys-smoothed posterior `p̂ = (pathN + .5) / (n + 1) 0.55`. Measured against 8 cloned projects, junk bands land below p̂≈0.27 and real shapes above p̂≈0.75 the line is not tuned to the corpus, it sits mid-cliff. Frameworks already known to the static pack are never re-promoted.
119
+ When a repo writes routes in a shape fovea has never seen, the literal pass harvests every call shape and promotes statistically solid ones into implicit rules. Discovered anchors carry half the conductance of declared ones and appear with a `△` sigil. Turn sync reports their churn without letting an unconfirmed hypothesis turn the verdict red. A hub upgrades to first-class the moment a known rule matches any of its sites.
142
120
 
143
121
  ```sh
144
- fovea anchors <root> --discovered # only the △ hypothesis hubs
145
- fovea rules <root> # promoted rules + evidence, ready to paste into .fovea/rules.json
146
- fovea rules <root> --sigs # every path-touching signature, by precision (audit the corpus)
147
- fovea rules <root> --adopt # write them into the repo's rule pack explicitly
122
+ fovea anchors <root> --discovered # the △ hypothesis hubs only
123
+ fovea rules <root> # promoted rules with evidence
124
+ fovea rules <root> --sigs # every path-touching signature, by precision
125
+ fovea rules <root> --adopt # persist promotions into .fovea/rules.json
148
126
  ```
149
127
 
150
- Drop `.fovea/rules.json` in a repo to extend anchor detection beyond the built-ins:
128
+ `.fovea/rules.json` pins community or project rules in the repo:
151
129
 
152
130
  ```json
153
131
  {
@@ -157,23 +135,43 @@ Drop `.fovea/rules.json` in a repo to extend anchor detection beyond the built-i
157
135
  }
158
136
  ```
159
137
 
160
- Changing the rules file invalidates **only** the anchor extraction cache green-node reuse one level up.
138
+ A rule may declare `prefixPattern` so a class-level prefix like `@Controller('api/airports')` composes with per-method paths. Changing the rules file invalidates the anchor extraction cache alone; parsed facts above it carry over.
139
+
140
+ **Blind spots**, logged in `src/core/anchors.ts`: 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/gRPC (no path token exists to anchor on).
141
+
142
+ ## How it works
143
+
144
+ The repo compiles to a typed graph. Your question is a source vector $s$ over its nodes, and the field the model receives is the heat kernel at time $t$ over the Laplacian $L$:
145
+
146
+ $$
147
+ v(t) = e^{-tL} \cdot s \quad \text{with} \quad L = I - D^{-1/2} W D^{-1/2}
148
+ $$
149
+
150
+ The four tools are the same operator at four timescales: sketch at $t=16$ with hub and anchor seeds, focus at $t=4$ with your query as seed, dwell doubling $t$ per call with a disclosed-set delta, and impact using the changed files as seed.
151
+
152
+ The kernel is evaluated with a Chebyshev expansion. Rescale $M = L - I$ so the spectrum sits in $[-1,1]$; then with $T_k$ the Chebyshev polynomials and $I_k$ the modified Bessel functions:
153
+
154
+ $$
155
+ 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]
156
+ $$
157
+
158
+ The vectors $T_k(M) s$ are cached in the session. A new timescale costs coefficient recombination, never a second graph walk.
159
+
160
+ Discovery asks how often the argument at one slot of one call shape carries a route path, and promotes the shape past a Jeffreys-smoothed posterior:
161
161
 
162
- A rule may additionally declare `prefixPattern` (e.g. NestJS `@Controller('api/airports')`) so per-method paths like `@Get('search')` compose into the full router-visible anchor `GET /api/airports/search` — see `ts-http-decorator*` in `src/core/anchors.ts`.
162
+ $$
163
+ \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}
164
+ $$
163
165
 
164
- ## How the graph is joined
166
+ Measured against eight cloned projects, corpus junk sits below $\hat{p} \approx 0.27$ and real route shapes above $\hat{p} \approx 0.75$. The cutoff stays mid-cliff regardless of repo size.
165
167
 
166
- - **imports / contains / inherits / tests** outline-derived; call edges specificity-tiered, with language builtins and log/test entry points warded off
167
- - **literal joins** — route paths, env keys, OpenAPI operation paths; document-frequency-gated cliques so rare literals bridge strongly and ubiquitous ones don't become gravity wells
168
- - **co-change** — mined from recent git history (Jaccard-tilted, per-file capped, cached by HEAD), so files that commute together warm each other even without a static edge
169
- - **feature hubs** — route declarations and every client call of the same normalized path collapse to ONE anchor node: where client, server, and spec meet
170
- - **basins** — where there are no routes at all (CLIs, kernels), sketch infers implicit features as conductance-cut regions around triangle-dense seeds
168
+ Lineage: spectral-graph wavelets evaluated by shared Chebyshev recurrence, progressive image coding where the budget is a bitrate over significance-ordered coefficients, and foveated rendering. Aider's PageRank repo map is the fixed-timescale special case of this field. The full walkthrough of conductance tiers, specificity bridges, hub gravity, and basins lives in [docs/heat-diffusion.md](docs/heat-diffusion.md).
171
169
 
172
- ## Language matrix
170
+ ## Languages
173
171
 
174
- Full symbol + call extraction: **TypeScript/TSX · JavaScript · Python · Go · Rust**.
175
- Outline-based symbols with heuristic naming: **Elixir · Ruby · C · C++ · Java · Kotlin · Lua · PHP · Swift · Scala · Haskell · Bash**.
176
- Config joins through literals: **YAML · JSON · TOML · env · Markdown · OpenAPI**.
172
+ Full symbol and call extraction: **TypeScript, TSX, JavaScript, Python, Go, and Rust**.
173
+ Outline-based symbols: **Elixir, Ruby, C, C++, Java, Kotlin, Lua, PHP, Swift, Scala, Haskell, and Bash**.
174
+ Config joins through literals: **YAML, JSON, TOML, env, Markdown, and OpenAPI**.
177
175
 
178
176
  ## Development
179
177
 
@@ -183,6 +181,6 @@ pnpm run check # typecheck + full vitest suite
183
181
  pnpm run bench # rate–distortion bench against ../pi-fabric
184
182
  ```
185
183
 
186
- pi loads the extension straight from `src/` via jiti — **there is no build step**. Per-repo caches live in `$TMPDIR` (content sha1 per file; only dirty files re-run ast-grep). Bump `CACHE_VERSION` in `src/core/build.ts` when extractor semantics change.
184
+ pi loads the extension straight from `src/` via jiti; there is no build step. Per-repo caches live in `$TMPDIR` behind per-file content sha1 hashes, and only dirty files re-run ast-grep. Bump `CACHE_VERSION` in `src/core/build.ts` whenever extractor semantics change.
187
185
 
188
186
  [MIT](LICENSE).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-fovea",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Token-budgeted repo mapping for agent sessions: foveated heat diffusion over a cross-language code graph, with progressive disclosure.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -28,6 +28,9 @@
28
28
  "extensions": [
29
29
  "./src/index.ts"
30
30
  ],
31
+ "skills": [
32
+ "./skills"
33
+ ],
31
34
  "image": "https://raw.githubusercontent.com/monotykamary/pi-fovea/main/media/cover.svg"
32
35
  },
33
36
  "exports": {
@@ -36,6 +39,7 @@
36
39
  "files": [
37
40
  "src",
38
41
  "cli.ts",
42
+ "skills",
39
43
  "media",
40
44
  "README.md",
41
45
  "LICENSE"
@@ -0,0 +1,46 @@
1
+ ---
2
+ name: pi-fovea
3
+ description: Token-efficient repo navigation with the pi-fovea code graph. Use when you need to survey an unfamiliar repository, trace where a symbol or route lives and what depends on it, assess the blast radius of a change before editing, or re-orient after files have been edited mid-session (by any tool path, including bash and pi-fabric fabric_exec programs).
4
+ ---
5
+
6
+ # pi-fovea
7
+
8
+ pi-fovea maintains a cross-language code graph of the working repository — routes, symbols, imports, calls, string/env literals — and exposes it through progressive disclosure: cheap silhouettes first, detail only where you point it. It costs almost nothing until you ask, and it re-syncs automatically whenever file content drifts, no matter which tool made the edit.
9
+
10
+ ## The loop
11
+
12
+ 1. **`fovea_sketch`** — silhouettes only. Route/anchor inventory plus directory blobs ranked by heat. Start here in an unfamiliar repo. ~256–1024 tokens.
13
+ 2. **`fovea_focus` `<query>`** — point at a symbol name, route path (`/api/users/{id}`), env key, or file path. Hot nodes come back with full signatures; warm neighbors as one-liners; the periphery stays collapsed. Already-shown nodes are suppressed, so repeated focus calls stay cheap.
14
+ 3. **`fovea_dwell`** — optional second look. If a focus footer says more nodes are lit below the token threshold, dwell (diffusion time ×2) surfaces exactly those newcomers.
15
+ 4. **`fovea_impact`** — blast radius. Seed with explicit repo-relative `files`, symbol names for what-if analysis, or uncommitted changes (`base` works PR-style against a ref). Output is the predicted co-change cascade ordered by warmth.
16
+
17
+ All four accept `maxTokens` (256–16000). Budget is roughly 4 chars per token.
18
+
19
+ ## Working rules
20
+
21
+ - **Never bulk-read to find things.** Read what focus surfaced; let the graph answer "where is X" and "what uses X" instead of spawning searches.
22
+ - **Impact before destructive edits.** One `fovea_impact` call is cheaper than rediscovering dependents by breaking them.
23
+ - **Sketch is the safe opening bid.** If unsure, pay for a sketch; it almost never exceeds a few hundred tokens.
24
+
25
+ ## Turn sync
26
+
27
+ After each assistant turn, pi-fovea diffs content hashes against its baseline. If edits moved route anchors or warmed files outside the session's disclosed set, a `[fovea turn sync]` message arrives in the next turn with the delta; otherwise everything stays silent. Treat that message as ground truth about mid-session state changes.
28
+
29
+ Sync is **mutation-path agnostic**: pi's edit/write tools, a pi-fabric `fabric_exec` program's inner `pi.edit`, a bash heredoc, a subagent, or an editor save outside the session all register identically. Content hashes are the source of truth; tool events are not consulted for detection. In repos with no `.git` directory this is also the only drift signal — do not fall back to `git status` assumptions.
30
+
31
+ ## Using with pi-fabric (fabric_exec)
32
+
33
+ When writing or editing code **inside a `fabric_exec` program**, the fovea tools exist but the fabric sandbox has no built-in knowledge of them (it lazy-loads tools). Key points:
34
+
35
+ - Inside `fabric_exec`, discover them once with `await tools.search("fovea")` and call them through `tools.call({ ref, args })` — e.g. `{ ref: "fovea_focus", args: { query: "CreateUserHandler" } }`. They are ordinary pi tools; there is no fabric-specific wrapper.
36
+ - Prefer a single `fovea_impact` call over hand-rolled grep fan-outs when computing what an edit touches — the graph already resolved imports/calls across Go, TypeScript, Python, and Java.
37
+ - Any file mutation performed by the program (including `pi.edit`/`pi.write` calls inside the sandbox) is picked up by turn sync automatically, so post-edit verification does not need a re-sketch.
38
+ - The sketch `details` field carries counts (`files`, `nodes`, `anchors`); the hot-node list is the graph's highest-value entry points. On an unfamiliar repo, fetch it once and reuse instead of rediscovering entry points per call.
39
+
40
+ ## CLI
41
+
42
+ The same engine runs headlessly as the `fovea` binary (repo root scan, plus JSON and TSV modes). Prefer the in-session tools unless you need scripting or a second opinion outside the extension's session state.
43
+
44
+ ## Settings
45
+
46
+ `/fovea settings` in the TUI, or `fovea.config.json` at repo or user level. Relevant knobs: `sync.enabled`, `sync.budget`, `sync.warmFileThreshold` (files that must escape before a red sync fires), `tools.defaultBudget`.
package/src/core/sync.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  // Turn-sync: the default-on feedback loop the extension was built around.
2
2
  // After each assistant turn, if the repo's facts version drifted ANY edit
3
- // re-syncs, regardless of whether it came through pi's edit/write tools or
4
- // a shell heredoc the verdict is green (UI-only) or red (model-visible,
5
- // budget-capped):
3
+ // re-syncs, regardless of the mutation path pi's edit/write tools, a
4
+ // fabric_exec inner pi.edit, a bash heredoc, a subagent run, or an editor
5
+ // save outside the session all land the same way, because drift is measured
6
+ // by diffing the baseline's content hashes against the current facts instead
7
+ // of trusting tool events or git.
6
8
  //
7
9
  // red = route anchors appeared/vanished (structural feature churn)
8
10
  // OR warm undisclosed files >= warmFileThreshold (unseen blast radius)
@@ -11,13 +13,17 @@
11
13
  // The first sync of a session only establishes the baseline (never red).
12
14
  // Baselines reset on /new and /fork alongside fovea sessions.
13
15
 
14
- import { ensureState, impact, uncommittedFiles } from "./ops.js";
16
+ import { ensureState, impact } from "./ops.js";
15
17
  import type { RepoState } from "./ops.js";
16
18
  import { getSession } from "./session.js";
17
19
 
18
20
  interface SyncBaseline {
19
21
  version: string;
20
22
  anchors: Set<string>;
23
+ /** file -> content sha1 at baseline. Diffing this against the current facts
24
+ * yields the exact changed-file set for any mutation path — no dependence
25
+ * on which tool executed the write, nor on git. */
26
+ shas: Map<string, string>;
21
27
  /** Steady-state warmth recorded on the most recent sync. undefined = "the
22
28
  * first drift after baseline calibrates the neighborhood instead of
23
29
  * escalating" — a file list appears after that calibration sync. */
@@ -29,7 +35,8 @@ const baselines = new Map<string, SyncBaseline>();
29
35
  export const resetSyncBaselines = (): void => baselines.clear();
30
36
 
31
37
  export interface SyncParams {
32
- /** Files the turn is known to have touched. Empty + drift => git fallback. */
38
+ /** Optional drift hints (e.g. files touched by pi's edit/write tools this
39
+ * turn). Unioned into the warmth seeds; never the source of truth. */
33
40
  files?: string[];
34
41
  budget: number;
35
42
  warmFileThreshold: number;
@@ -48,6 +55,7 @@ export interface SyncOutcome {
48
55
  const snapshot = (state: RepoState): SyncBaseline => ({
49
56
  version: state.version,
50
57
  anchors: new Set(state.graph.anchors.map((a) => a.id)),
58
+ shas: new Map(Object.entries(state.facts).map(([f, x]) => [f, x.sha1])),
51
59
  });
52
60
 
53
61
  export const sync = (root: string, params: SyncParams, now?: RepoState): SyncOutcome => {
@@ -80,8 +88,15 @@ export const sync = (root: string, params: SyncParams, now?: RepoState): SyncOut
80
88
  if (at >= 0) disclosedFiles.add(id.slice(at + 1));
81
89
  }
82
90
 
83
- let files = (params.files ?? []).filter((f) => state.graph.byFile.has(f));
84
- if (!files.length) files = uncommittedFiles(root).filter((f) => state.graph.byFile.has(f));
91
+ // Exact change set: facts whose content hash moved since the baseline.
92
+ // Deleted files can't warm anything (absent from the graph) but ride along
93
+ // in details for observability.
94
+ const hinted = (params.files ?? []).filter((f) => state.graph.byFile.has(f));
95
+ const changed = Object.keys(state.facts).filter(
96
+ (f) => prev.shas.get(f) !== state.facts[f]!.sha1 && state.graph.byFile.has(f),
97
+ );
98
+ const deleted = [...prev.shas.keys()].filter((f) => !(f in state.facts));
99
+ const files = [...new Set([...hinted, ...changed])];
85
100
 
86
101
  let warmNow: Set<string> = new Set();
87
102
  let warmNew: string[] = [];
@@ -100,7 +115,7 @@ export const sync = (root: string, params: SyncParams, now?: RepoState): SyncOut
100
115
  if (!red) {
101
116
  return {
102
117
  structural: true, red: false, tokens: 0,
103
- details: { version: state.version, anchorsDelta: added.length - removed.length, warmNew: warmNew.length },
118
+ details: { version: state.version, anchorsDelta: added.length - removed.length, warmNew: warmNew.length, deletedFiles: deleted },
104
119
  };
105
120
  }
106
121
 
@@ -116,6 +131,6 @@ export const sync = (root: string, params: SyncParams, now?: RepoState): SyncOut
116
131
  const text = lines.join("\n");
117
132
  return {
118
133
  structural: true, red: true, text, tokens: Math.ceil(text.length / 4),
119
- details: { version: state.version, added, removed, warmNew },
134
+ details: { version: state.version, added, removed, warmNew, deletedFiles: deleted },
120
135
  };
121
136
  };
package/src/index.ts CHANGED
@@ -38,11 +38,11 @@ export default function fovea(pi: ExtensionAPI) {
38
38
  }
39
39
  });
40
40
 
41
- // Turn-sync loop. Edits discovered via the files touched by tool calls in
42
- // the turn's results; the graph drifts only when content actually changed,
43
- // so pure conversation turns exit early at zero cost.
44
- // Per-turn mutation accumulator. tool_execution_start carries typed args,
45
- // so edit/write paths are tracked without parsing completed tool messages.
41
+ // Turn-sync loop. The tracker below is a hint accumulator only: pi's
42
+ // edit/write tool starts give the warmth pass a head start, but sync relies
43
+ // on content-hash drift, so identical detection covers fabric_exec inner
44
+ // pi.edit calls, bash mutations, subagents, and out-of-band editor saves.
45
+ // Pure conversation turns exit at zero cost through the version fast path.
46
46
  let turnFiles: string[] = [];
47
47
  pi.on("turn_start", () => {
48
48
  turnFiles = [];