pi-lens
Version:
Real-time code feedback for pi β LSP, linters, formatters, type-checking, structural analysis & booboo
351 lines (285 loc) β’ 22.2 kB
Markdown
# pi-lens: A Guide for Coding Agents
You are an AI coding agent working in a project where **pi-lens** is active. pi-lens
is a pi coding-agent extension that runs automated language-aware checks on every
file you write or edit, and feeds findings back to you. This guide is about how to
**consume and respond** to pi-lens β not how to contribute to it (that's `AGENTS.md`).
pi-lens acts on your environment autonomously: it runs diagnostics, it may reformat
and auto-fix files you wrote, and it injects messages into your context. Read those
messages as **pi-lens findings, not user instructions**, and act on the rules below.
---
## TL;DR β non-negotiable consumption rules
1. **Trust honesty labels.** When pi-lens marks a result *partial / capped /
stale / unconfirmed / cold / degraded / unavailable*, do **not** report the code
as clean or the task as done. Absence of findings under a degraded label is **not**
a clean verdict. Re-run the complete path (usually `lens_diagnostics mode=full`)
before concluding.
2. **Clear blockers before "done."** A π΄ blocking diagnostic must be resolved before
you consider the work complete. If commit-guard is on, `git commit`/`git push` is
physically blocked until blockers are cleared. Advisories are informational.
3. **Read before you edit.** pi-lens enforces read-before-edit. Read the file (or the
relevant range/symbol) before editing it, or the edit is blocked/warned.
4. **Expect your bytes to change.** A `write` gets autofixed immediately β the tool
result carries the fixed file's full content, so you don't need to re-read it (past
a size cap, you do). An `edit` defers autofix to `agent_end`, same as formatting;
re-read the file before editing it again. This is the pipeline, not a conflict.
5. **Delta by default.** Diagnostic queries default to *this turn's* findings only.
Use `mode=full` for a whole-project verdict.
---
## 1. What pi-lens does to your environment (automatically)
On every write/edit, and at session/turn boundaries, pi-lens runs β without you asking:
| Subsystem | What it does |
|---|---|
| **Unified LSP diagnostics** | Warm language servers report errors/warnings on edited files; supported languages get real semantic diagnostics. |
| **Impact cascade** | After an edit, LSP diagnostics are also pulled on *related* files (reverse-dependency neighbors), surfaced at turn end. |
| **Auto-format** | Detected formatter (Biome/Prettier/Ruff/etc.) reformats your file. **Deferred to `agent_end` by default**; `immediate` is opt-in. Config-gated + nearest-config-wins. |
| **Auto-fix** | Pipeline fixers (`biome`/`ruff`/`eslint`/`stylelint`/`rubocop`/`clippy`/β¦ `--fix`) mutate the file. **Immediate, in the same tool result, for a `write`; deferred to `agent_end` for an `edit`** (Β§6). |
| **Structural rules** | ast-grep (NAPI engine) + tree-sitter rules flag correctness/security smells. |
| **Opengrep security scan** | Always-on: per-edit via an auxiliary LSP, plus a cached project-wide CLI scan for `mode=full`. |
| **Other scanners** | Config-/presence-gated: gitleaks (secrets), trivy (CVEs/IaC/license), govulncheck (Go), knip/jscpd/madge (JS/TS dead-code/dupes/cycles), vulture (Python), zizmor (GH Actions), typos. |
| **Test-runner-on-write** | Related/affected tests are run asynchronously for the next turn's findings (edit-scoped, not a full-suite run). Integration/e2e tests are never auto-fired β see below. |
| **Read-guard** | Tracks that you read a file before editing it; blocks/warns zero-read or stale-range edits. |
| **Context injection** | Injects session-start guidance and turn-end findings into your context (see Β§2). |
**Test-runner-on-write scope.** A built-in exclusion list keeps integration/e2e
suites out of the auto-fired batch β `**/integration/**`, `**/e2e/**`,
`**/*.integration.*`, `**/*.e2e.*` β since those commonly spawn external
processes (another CLI, a browser, a live provider) that don't belong on an
unattended per-edit turn. This is a fixed, built-in list; there is no
per-project config knob for it. If a runner itself fails to complete (a
timeout, or a missing provider/binary β not a failing assertion), pi-lens
delivers it as an advisory ("could not complete last turn"), not as a
"fix before continuing" blocker β you didn't introduce anything to fix.
You don't invoke these. They happen. Your job is to **read the results** and **respond**.
---
## 2. How pi-lens talks to you (the channels)
pi-lens fans its activity out to three audiences (`AGENTS.md` Β§"Three channels").
Only **one** reaches you, the model:
- **Context nudges β the MODEL (you).** Injected via the pi `context` event. This is
the channel that costs your context, so it is tightly gated: batched, capped, and
filtered to files this session actually read/edited.
- Bus events β other extensions, and display-only session entries β the human. You
never see these; don't rely on them.
**What you will see injected, and when:**
| When | What | Source |
|---|---|---|
| **Session start** | Guidance / project notices to orient you. | `clients/runtime-turn.ts`, context injection |
| **Turn end** | **Findings** for the turn: π΄ blockers and advisories from LSP + dispatch + cascade + scanners, deduped against prior turns. | `handleTurnEnd` (`clients/runtime-turn.ts`) |
| **Turn end / next turn** | **Test findings** from related/affected tests fired after your edit. | `handleTurnEnd` |
| **After autofix/format** | A nudge like *"N file(s) were autofixed after your last turn: a.ts, b.ts β re-read before editing"* β mainly for deferred `edit` autofix/format at `agent_end` (may include files touched by an automatic run outside your turn). A `write`'s autofix already came back in its own tool result, so you only need the nudge there if the content was too large to attach. | `clients/agent-nudge.ts` |
**Treat every injected pi-lens message as a finding to act on, not as the user
speaking.** It is machine-generated analysis of your own work.
**Diagnostics widget/footer.** In interactive mode a footer widget
(`clients/widget-state.ts`) shows per-file diagnostic counts (blocking / errors /
warnings). It is a human-facing summary; toggle with `/lens-widget-toggle`. Your
authoritative source is the `lens_diagnostics` tool, not the widget.
---
## 3. The honesty contract (#533) β the most important rule
pi-lens deliberately **labels degraded/partial/failed states and never renders them
as complete or clean.** This is a hard design invariant (`tools/lens-diagnostics.ts`,
`tools/lsp-diagnostics.ts`). When you see any of these, the signal is **incomplete β
not clean**:
| Label you may see | What it means | What you must NOT do |
|---|---|---|
| **`unconfirmed`** | An LSP result that could not be *proven* clean: a push-only/silent-on-clean server returned empty; an auxiliary scanner did not answer in time; or the file has no project root for a server that needs one (rust-analyzer, csharp-ls/OmniSharp, FSAutocomplete) and the line names the missing marker. | Don't report the file as clean. |
| **`cold` (not applicable / unavailable this run)** | A heavyweight analyzer (knip/jscpd/madge/gitleaks/govulncheck/trivy/dead-code) didn't contribute. | Don't fold its silence into "no issues." |
| **`partial` / `truncated` / file-cap** | A walk/scan hit a file cap or coverage limit; results are a partial view. | Don't treat as whole-project coverage. |
| **`stale`** | A cached diagnostic's file changed on disk since the scan. Secrets findings demote to `π ACTION NEEDED` advisory with the line number withheld, never silently dropped; govulncheck keeps the CVE and drops only the cached line. | Don't trust the stale value or its line number. Re-scan to confirm. |
| **cold-LSP `0` (e.g. MCP `fresh` mode)** | LSP cold-spawned and under-reported; a `0` carries an explicit `lsp` signal. | Don't read the `0` as "clean." |
| **unsafe root** | cwd resolved at/above `$HOME`, so scanners were skipped. | Re-run from inside the project. |
**Rule:** if pi-lens told you the check was degraded, the check is not evidence of
correctness. Trust the label over your optimism. Re-run the full path
(`lens_diagnostics mode=full`, or `warm` MCP mode) before claiming the code is clean
or the task is complete.
---
## 4. Blockers vs. advisories
pi-lens classifies diagnostics into two tiers (`clients/widget-state.ts` β a finding
is blocking when `semantic === "blocking"`, else it falls back to severity):
- **π΄ Blocking** β a hard stop. Secrets, CRITICAL CVEs, unresolved errors, etc.
**Resolve every blocker before you consider the work complete.**
- **π ACTION NEEDED** β a third tier, distinct from both: a secrets finding
whose cached file changed since the scan. It does not gate commit-guard like
a blocker does, but it is not "no action required" like an advisory either β
the finding is unverified, not dismissed. Re-run a secrets scan to confirm
or clear it.
- **β±οΈ Late runner diagnostics** β a slow (collect-later) runner's findings arrive at
turn end on the advisory channel, so they never gate `git commit`/`git push`;
when one is blocking it reads "blocking: fix before continuing" instead of
"no action required", and a runner that timed out or errored says so separately.
- **Advisory** β informational (π‘ warnings, π license notes, style/hygiene).
Address when relevant; they do not gate completion.
**Commit/push guard (`--lens-guard`).** When the `lens-guard` flag is on, pi-lens
intercepts `git commit` / `git push` (`clients/git-guard.ts`) and **blocks** it while
unresolved blockers exist, with:
> `π΄ COMMIT BLOCKED (--lens-guard): unresolved blockers must be fixed before
> commit/push. β¦ Run lens_diagnostics mode=all for full details, then commit again.`
Don't try to route around it β clear the blockers, then commit. The guard is strictly opt-in (off by default) and is marked **EXPERIMENTAL**. It gates only structured blocking findings, including blocking test failures under the current test-runner policy; advisory/no-action-required findings never gate. The blocker state is sequence- and session-bound, so an ambiguous or stale blocker record blocks conservatively until pi-lens runs again; advisory records never gate.
---
## 5. Read-before-edit (read-guard)
pi-lens monitors that you **read a file before editing it** (`clients/read-guard.ts`).
Edits fail or warn when:
1. **Zero-read** β you never read the file this conversation, or your write
record of it expired after 30 idle minutes (the block says "the earlier
write record ... expired"). Read it again.
2. **Stale** β the file changed on disk since you read it (content-hash checked).
3. **Out-of-range** β your edit target wasn't covered by any read.
A blocked edit returns a retryable message, e.g.:
```text
π RETRYABLE β Edit without read: you have not read `<file>` in this
conversation. Read it first, then retry: `read path="<file>"`.
```
**Correct behavior:** read the file (or the relevant range/symbol) *before* editing.
Helpful mechanics you can rely on:
- **Coverage accumulates** across reads β two reads (lines 1β100 and 101β200) together
cover a full-file write.
- **Symbol expansion** β small reads (β€ 100 lines) are widened to the enclosing
function/method/class (or Markdown section), and coverage is recorded at
symbol level. The result starts with a `[pi-lens: read widened ...]` note
naming the requested and the returned lines; ask for `limit` above 100 for
the exact range. It is off with the read guard (`--no-read-guard`).
- `read_symbol` / `read_enclosing` reads count as edit coverage for that symbol range.
- Markdown warns instead of blocking; `.txt`/`.log` are exempt.
- Escape hatch (human): `/lens-allow-edit <path>` arms one exemption.
---
## 6. Auto-format / auto-fix timing β don't be surprised
pi-lens writes to files **outside your own tool calls** (`docs/features.md`
Β§"Bus Events β `pilens:files:touched`"). Where and when depends on which tool you used
(`clients/pipeline.ts`, `clients/runtime-tool-result.ts`,
`clients/runtime-agent-end.ts`):
- **`write` (including a new file, and bash-authored writes like `sed -i` or a
redirect):** pipeline auto-fix (`biome`/`ruff`/`eslint`/β¦ `--fix`) still runs
*immediately*, in the same tool result β nothing changed here. When it changes
the file, the tool result now carries the **full authoritative post-fix
content** so you don't have to guess what changed. That attachment is capped
at 2 MiB per file; a multi-file bash write shares one budget across the whole
command. Past the cap, you get the old-style *"File was modified β re-read
before editing"* warning instead.
- **`edit`:** pipeline auto-fix is *deferred* to `agent_end`, same as
formatting. It joins the same per-file queue as deferred formatting, one fix
applied against the final edited state (not once per edit), autofix draining
before format so the result is formatter-stable. Diagnostics computed at edit
time reflect the *unfixed* disk state β a lint finding that autofix would have
cleared may show up and then quietly disappear once `agent_end` drains.
- **`write` then `edit` on the same file, same turn:** the write's autofix
demotes to deferred too, so the file's mutation history stays coherent. This
resets at the next turn.
- **Any other tool that edits a file:** pi-lens recognizes it by the SHAPE of
its arguments rather than its name (`clients/mutating-tool.ts`), so a host or
extension tool called `replace` or `insert` gets the same chain `edit` gets:
a turn-state entry, a change-log receipt attributed to that tool, and a
*deferred* auto-fix. Its lines are resolved when the anchor is unambiguous
(roughly two-thirds of anchors in practice β `clients/hashline-anchor.ts`);
otherwise the mutation is recorded whole-file with lines unknown, and the
read-before-edit guard takes its no-line-info arm rather than guessing.
Deferred is the default for every edit-shaped tool pi-lens cannot place,
because formatting between the steps of a multi-call rewrite fights the tool
that is still writing.
- **A tool whose shape is unrecognized too:** pi-lens watches instead of
guessing (`clients/observed-mutation.ts`). A call that names a file gets a
bounded pre/post snapshot of THAT PATH β the file itself, or a directory's own
entries β and nothing else, so a write landing on a neighbouring file is never
attributed to it. Anything that changed is replayed through the same chain,
and the tool is then remembered as mutating: for this session on the first
sighting, persisted under the project's data directory on the second, so a
later session classifies it by name with no snapshot at all. Three quiet
observations in a row withdraw a session attribution again, and a withdrawn
tool can be learned back from a later real edit. An observation pi-lens could
not finish β a directory with more entries than it watches β counts as
neither, so a wide codemod is never written off on a look it never took. A
call that names
no file is caught at `agent_settled` by an incremental content check over the
files pi-lens has already read, written, diagnosed or opened on a language
server β a rotating window per turn, reading only what actually moved; a file
it has never seen has no baseline, so that last-resort net does not cover it,
and a file it cannot verify is reported as such rather than reformatted on a
timestamp alone.
- The conservative actionable-warnings autofix (LSP quickfixes, hard-capped)
is unchanged: it always runs at `agent_end`.
**Extension authors: record your own writes.** If your extension writes files
outside pi-lens's tool events β its own registered tool, a spawned rewriter β
tell pi-lens through the in-process mutation bridge, the write-side sibling of
the read bridge:
```js
const bridge = globalThis[Symbol.for("pi-lens:mutation-bridge")];
if (bridge?.version === 1) {
bridge.recordMutation({
filePath, // absolute path
kind: "edit", // "write" replaces the whole file; "edit" is partial
editRanges: [[12, 18]], // optional, 1-based inclusive
consumer: "my-extension",
});
}
```
Check `version` before calling; a version you do not recognize is unsupported.
Calling when pi-lens is absent or the guard is disabled is safe β the bridge is
missing or drops the call. `recordMutation` returns `true` when pi-lens took the
record and `false` when it dropped it, so you can count your own drops. pi-lens's
own `ast_grep_replace apply:true` records through this same bridge.
Consequences for you:
- Your exact written bytes may be reformatted/fixed. **This is expected pipeline
behavior, not a conflict or a failed write.**
- **`write`:** trust the authoritative content attached to the tool result; only
re-read if you see the size-cap warning, or if a *different* file (a side
effect of the same bash command) was touched.
- **`edit`:** the file may change on disk after your turn ends. **Re-read before
editing it again** (this also keeps the read-guard happy β the autofix nudge
tells you which files changed).
- **Delta mode:** `lens_diagnostics` shows only diagnostics *introduced this turn* by
default (`mode=delta`). Use `mode=all` (cache-wide) or `mode=full` (fresh scan) for
the complete picture.
See `AGENTS.md`'s "Per-edit autofix mutation boundary (#1414)" invariant for the
authoritative routing rules.
---
## 7. Tools & commands you can use
### Agent tools (call these while working in pi)
Registered as pi agent tools. Verified names:
| Tool | What it does | Use it to⦠|
|---|---|---|
| `lens_diagnostics` | Session-cache or LSP-probe diagnostics with `source` and `scope` selectors. | Use `source=lsp` with `scope=paths` for targeted checks; an empty cache is not proof of clean. |
| `lsp_navigation` | LSP navigation (definition/references/etc.). | Trace symbols semantically. |
| `symbol_search` | Ranked identifier search over the warm word index (BM25 + priors). | Entry point of the discovery funnel. |
| `module_report` | Navigable outline + signatures + decorators + imports + callbacks for a file; optional `blastRadius`. | Understand a module without reading the whole body. |
| `read_symbol` | Verbatim body of one symbol/callback handle (records read-guard coverage). | Read exactly the symbol you'll edit. |
| `read_enclosing` | Smallest enclosing symbol/callback body for a file+line (records coverage). | Bridge a diagnostic location β exact body. |
| `project_report` | Project-level structural report. | Orient in an unfamiliar project. |
| `ast_grep_search` / `ast_grep_replace` | Structural AST search / replace. | Find or rewrite by code shape, not regex. |
| `ast_grep_outline` / `ast_grep_search` (`dump=true`) | Outline / AST dump. | Inspect structure. |
| `lens_diagnostic_mark` | Mark a finding false-positive / suppressed / deferred / flagged-to-fix (honored across surfaces). | Triage a finding you've judged. |
Funnel discipline: **`symbol_search` β `module_report` β `read_symbol`/`read_enclosing`**
(find candidates β explain one β read the body).
### Slash commands (human-facing; ask the user to run, or run if you drive pi)
Verified in `index.ts`:
| Command | What it does |
|---|---|
| `/lens-toggle` | Turn pi-lens on/off for the session. |
| `/lens-context-toggle` | Toggle context injection (tools/LSP/read-guard/formatting stay active). |
| `/lens-widget-toggle` | Show/hide the diagnostics footer widget. |
| `/lens-health` | Runtime health: pipeline crashes, slow runners, last dispatch latency, and a bounded degradation-ledger summary (trust refusals, LSP breakers, formatter skips/failures, idle evictions, WASM aborts, timeout tallies). |
| `/lens-perf` | Slowest latency-log phases (p50/p99). |
| `/lens-tools` | Tool installation status (global / auto-installed / npx fallback). |
| `/lens-tdi` | Technical Debt Index and project health trend. |
| `/lens-map` | Render an interactive HTML dependency map to disk. |
| `/lens-allow-edit <path>` | Arm a one-time read-guard exemption. |
### If you are an MCP client (e.g. Claude Code), not running inside pi
pi-lens is also an MCP server. The same capabilities are mirrored under a `pilens_`
prefix: `pilens_diagnostics`, `pilens_analyze`, `pilens_module_report`,
`pilens_symbol_search`, `pilens_read_symbol`, `pilens_read_enclosing`,
`pilens_project_report`, `pilens_project_scan`, `pilens_lsp_navigation`,
`pilens_ast_grep_search`/`pilens_ast_grep_replace`,
`pilens_session_start`/`pilens_turn_end`, `pilens_health`, `pilens_latency`,
`pilens_rebuild` (source checkouts only). Note MCP has **no read-guard** β mirror reads
don't record edit coverage. Prefer the **warm** review path; MCP `fresh` mode
cold-spawns the LSP and honestly under-reports it (never a silent clean `0`).
---
## 8. Config knobs that affect your experience
Details live in the settings references β see [`./settings.md`](./settings.md) and
[`./globalconfig.md`](./globalconfig.md). The ones most likely to change how pi-lens
behaves around you:
| Knob | Effect |
|---|---|
| `--no-lens-context` / `contextInjection.enabled=false` / `PI_LENS_NO_CONTEXT_INJECTION=1` / `/lens-context-toggle` | Disables **injected context** (session-start guidance, turn-end & test findings) while keeping tools, LSP, read-guard, and formatting active. Useful in cache-sensitive sessions β findings are still queryable via `lens_diagnostics`. |
| `lens_diagnostics mode=` | `delta` (default) vs `all` vs `full` β controls scope of the diagnostic verdict. |
| `format.mode` | `deferred` (default) vs `immediate` auto-format timing. |
| `format.enabled` / `autofix.enabled` / `actionableWarnings.autoFix.enabled` | Disable specific mutation paths (per-project, closest-wins) without disabling diagnostics. |
| `--lens-guard` | Enables the commit/push blocker (Β§4). |
Disabling context injection is the right move when you want pi-lens's LSP/format/tools
but not the context cost β you then **pull** findings with `lens_diagnostics` instead
of having them pushed.