UNPKG

pi-lens

Version:

Real-time code feedback for pi β€” LSP, linters, formatters, type-checking, structural analysis & booboo

351 lines (285 loc) β€’ 22.2 kB
# 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.