pi-linter
Inline session linter for Pi — flags vague openers, pronoun soup, scope creep, and other input anti-patterns above the editor.
Package details
Install pi-linter from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:pi-linter- Package
pi-linter- Version
0.2.6- Published
- May 18, 2026
- Downloads
- 662/mo · 414/wk
- Author
- tianrendong
- License
- MIT
- Types
- extension
- Size
- 32.4 KB
- Dependencies
- 0 dependencies · 2 peers
Pi manifest JSON
{
"extensions": [
"./index.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
pi-linter
Inline session linter for pi. Opt-in rules catch vague openers, pronoun soup, scope creep, unbounded loops, and other input anti-patterns before you hit Enter.
A small, deterministic linter that renders findings above the input bar in pi. No LLM calls, no latency. Pure regex/heuristic rules. Suggestions only — it never blocks your message.
▲ vague opener — add a link, file path, or error pi-lint:vague-opener
↳ implement <linear/notion url> · fix <file>:<line>: <error>
✖ unbounded loop — add stop criteria: when to escalate or quit pi-lint:unbounded-loop
↳ "retry once on flaky tests, ping me on any other failure"
Why
System-prompt linters (PromptLint, PromptDoctor, etc.) check production prompt artifacts for things like prompt injection and token bloat. pi-lint targets a different surface: the user's chat turn in a multi-turn coding-agent session. Its rules need conversation context (last assistant turn, message count) and they fire on the soft, human anti-patterns that quietly degrade agent sessions.
Install
pi install npm:pi-linter
Or develop locally:
git clone https://github.com/tianrendong/pi-packs
cd pi-packs
npm install
pi install ./packages/pi-linter
The npm package is
pi-linter(the unscopedpi-lintslot was blocked by npm's name-similarity check). Slash command is/linter; config file (~/.pi/pi-lint.json) and env vars (PI_LINT_*) still usepi-lint.
Rules
pi-linter ships with every rule off by default. Opt in per rule via
/linter enable <rule> — start silent, then enable only rules you want.
| Rule | Severity | Triggers when | Fix template |
|---|---|---|---|
vague-opener |
warn | First message of session, <60 chars, no URL / file path / issue ID | implement <linear/notion url> · fix <file>:<line>: <error> |
reactive-noop |
warn | Prompt <80 chars matching still not working, try again, same issue, didn't work, still failing/broken/wrong |
"ran X, got Y instead of Z; restarted the worker first" |
unbounded-loop |
critical | Contains watch, monitor, keep running/trying, every <N>, until, loop, forever AND no stop criterion |
"retry once on flaky tests, ping me on any other failure" |
pronoun-soup |
warn | Prompt <300 chars contains 2+ bare this/that/it/them/they not anchored to a noun |
"the receipt processor" not "it" |
imperative-only |
warn | Prompt is exactly do it, yes, go, continue, ok, proceed, fix it, etc. AND last assistant turn didn't end with a question |
"go ahead with option B" |
scope-creep |
info | Not the first message AND starts with let's also, also,, btw,, while you're at it, one more thing |
capture in TODO.md, finish current PR, start a fresh pi session |
reversal |
info | Starts with actually, or actually |
"sketch the data model first, push back if you disagree" |
naked-review-paste |
warn | Contains Comment N: / Hunk: AND non-paste instruction text is <40 chars |
"address all" |
review-drip |
info | 3rd+ pasted review comment in the same session | "here are 5 comments, address them all and tell me which you disagree with" |
Configure
Inside pi:
/linter interactive menu
/linter status show rule state
/linter disable <rule> opt out of one rule
/linter enable <rule> opt in to one rule
/linter off fully disable
/linter on re-enable
/linter reset clear rule opt-ins (all rules off)
Persistent config lives at ~/.pi/pi-lint.json.
Environment variables
Env vars override the persisted config so existing setups keep working:
| Var | Effect |
|---|---|
PI_LINT_OFF=1 |
Fully disable pi-lint for this session |
PI_LINT_DISABLE=rule1,rule2 |
Disable specific rules for this session |
PI_LINT_ENABLE=rule1,rule2 |
Opt in to rules for this session |
PI_LINT_POLL_MS=250 |
How often to re-evaluate the draft (default 250ms, min 50ms) |
How it works
- On
session_start, pi-lint installs an interval that polls the editor text every ~250ms. - Each tick rebuilds a small
LintContextfrom session state (isFirstMessage,lastAssistantText,priorReviewPasteCount) and runs all enabled rules against the current draft. - If the set of findings changes, pi-lint updates a widget above the editor via
ctx.ui.setWidget("pi-lint", lines, { placement: "aboveEditor" }). - On
session_shutdown, the interval is cleared and the widget is removed.
The rules in rules.ts are pure functions over LintContext.
You can read them like ESLint rules — the file is small and adding a new rule
is one entry in the RULES array.
Quiet contexts
pi-linter intentionally stays silent when:
- The draft is a pi bash-mode invocation (leading
!or!!, e.g.!ls,!!grep -rn 'still failing' .). These are shell commands, not natural-language prompts — the rules don't apply. - The draft is empty or whitespace-only.
/linter offorPI_LINT_OFF=1.
Compatibility
Works in interactive mode (TUI). In -p print mode and JSON mode, hasUI is
false and pi-lint is a no-op. In RPC mode, getEditorText() returns "", so
pi-lint also stays silent there.
License
MIT — see LICENSE.