pi-lens
AGENTS.md521 starsChanged 15 days agoMITView on GitHub
Read order: the engineering principles (every harness here carries a verbatim copy in its global instructions; vendored at docs/engineering-principles.md, never hand-edited), then this file, then the role contract for the task. This file holds only what is specific to pi-lens and wins on conflict. Dated incidents and closed decisions live in HISTORY.md. The principles, one line each, linked to their section of the vendored file. The vendored file wins on any difference from this digest. - Premise first: reproduce the…
- Reads credentials
Where files are headingNew AGENTS.md files have more rules about keeping secrets out of the code, and fewer about code style.
Of the AGENTS.md files we read, the 31,542 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.
In this repository9 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- orchestrateSkill
- release-qaSkill
- retroSkill
- verify-pi-lensSkill
- pi-lens-ast-grepSkill
- pi-lens-lsp-navigationSkill
- pi-lens-write-ast-grep-ruleSkill
- pi-lens-write-tree-sitter-ruleSkill
The file
Show its rules and text
We split this AGENTS.md into the rules it gives an agent
38 rules. About 0 are common, 18 are distinctive, and 20 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈18
Distinctive · estimate
This author's own choices. Worth a look.
20
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- could be automatic: a setting could enforce it
- Distinctive: line 5
Read order: the engineering principles (every harness here carries a verbatim copy in its global instructions.
- Distinctive: line 38
No tautological test: assert an independent effect through the real seam (§2 Testing).
- Distinctive: line 93
Update this file in the same change as the behavior, structure, command, or invariant it documents.
- Distinctive: line 99
Place new invariants in the matching subsystem section, not at the file end.
- Very specific: line 104
Keep project instructions consistent with CLAUDE.md, role contracts, skills, and tests.
- Distinctive: line 130
Test telemetry by flushing and reading the real sink.
- Distinctive: line 247
Mixed path comparison: use one platform-aware containment expression.
- Distinctive: line 259
Unsettled resource: every timer, worker, child, watcher, and loser path is unref'd or cleared on every settle path.
- Distinctive: line 260
Incomplete freshness: use the right content, size, mtime, dependency, and existence axes.
- Distinctive: line 264
Cooldown beyond caller cadence: verify both recovery suppression and promotion of values served during cooldown.
- Distinctive: line 300
Wrong argv transform: verify a command is the wrapper shape before dropping an argv element or launcher name.
- Very specific: line 303
Tool root drift: all runner, formatter, and LSP child spawns use resolveToolCwd.
- Very specific: line 304
Language-specific rule: use LANGUAGES and registry facts.
- Very specific: line 306
Unhandled stream or process event is a host-fatal throw: every callback on a child process, socket, or stream is total (catch, bound, …
- Very specific: line 308
Wasm failure outside the #3605 containment: every failure thrown out of web-tree-sitter classifies through classifyTreeSitterWasmError, and …
- Distinctive: line 326
Source assertion for runtime behavior: prefer a runtime probe; source scans need proof that runtime observation is impossible.
- Very specific: line 336
In-process module double with a real seam: when a clients/ module has a test setter, adopter, or reset, drive that seam.
- Distinctive: line 356
Process latch for session state: every once-latch has a session reset.
- Distinctive: line 363
Load-time platform constant: use a live platform read or an isolated fresh import for every platform branch test.
- Very specific: line 365
Subset without a population verdict: when a mechanism, policy, guard, or optimisation targets N of M members, name the excluded default and …
- Distinctive: line 367
Known identity carried forward: when a producer knows an identity, carry it through asynchronous stages instead of re-deriving it …
- Very specific: line 429
Use module_report with blastRadius: true before and after production edits.
- Very specific: line 430
Use read_symbol or read_enclosing for bodies.
- Very specific: line 460
Only the write/edit tool_result path may block the host.
- Very specific: line 463
Do not pair bumpFileSeq and change-log writes at a new call site.
- Distinctive: line 506
Use a pinned home/data environment for probes and child processes.
- Very specific: line 555
Never hand-edit generated .js or dist/.
- Very specific: line 572
All loggers use createNdjsonLogger.
- Very specific: line 605
Every logic change has relevant tests.
- Very specific: line 664
When the defect is an ordering of awaits on one seam (a coalescing queue, a per-key serializer), or the seam has regressed before, write a …
- Very specific: line 670
Use tests/support/fault-injection.ts for wedged children, seam delays, starved budgets, and gates.
- Very specific: line 683
Every skipIf(process.platform …) names the lane that runs it or reads // lane: dev-box-only.
- Distinctive: line 716
Keep performance bounds close to measured fixed and regressed values.
- Distinctive: line 719
Prefer behavioral assertions over snapshots.
- Distinctive: line 778
Commit subjects use the repository conventional prefix, imperative mood, issue reference, and no trailing period.
- Very specific: line 799
Every issue should carry one TYPE label and at least one area: label.
- Very specific: line 814
When a session touches the repository, triage open unlabelled issues.
- Very specific: line 836
Every agent Bash call under Claude Code runs through scripts/hooks/guard-bash.mjs (PreToolUse), which denies with its reason.
≈ “Common” is an estimate: at least 1 in 5 of the 40,246 AGENTS.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
pi-lens — agent context
How to use this file
Read order: the engineering principles (every harness here carries a verbatim
copy in its global instructions; vendored at
docs/engineering-principles.md, never
hand-edited), then this file, then the role contract for the task. This file
holds only what is specific to pi-lens and wins on conflict. Dated incidents and
closed decisions live in HISTORY.md.
The principles, one line each, linked to their section of the vendored file. The vendored file wins on any difference from this digest.
- Premise first: reproduce the defect through the production call path before the fix (§1 Building).
- Speculative robustness needs buy-in: no new mechanism for an unreproduced race or edge case without the trigger path, evidence, cost and a maintainer decision (§1 Building).
- Climb the minimalism ladder: reuse an existing seam before adding a line (§1 Building).
- Net-count rule: a new shared helper deletes a same-shape sibling in the same change (§1 Building).
- Sweep the pattern, not the instance, and record the consolidation verdict (§1 Building).
- Design shared state once, before a second writer joins (§1 Building).
- Give every new failure path one bounded, observable record (§1 Building).
- Diff the blast radius as a call tree, with
+/-on moved lines (§1 Building). - Red first, and paste the failing transcript (§2 Testing).
- Ship the tests in the same commit as the code (§2 Testing).
- Quote each mutation's red; a ticked box with no transcript is false (§2 Testing).
- No tautological test: assert an independent effect through the real seam (§2 Testing).
- Close only when every criterion is met; otherwise
Refsand post the remainder (§4 Honesty and records).
<important if="a code change, subsystem-specific change, delegated work, or pi documentation work">Task routing:
- Any code change: read Issue and PR design contract, Recurring defect shapes, and Test requirements.
- LSP, dispatch, runner, formatter, installer, cache, session, telemetry, review-graph, Git-guard, or rule work: read the matching row of the Standing invariants table and the file it names, plus the relevant source tests.
- Delegated work: read
docs/pi-lens-subagent.mdand exactly one role contract fromdocs/pi-lens-{fixer,reviewer,investigator,monitor,warden,retro}.md. - Pi documentation work: read the installed pi documentation named by the global instructions. Do not infer SDK behavior from memory.
</important>What it is
pi-lens is a pi coding-agent extension. It runs bounded analyzers on file
writes, dispatches LSP and CLI runners, stores diagnostics, and exposes pi and
MCP tools. The host adapters are index.ts and mcp/server.ts; internal work
flows through clients/lens-engine.ts or the appropriate client seam.
Analysis-root invariant: clients/analysis-root.ts is the sole classifier for
out-of-session edits. A marked project may be adopted for LSP and per-file
linters, but adopted roots are separate from session roots, capped at two with
early idle eviction, and never receive session bookkeeping, whole-project
scanners, turn-end tests, or project-local executables under unknown pi trust.
Registration follows the class, not a directory, through
resolveSessionConfigRoot: a session file registers the nearest directory
holding any file the config loader layers (PROJECT_CONFIG_LOCATIONS, else
the session root), a linked-worktree file the
same below the worktree root, spelled as the file spells it (the session-root
registry contains lexically; the class compares identities, #4689), and an
adopted or none file registers nothing —
an adopted root reaches LSP only through resolveServerRoot's adopted branch
and ClientRegistry.admitAdoptedRoot. A linked worktree is its own workspace,
so its edits take the same dispatch, formatting, autofix and LSP treatment as a
session edit, and tool_result records tool-result-linked-worktree for that
root and states a run a language server answered that recorded no finding
(tool-result-linked-worktree-clean, keyed off the required
PipelineResult.checkedClean, never off output or the all-clear line), so
the path is never silent.
The repository ships compiled JavaScript. TypeScript sources are authoritative; compiled twins are generated and must not be edited by hand.
Maintaining this file
Update this file in the same change as the behavior, structure, command, or
invariant it documents. Model-facing prose is derived: the schema constant owns
the sentence, docs link to it, and a doc that restates behaviour is a bug. Keep
live rules short and load-bearing. Put dated
narratives and completed arcs in HISTORY.md; do not delete their decision
record, and do not copy its detail back unless it changes a future decision.
Place new invariants in the matching subsystem section, not at the file
end. Cite symbols and section headings, not line numbers. A defect shape that
claims enforcement names its guard by path; a guard that is only planned is
named by its open issue, never described as if it runs.
Keep project instructions consistent with CLAUDE.md, role contracts, skills,
and tests. The repository wins when a runner-side copy differs. Role rules live
only in docs/pi-lens-*.md; .claude/agents/*.md are thin Claude Code
wrappers that point there and hold only harness-specific lines.
Issue and PR design contract
The principles govern building, testing, and closes-versus-refs. pi-lens adds:
- Issues lead with the outcome, then evidence, root cause (or a labeled hypothesis), acceptance criteria, non-goals, failure semantics, test matrix, observability, and class-sweep coverage. A PR body leads with a short summary and stays short: proof is transcripts and mutation rows, not narrative. The issue reference goes in the PR title; the closing keyword goes in the body, because GitHub ignores it in a title.
- Mutation acceptance has one PR layer (#4005): every new guard, branch,
filter, cap, or fallback, and the behaviour behind every relaxed or rewritten
assertion, has bounded compile-valid hand-mutation proof under the
engineering principles (rows:
docs/pi-lens-fixer.md"Evidence"), and a missing proof or a demonstrated correctness gap blocks merge. There is no per-PR Stryker job, comment, orMUTATIONline to read; the nightly Stryker run on master (.github/workflows/stryker-nightly.yml) is an exploratory report, never a merge input or a duty of a fixer or reviewer. - A declared behaviour-preserving refactor proves itself with an old-versus-new probe table through the built seam plus a shared-seam mutation that reds a caller-side witness; a passing pre-fix run is expected there.
- Test telemetry by flushing and reading the real sink. State the record that proves the fix in the PR body.
- Blast radius uses
module_reportwithblastRadius: truebefore and after the edit, and is re-run after conflict resolution. - A fold verdict tables the ordered stages each site passes through and the count each stage sees, not only the participants: moving a filter one stage late can starve it of its population (#3166 r1).
- A core-domain rule lives in its owning module; every other caller asks that owner. A fix that re-derives an owned rule at a consumer is wrong: extend the owner, or create a new one only with a stated reason (#3781, #3794, #3796).
- A change on a lifecycle, timing, or identity seam extends or adds a TLA+
model in step with the code (#3802, #3878).
formal/coverage-map.jsonmaps source globs to model families;scripts/check-pr-body.mjsrequires a.tla/.cfgchange under any one of a mapped row's families, or aTLA+ unaffected: <family> — <reason>line, andvalidateCoverageMapreds a family without its map row.unmodelledrows and rows of 4+ families stay advisory, except theindex.tsrow (docs/pi-lens-fixer.md"Before code"). The unaffected line never covers a change to behaviour an as-built config constant encodes: flip the constant and its configs in the same PR. A round that writes a state-space table for a seam whose row isunmodelled, or that no row maps, adds a model family and its map row in the same PR; the only alternative is aTLA+ deferred: <reason>line that the maintainer accepts. Screen: does this round's state table have a model that runs in CI?
<important if="delegating work or coordinating a lane">Orchestration and delegated work
The principles' delegation contract applies. pi-lens adds:
- Plegma fix lanes publish themselves:
publishRefscovers a new lane's own branch,fixForcovers its rounds, andghpassthrough comes with either. A worker still proves its own registered worktree before it touches Git. A worker without that authority hands off throughPR_BODY.mdandCOMMIT_MSG.txt; the warden is read-only. Anyone who moves a hand-back tree ports the diff onto the lane base and never copies whole files over a newer base (#4504's copy reverted 5 AGENTS.md lines). - Route to the strongest available fixer, whatever the priority label: a brief
that adds or edits a
tests/support/sweep-kit.tsregistered-or-fail sweep, and a path-key or normalizer change on a shared map (#3178). A cheap model (Haiku) takes a fix lane only when its brief prescribes the change; a lane that must find its own failure space or make a design call goes to Sonnet or Opus. - Human decision, never an agent verdict alone: workflow permission grants, release/version changes, dependency majors or lockfile regeneration, deletion of user data or durable records, external-contributor PRs, and changes to these rules.
The orchestrator-only procedure (brief authoring, review intake, the fold check, the per-PR loop, and the CI read) lives in docs/pi-lens-orchestration.md.
</important>
## Glossary- finding — Umbrella term for an agent-visible result; owned by
clients/finding-delivery-gate.ts; retiresdiagnostic,blocker,advisory, andrecord(except a durable cache row). - diagnostic — A structured finding carrying dispatch identity such as
tool,rule, and location; owned byclients/dispatch/types.ts; retires unqualifiedfindingwhen a structured dispatch value is meant. - blocker — A semantic
blockingfinding that can stop progress; owned byclients/dispatch/types.ts(OutputSemanticandDiagnostic); retiresstop issueanderrorwhen the delivery tier is meant. - advisory — A non-blocking finding delivery tier; owned by
clients/finding-delivery-gate.ts; retireswarningwhen the model-facing tier is meant. - disposition — A mark and its policy result (
false-positive,suppress,defer, orflagged); owned byclients/diagnostic-dispositions.ts; retiresmarkandstatusfor the stored policy concept. - strict anchor — A content-bound
dd:disposition identity; owned byclients/diagnostic-dispositions.ts; retirescontent keyandfalse-positive id. - weak anchor — A non-content-bound
ddw:disposition identity; owned byclients/diagnostic-dispositions.ts; retiressoft anchorandpersistent mark id. - freshness — The verdict that evidence still matches its reference (
fresh,stale, orindeterminate); owned byclients/freshness.ts; retiresvalidityandagewhen reference drift is meant. - delivery surface — A concrete model-facing place that renders or returns findings; owned by
clients/finding-delivery-gate.ts; retiresconsumerandoutput path. - delivery gate — The freshness, disposition, and policy admission applied before a delivery surface emits findings; owned by
clients/finding-delivery-gate.ts; retiresfilterandrender check. - lane — One producer-and-delivery contract within the delivery-surface registry; owned by
clients/finding-delivery-gate.ts; retirespathandchannelfor a registered surface. - seam — A shared call through which sibling surfaces enforce one rule; owned by
clients/dispatch/finding-policy.ts; retireshelperwhen the call is an architectural enforcement boundary. - store — The owner of durable or session rows, including their read/modify/write lifecycle; owned by
clients/durable-store.ts; retirescachewhen state ownership, not derived reuse, is meant. - mirror — A derived copy refreshed inside the writer's guard; owned by
clients/diagnostic-dispositions.ts; retiresreplicaandshadow. - path spelling — The input string form of a path, before key or canonical normalization; owned by
clients/path-utils.ts; retirespath nameandraw pathwhen form is meant. - path key — A normalized process-local map key; owned by
clients/path-utils.ts(normalizeEphemeralMapKey); retirescanonical pathfor ephemeral maps. - path identity — The kernel-real path of a file: the realpath of its nearest existing ancestor plus the missing tail, what
normalizeMapKeyanswers for an absolute spelling andrealpathOrResolvefor any spelling, on every platform; owned byclients/path-utils.ts; retirescanonical pathandresolved pathwhere identity is meant. - rendezvous id — A pure, cross-process string derivation shared by independent writers/readers; owned by
clients/mcp/ipc.ts; retiresworkspace keyandIPC path key. - generation — A monotonic/session/content/scan/disposition-store identity that rejects late work; owned by
clients/generation-guard.ts(GenerationSource,GenerationHandle, andcreateGenerationSource); retiresepochandversionwhen the identity's lifecycle is meant. - degradation record — A bounded once-only or counted ledger event for a partial, unavailable, or deferred result; owned by
clients/degradation-ledger.ts; retireslog,warning, andtelemetry. - ratchet — A governance assertion whose admitted population may shrink but not silently grow; owned by
tests/support/sweep-kit.ts; retiresallowlistandbaselinewhen shrink-only enforcement is meant. - sweep — A governance scan that enumerates a whole defect population and asserts its floor or emptiness; owned by
tests/support/sweep-kit.ts; retiresgrep checkandspot check. - pin — A test assertion that keeps a known site, count, or identity from moving silently; owned by
tests/support/sweep-kit.ts; retiressnapshotwhen a semantic location is meant. - admission — A recorded reason that permits a known exception into a governed population; owned by
tests/support/sweep-kit.ts; retiresexemptionwhen the entry is accepted as a positive capability. - exemption — A recorded reason that excludes a known non-member from a governance population; owned by
tests/support/sweep-kit.ts; retiresignoreandexception. - runner outcome — The classified result of a tool run: clean/findings, skipped, failed, or rejected; owned by
clients/dispatch/runners/utils/spawn-outcome.ts; retiresexit codeandtool failureas the user-facing classification. The classifier isRunOutcome; the older wording survives asToolFailureInput(tool-failure.ts).
Where two spellings are still live, use the more specific canonical term above in new text: diagnostic for a structured dispatch value and finding for the umbrella delivery concept; normalizeEphemeralMapKey for a process-local path key and workspaceHash (clients/mcp/ipc.ts) for a cross-process rendezvous derivation.
ADR: docs/adr/0001-stale-advisory-live-arm.md ADR: docs/adr/0002-workspace-hash-rendezvous.md ADR: docs/adr/0003-git-guard-latch-writer.md ADR: docs/adr/0004-disposition-policy-seam.md ADR: docs/adr/0005-tool-availability-enforcement-seam.md ADR: docs/adr/0006-derived-state-benchmark-first.md ADR: docs/adr/0007-end-to-end-witness-per-seam-slice.md ADR: docs/adr/0008-turn-end-lane-interface.md ADR: docs/adr/0009-reported-path-attribution.md ADR: docs/adr/0010-trace-validation-session-lifecycle.md
Recurring defect shapes
Screen against these before writing code. Shapes are numbered once, grouped by the surface they bite; each block loads only when its trigger applies. A shape whose open population is countable names its census in its entry. The census is one command, run at the repo root, printing one line per member or naming one baseline key. A PR that fixes members of the shape quotes that census before and after. A shape with no census in its entry is uncounted: a PR that fixes its members says so instead of quoting a before and after.
The full entries live in docs/defect-shapes.md: read the entry for any shape a brief or a review names. Each line here is its entry's screen, or its first sentence when it has none.
<!-- markdownlint-disable MD029 --><important if="touching a path key or path spelling">touching a path key or path spelling
- Divergent path keys: Screen: does any spelling reach
normalizeMapKeyorrealpathOrResolvethrough apath.resolvefirst? - Host path functions in a shape branch: once a path is classified as Windows-shaped, use
path.win32; do not use host-defaultpathfunctions. - Mixed path comparison: use one platform-aware containment expression; do not combine case-sensitive equality with case-folded relative paths.
- Walk-up result used as eligibility: return ownership and start-directory identity separately; enumerate root-position by ambient-input cells.
- Split host-path resolution: a path is resolved by its provenance, at one seam per provenance.
- Named path vs written set: a guard keyed on the path a tool NAMES must hold for every path the tool WRITES; enumerate the written set (a rename's importers, an
ast_grep_replacefolder or its project default, a server-initiatedworkspace/applyEditwith no call at all) and key the permission per call, never per path. - Unknown authorship range: a producer that cannot recover a truthful range must carry UNKNOWN to
ReadGuard.creditAuthorship; UNKNOWN creates no authorship.
</important><important if="adding or reading a cache, durable record, or project-intelligence state">adding or reading a cache / durable record
- Unsettled resource: every timer, worker, child, watcher, and loser path is unref'd or cleared on every settle path.
- Incomplete freshness: use the right content, size, mtime, dependency, and existence axes.
- One-axis bound: bound the resource axis that grows, including bytes, timers, WASM objects, and retained evidence.
- Out-of-guard mirror refresh: refresh behavior-gating mirrors before the guard releases, or validate the committed generation/object identity.
- Timer versus long operation: an operation holds a counted gate for its lifetime; a background timer checks it at fire time and re-arms a fresh, bounded delay.
- Cooldown beyond caller cadence: verify both recovery suppression and promotion of values served during cooldown.
- Second writer without discriminator: enumerate all writers and add a reason/kind field before composing branches.
- Cold-path work on warm path: compute expensive record fields only inside the failure or timeout branch, and measure any hot-path cost.
- Reset cap counter: a retire or skip records its decision where the selector reads it, so the same item cannot re-enter with a fresh count.
- Hot bound reached at p50: record hit rate and prefer adaptive or demotion behavior over a constant that has become the work.
- Long-lived container without a bound: module-level
Map/Setstate is bounded, evicted, or content-keyed; a reset or TTL read is not a bound. - Retry or drain loop consumes its own work list: a bounded retry or drain loop must not remove its tracked item from the collection it iterates on the first successful pass.
- Cross-request derived-state cache where a request-local pass is affordable: for a derived value on a hot path, measure a request-local bounded recompute first; persist it only when the fresh-process benchmark shows the recompute is the cost, and then the entry carries the generation it was derived from.
- Unlocked async read-modify-write of a shared record: Screen: does the write's value come from a read of the same target with an await between, and is any ordering field assigned outside the lock?
</important><important if="a delivery surface or lane">a delivery surface or lane
- Dropped side channel: trace flags, bindings, and provenance through spreads, maps, filters, and JSON serialization.
- Silencing as fixing: distinguish clean, filtered, unavailable, errored, suppressed, deferred, and partial results.
- Old-role filter on a substitute: compare fallback output with the substituted surface's contract, including non-blocking findings.
- Unredacted user content: parser messages and hand-authored strings may contain file input; normalize and redact at the shared diagnostic seam.
- Pull-only observability: new behavior emits a success or decision record in the streams that monitors and analyzers read.
- Swallowed failure recorded as a different event: Screen: force the failure through the real entry point and read the record it wrote.
- Prose mistaken for executable structure: define lexical states and reachability before scanning shell, workflow, or source text.
- Stand-in for unknown text stripped per consumer: when a lexer leaves a placeholder for text it cannot know (a command substitution's output), read the input under each bounding assumption rather than teaching each consumer to ignore the placeholder: empty (the text the rules matched before it existed) and opaque (no rule word matches it, no path resolves through it), denying if either reading denies.
- Whitespace counted as structure when it is alignment: a leading run can be alignment, not one nesting unit (call continuations, block-comment and template-literal interiors; #3038, #3039, #3052, #3059, #3116).
- One-direction filter proof: a filter that drops stale input is proven in both directions: it never passes stale input and never drops the only fresh answer.
- Prefix churn from a non-append write: Screen: drive two real-session requests that differ only in the writer, and compare the head and pi-ai's collapsed head.
- Tool-state prose blind to a posture: Screen: render the prose under each posture combination and each host, and assert every clause is true (#4293, #4294 enabled axis; #4682 lazy axis and the hand list that dropped two tools).
- Generated proof line for a step that did not run: Screen: for each proof sentence a composer emits, which input proves the step ran, and does the failed branch render something else?
</important><important if="a runner outcome or tool execution">a runner or tool outcome
- Wrong argv transform: verify a command is the wrapper shape before dropping an argv element or launcher name.
- Wrong failure classification: derive availability and verdicts from raw evidence; preserve the classifier and evidence when a caller asserts a fact.
- Unverified external-tool claim: probe the real binary before encoding exit codes, output shapes, severity names, or fixtures.
- Tool root drift: all runner, formatter, and LSP child spawns use
resolveToolCwd; mutation of the seam, log, or fallback must turn a test red. - Language-specific rule: use
LANGUAGESand registry facts; add a non-TypeScript row whenever the rule is language-neutral. - Fallback direction chosen without naming the user-facing obstruction: "fail closed" is not a universal justification.
- Unhandled stream or process event is a host-fatal throw: every callback on a child process, socket, or stream is total (catch, bound, record); a throw there bypasses the caller's
try/catchand kills the pi host (#3375, #3383, #3389). - Serial await of independent work on a hot path: Screen: do the calls this loop or sequence awaits one at a time need each other's results, share a write order, or go to one server that answers one request at a time? If none, overlap them through
clients/map-with-concurrency.ts; if one does, a comment at the loop names it. Measure before overlapping calls to one server. - Wasm failure outside the #3605 containment: every failure thrown out of web-tree-sitter classifies through
classifyTreeSitterWasmError, and acatchinside awithParsedTreeconsumer callsreportWasmAbortbefore it swallows the error. - Delete or rewrite through a dependency link: a lane's
node_modulesis a symlink to the main checkout's install, so any verb that removes or rewrites under it reaches every lane (#3173:git worktree removefollowed the link twice; #4044: a worker'snpm ci --dry-runemptied it under ~7 lanes, because npm 9.2.0 ignores--dry-runfor the clean-install family). - Tool console output parsed unstripped: parsing Vitest or any tool's console output: strip ANSI first, and test with
FORCE_COLOR=1. - Single scan root: Screen: for each detector, list the roots its subject can write in.
- Rule re-derived beside its owner: Screen: before writing a marker, pattern, bound or normaliser, grep the tree for its literal and for the matcher that applies it; does a module already own it, and does this caller route through that module?
- Learned attribution keyed on a name alone: Screen: can a call whose work already ran, or a tool that never wrote, reach the learned lookup or the arm gate on name plus path alone?
- Failure kind checked after the generic error branch: Screen: does every non-success exit reach the state updates the transition table gives it, or does a broader check catch it first?
</important><important if="a test double, ratchet or sweep">a test double, ratchet or sweep
- Vacuous test: prove the real entry point and real fixture arm.
- Name heuristic: a filename skip has an observable count and a content escape hatch; never silently drop a real file.
- Skipped CI as green: absent required checks are not passing checks.
- Duplicate module instance: tests import the same
.jsartifact as the runtime and never reset a private.tstwin. - Source assertion for runtime behavior: prefer a runtime probe; source scans need proof that runtime observation is impossible.
- Spelling enumerator: detect semantic structure, not a finite list of syntactic spellings, and test an unlisted spelling.
- Platform-only red: platform-dependent tests run under injectable
path.posixandpath.win32semantics on every authoritative lane. - Count-based baseline laundering: maintenance tools match content identity, not occurrence counts, and refuse replacement identities.
- Raw control byte: source fixtures encode control characters as escapes or buffers; tracked-source sweeps enforce this.
- Data-only admission: a new exemption or baseline row requires a reason in a separate checked file and a fixture that crosses the boundary.
- Portable entry-module check: decide the entry module only with
isMainModule(import.meta.url)fromscripts/lib/is-main-module.mjs;scripts/hooks/guard-bash.mjsis the one standalone gate. - Root wrapper drops metadata: wrappers preserve the complete marker table and are checked against direct root resolution.
- Test double's fabricated identifier reaching code that acts on it: a pid, fd, port, lock path or handle invented by a mock reaches production code that registers, signals, writes or deletes by it (#2042, #3091).
- Generated edit with unlisted consumers: Screen: apply the generator's real output to the real tree (all-eligible case included), build, and run every test that mentions the field or the registry; this sweep is manual (#3994 F1, r3).
- In-process module double with a real seam: when a
clients/module has a test setter, adopter, or reset, drive that seam; keep a double only at a process or host boundary. - Sink read before drain: a log sink's
log/appendonly enqueue, so a test that reads the sink's byte offset, line count, or rows while one of its own writes is queued counts late bytes on the wrong side of its window, or misses them. - Model switch drift: Screen: for a mapped file's diff, list the CONSTANTS in the family's as-built
formal/<family>/*.cfgand check each against the changed branch. - Enumerated detector: Screen: plant an alias, an indirect call and a value reached through another binding, and quote each result; every green plant is fixed or named as a residual.
- Fixture adopts an ancestor root: a fixture created with no project marker inherits whatever root sits above it, so one stray
.gitat the tmp root became the project root and #4559's quick-mode cell failed three pre-push runs. - Instrument attributes by id and order, not by the emitting subject: a probe that brackets each phase with an offset into a buffered async sink, and drains rows by subject id alone, folds a sibling's late row into its own phase.
- Cancellation read as failure: an aggregate that compares each shard's result against
successreds a required check when the shard finished every step and lost its race withtimeout-minutes, and no rerun recovers it, because the infra-kill classifier reads onlyfailureruns. - A dependency's cache directory attributed to the fixture that triggered it: a loader that materialises
os.tmpdir()/<tool>on the caller's own stack looks like a fixture sibling, so the creator recorded for it is accurate per process and wrong per cause, and the sweep reds a file that never created it. - Synchronous native-tree walk holds every tree: Screen: does the loop's body parse, and does it reach a
setImmediateor a real I/O await per iteration? - One home variable pinned, the rest left at the real home: Screen: which directory does the process I run resolve from, and is every variable that names it pinned?
- Default-budget tree census: Screen: does this case call a walker or read a binding one produced, and is its budget one of those two forms?
- Turn-count wait for real I/O: Screen: does the wait end on the event it waits for or on a real-clock deadline, never on a count?
- Test double with a result shape the real producer never returns: Screen: was the double's result shape copied from a real run of the producer, or driven through
tests/support/fake-child.ts, or written by hand?
</important><important if="session, turn or generation lifecycle">session, turn or generation lifecycle
- Process latch for session state: every once-latch has a session reset; session dedupe belongs in the degradation ledger where possible.
- Re-derived identity: carry resolved identity or correlation across asynchronous stages; do not reconstruct it from ambiguous later inputs.
- Staleness-only fallback: stale work is not proof of ownership; require origin provenance before claiming it.
- Late loser overwrite: concurrent writers carry a monotonic generation; mutation of the generation guard must turn a test red.
- Session-straddling write: capture the session generation before an await and check it before publishing.
- Advanced cursor predicate: predicates about the starting leaf receive the starting path, not the loop cursor.
- Module-scope uniqueness assumption: process evaluation can create multiple copies; process-wide registries and latches use
getProcessSingleton. - Load-time platform constant: use a live platform read or an isolated fresh import for every platform branch test.
- Field inherited across entry kinds: when a coalescing queue carries a field from a replaced entry into its replacement (a read stamp, a save flag), check every kind the replacement can be, not only the kind the fix was written for (#3491: a queued close inherited a stale touch's read stamp and the stale-read drop discarded the close).
- Subset without a population verdict: when a mechanism, policy, guard, or optimisation targets N of M members, name the excluded default and a generalization verdict; see
docs/pi-lens-reviewer.md(recurrence: #3622). - Released-writer input shapes: property-test generators include the input shapes produced by older released writers (#3594 R2-F1).
- Known identity carried forward: when a producer knows an identity, carry it through asynchronous stages instead of re-deriving it downstream (#3643 F3).
- Joined single flight answers for edits after it started: Screen: for a join, list the writers that can store the row between the join and the next scan, and show the finding survives each.
- One signal wired consumer by consumer: Screen: for a new detector, notifier or staleness marker, list every consumer that holds state derived from the same content, and show each one receives the change or record why not.
- Process-global last-write-wins session field: a field that holds one session's value for the whole process answers for whichever session wrote last, so a second live session overwrites what another one read.
- Factory-time read of a host value set later: pi applies an extension flag's command-line value after every factory has run, so a flag read while the factory runs answers the registered default. Screen: does any flag read run while the factory runs, and does the host test apply the value after the factory, as pi does?
</important><important if="availability policy or installer">availability or installer
- A second store answering the same availability question: a new latch, map or cache answering "can
<tool>run, at what path" beside the shared policy (availability-policy.ts,createAvailabilityLatch).
</important><!-- markdownlint-enable MD029 -->Standing invariants
The invariants hold across the tree. Each file below holds one seam's full text, opened by the condition that loads it; the third column names the paths it governs. Read the file for the seam a brief or a diff names.
| seam | file | source paths it covers |
| Language and configuration | docs/invariants/language-config.md | clients/language-registry.ts, clients/config-core/, clients/tool-config.ts, rules/, tools/ |
| Paths, data, and operating systems | docs/invariants/paths-os.md | clients/path-utils.ts, clients/file-utils.ts, clients/installer/index.ts, tests/support/ |
| LSP, trust, and process execution | docs/invariants/lsp-trust-exec.md | clients/lsp/, clients/safe-spawn.ts, clients/session-repository.ts, scripts/, formal/ |
| Dispatch, runners, formatters, and installers | docs/invariants/dispatch-runners.md | clients/dispatch/, clients/formatters.ts, clients/installer/, clients/test-runner-client.ts, tests/fixtures/runner-output/ |
| Caches, stores, and project intelligence | docs/invariants/caches-stores.md | clients/durable-store.ts, clients/content-stamp.ts, clients/review-graph/, clients/word-index.ts, tools/ |
| Session, telemetry, and delivery | docs/invariants/session-telemetry.md | clients/runtime-*.ts, clients/session-lifecycle.ts, clients/degradation-ledger.ts, clients/finding-delivery-gate.ts |
| Git guard and host adapters | docs/invariants/git-guard-host.md | clients/git-guard.ts, clients/inline-blocker-fields.ts, clients/deferred-runner-blockers.ts, clients/lens-engine.ts, index.ts, mcp/ |
Key source layout
index.ts pi host adapter and lifecycle wiring
mcp/ MCP adapter and IPC hook bin
clients/lens-engine.ts engine seam shared by host adapters
clients/runtime-session.ts session_start lifecycle
clients/runtime-tool-call.ts tool_call and read guard
clients/runtime-tool-result.ts tool_result and dispatch
clients/runtime-turn.ts turn_end and deferred delivery
clients/runtime-coordinator.ts session, sequence, and mutation state
clients/language-registry.ts language identity
clients/tool-config.ts tool registry and activation policy
clients/config-core/ config validation and resolution
clients/path-utils.ts path and walk seams
clients/file-utils.ts data directories and project files
clients/safe-spawn.ts subprocess seam
clients/degradation-ledger.ts bounded degradation state
clients/lsp/ LSP service, roots, waits, and coverage
clients/dispatch/ dispatch plans, runners, and policies
clients/durable-store.ts locked durable read/modify/write
clients/review-graph/ graph build, query, and persistence
clients/word-index.ts symbol index and persistence
clients/finding-delivery-gate.ts delivery inventory and freshness policy
tools/ model-facing pi tool handlers
tests/support/ shared fixtures, fault injection, and seamsUse module_report with blastRadius: true before and after production edits.
Use read_symbol or read_enclosing for bodies. Use LSP navigation as the
primary code-intelligence path; use AST search for semantic population sweeps.
<important if="tracing a host lifecycle hook or mutation seam">Lifecycle and mutation seams
The four primary host hooks are:
session_start: reset session state, rehydrate snapshots, start bounded background work, and defer config/LSP discovery.tool_call: classify mutations, apply read-guard preflight, register reads or pending writes, and capture a file's pre-edit linter seed (below) before the host tool runs.tool_result: record observed mutations throughRuntimeCoordinator.recordProjectMutation, then run format, autofix, LSP, dispatch, and bounded deferred work.turn_end: settle deferred work, deliver findings, persist bounded state, and run the test/actionable-warning drains. One-shot state a producer consumes for a part of the message (a retirement, a delivery count, a drained run) commits only when that part reaches the capped message; a cut part stays pending for the next turn (clients/turn-end/delivery-holds.ts, #3813). An item-bearing advisory with no queue to restore (knip, dead-code, call-graph impact) parks the items it showed on the coordinator for ONE re-offer, taken by the lane's next successful run and offered only while that run still reports them (#3901); count-and-pointer advisories (actionable and code-quality warnings) are not held, their report is the pull record. A turn end reads only the inline blockers its own session's writes recorded; the commit gate reads every one (git-guard invariants, #4921).- Only the write/edit
tool_resultpath may block the host;session_start,turn_end,agent_end,agent_settled, and read-onlytool_resultare bounded by the outer wall; new hook awaits register intests/config/hook-await-bounds.test.ts.
RuntimeCoordinator.recordProjectMutation is the one mutation bookkeeping seam.
Do not pair bumpFileSeq and change-log writes at a new call site. The mutation
bridge and opaque-write recovery feed this seam for non-native producers. A
write is stamped with its turn's session (deps.sessionId, else the
coordinator's), in the change log, the pipeline's log rows and the collect-later
answer it defers; telemetrySessionId is for coordinator-owned records.
Tier-4 mutation attribution is only for third-party tool names. Names in
clients/tool-config.ts's PI_LENS_TOOL_NAMES projection are never learned or
observed as generic edits, because one pi-lens tool may mix write and read-only
operations (for example lsp_navigation). MCP-only registry names remain
third-party names on pi and retain the bounded observation path. A provider's
display-only replay (isProviderReplayCall in clients/mutating-tool.ts,
keyed on the cursor-replay- id prefix) is neither armed for observation,
classified from a learned name, diffed by the opaque net, nor checked by the
read guard, built-in edit replays included, because the work ran before the
replay arrived. Its write credit passes advanceAuthorship: false.
The read guard keys all path state through its normalizer. It accepts Read,
search, LSP, bridge, bash-view, and authored-write evidence, but name-only
ls/find output is not file content. Partial edits consume preflight-approved
spans and never re-search stale bytes. Normalized oldText matches map back to
one raw span only when the normalized span has no length-expanding fold (for
example fi→fi); the existing end validation remains the final guard. When
oldText is absent, the preflight wording distinguishes an unchanged full-file
read binding from actual content drift (#4265). Authorship (writtenThisSession)
follows content identity (#4131): it holds the bytes the conversation last
wrote, stat only pre-filters the hash, and another writer's byte change ends
it at the next zero-read edit, bash write or drain on the file
(retireChangedAuthorship); no later write resumes it. A zero-read allow
over a whole-file authorship injects a whole-file read that names the
authorship's call, so the own edit's one-line read cannot shadow the rest
and a move keeps that read exactly when it keeps the authorship; a range
scope gets none. A mutation-bridge
write (no pre-write check) may create a first authorship but ends an
existing one; the observed replay alone advances it, its tool_call having
retired every file it may replay. The store holds at most 4096 files. A branch move keeps it
iff its write's tool result is on the branch (#3603). FileTime moves only over
bytes the conversation holds whole: process bridges, observed replays and
range bridge reads leave it (#3865).
</important>
## Commands and gatesUse a pinned home/data environment for probes and child processes.
npm run build compile in-place runtime twins
npm run build:dist build the published dist bundle
npm run lint tsc plus oxlint
npm run lint:js:tests required type-aware oxlint rules over tests
npm run fmt:check oxfmt gate
npm run knip unused-code gate (CI job `knip`, gating)
npm test serialized full suite
npm run test:targeted -- <paths> shared-slot targeted suite
npm run test:unit serialized unit suite
npm run test:integration serialized integration suite
npm run preflight local merge/preflight gates
npm run check:lockfile lockfile consistency, plus a libc field dropped against the lock a commit records (#5207)
npm run check:allow-scripts allowScripts policy vs the resolved lockfile (#1185)
npm run changelog:check rollup check: fragment area and shape (.changelog/README.md); check-changelog-fragments.mjs runs it standalone
npm run docs:rule-catalogs regenerate rule catalogs
npm run hygiene -- --dry-run inspect worktree/process hygiene
node scripts/ci-verdict.mjs <pr|sha> exact-head CI verdict
node scripts/gen-test-shard-weights.mjs --run <dir>... regenerate the Unit tests shard weights
node scripts/guard-bash-probe.mjs <matrix.jsonl> [--head <ref|file>] [--base <ref|file>]
[--lane linked|real|both]
real-hook corpus probe (#4071): per-row verdicts
against tests/fixtures/guard-bash-probes; --base
prints only the rows that changed head vs baseCI jobs, required contexts, the Windows leg, caches, smoke paths, shard weights and the pre-push base-range logic are in docs/ci.md. Two rules bind an agent who edits a workflow or a hook:
- A workflow job no pull request can run needs a registered reason in
tests/config/workflow-pull-request-reachability.test.ts, and the branch run (gh workflow run <file> --ref <branch>) quoted with its run id (#3043);scripts/check-pr-body.mjsenforces the quote per edited workflow, andWorkflow run unaffected: <file> — <reason>clears it only for a workflow with noworkflow_dispatchtrigger or an edit of comments and blank lines (#3085). - The stale-build guard (
tests/support/check-build-freshness.ts) rejects a missing or older compiled twin. Pre-push fails when its bounded test-lock wait times out (#3717);PI_LENS_PREPUSH_LOCK_SKIP=1is the only lock opt-out and is logged topre-push.log. CI stays the gate.
A red is "unrelated" only when node scripts/red-on-base.mjs <test files…>
[--base origin/master] [--repeat 3] reports RED-ON-BASE for every failing
test; paste its output. CAUSED-BY-CHANGE, INCONCLUSIVE, and ALL-GREEN are
not evidence of unrelated.
Never hand-edit generated .js or dist/. Open and close PR worktrees with
node scripts/pr-worktree.mjs open <PR|branch> [--merge|--head] and
close <path>; close unlinks a symlinked node_modules before removal
(#2704) and refuses the main checkout, a dirty tree, or a real node_modules.
Open refuses a destination inside a registered checkout (#3981); set
PI_LENS_WORKTREES_ROOT to a directory outside every checkout.
<important if="relocating project data, machine state, or telemetry">Data directories and logs
Project caches, snapshots, indexes, reports, and change logs use
getProjectDataDir(cwd). Machine state uses getGlobalPiLensDir(); telemetry
uses getGlobalPiLensLogDir(). PILENS_DATA_DIR relocates project state and
PI_LENS_HOME relocates machine state. Display paths through
displayProjectDataPath; do not spell a project-data path in agent text.
All loggers use createNdjsonLogger. Flush the specific logger before reading
its file (shape 67); graceful session_shutdown returns the shared bounded drain before
pi closes stdin or exits. Relevant logs are latency.log, sessionstart.log, cascade.log,
review-graph.log, read-guard.log, actionable-warnings.log,
extension.log, tree-sitter.log, and dispositions.log.
</important>
## Build, packaging, and releaseThe build, packaging, and release rules live in docs/pi-lens-release.md.
Test requirements
setupTestEnvironmentplants the shared.gitproject-root marker on every fixture, so a fixture cannot adopt a project marker above the tmp root.{ rootMarker: false }is the named opt-out for a cell that asserts marker-less resolution, and says why. No cell plants a second marker at a fixture root: the shared one already supplies it, and a non-recursivemkdirSyncthere throwsEEXISTinbeforeAll. The hygiene owner (tests/config/tmp-fixture-hygiene.test.ts) also judges new non-pi-lens-entries at the tmp root that a test process created; a sibling a file leaves after the owner'safterAllis out of scope for its census, so fix the producer rather than reading the line as clean.- A test that pins
HOME/USERPROFILEusespinFixtureHome(tests/clients/test-utils.ts): the home lives in the worker's scratch root beside its worker home, never inside a fixture root the harness deletes and never underPI_LENS_HOME, which production treats as its own housekeeping. A rawprocess.env.HOME = …pin in new test code is that shape (tests/clients/fixture-home-teardown.test.ts); the pin registers its undo with the harness, so a setup step that throws before the call site'sfinallycannot hand the next case a fixture directory.
Every logic change has relevant tests. New tests use fake clocks and
tests/clients/interleaving-kit.ts before real time, raw sleeps, or real child
processes. Real elapsed-time assertions belong in the serialized
wallClockBudgetInclude lane. Real LSP child tests belong in
lsp-spawn-heavy. Any admitted real spawn or timer carries the flake-shape
header, baseline row, and lane membership. A new vi.mock of a module outside
PROCESS_BOUNDARY_MODULES (tests/support/flake-shape-scan.ts), or a new
hand-rolled LSP client or service double, carries the flake-shape header for
its detector, a baseline row, and an ADMITTED_AFTER_BASELINE entry.
The real-pi harness defaults to --no-session; a persisted-session witness
opts into withRealPi({ persistedSession: true }), which pins --session-dir
under the probe home and uses pi's documented --continue flag for a second
process. Read lifecycle order from the real session_scope_transition rows
and dead-weight rows, not from the pi mock; the mock does not re-run the
extension factory or reproduce pi lifecycle ordering.
Two in-process pi tiers sit under that child lane. A case whose verdict
depends on what pi does with a handler's return value, what the model
receives, or how pi orders a session goes through one of them, never
tests/support/pi-mock.ts (#4631: the double kept a context list as
returned, pi rebuilt the system head, and every test passed). Tier 1,
tests/support/pi-runner.ts, loads the built extension into pi's real
ExtensionRunner: flags, tools, commands, declared registrations, and no
emit, by design. Tier 2, tests/support/pi-session.ts, runs a real
AgentSession with pi-ai's fauxProvider: turns, tool round trips, session
ids, and the request the model sees. By default it loads the built entry by
path, so a vi.mock or a test-side singleton never reaches the extension; its
opt-in inline-factory mode (startPiSession({ inlineFactory })) hands pi the
factory the test imported, so a vi.mock, a prototype spy or a module-scope
registry the test reads is the one the extension uses. Their header comments
state the APIs and limits. A new tier capability adds a case to
pi-runner.test.ts or pi-session.test.ts first.
pi-mock.ts stays only for a test file on PI_MOCK_IMPORTERS
(tests/support/pi-mock-importers.ts), whose row states one class:
process-boundary, internal-seam (the case interposes on a pi-lens clients/
module through vi.mock, which cannot reach the copy pi's loader builds, or
through a test-side copy of a process-global service the loaded copy does not
share), unproducible-payload (a payload a scripted provider cannot produce),
double-subject (the double is the subject), or real-ui-harness (the claim is
what the TUI shows or mounts, which no in-process tier can produce). The double
throws at load for a file not on the list, and the list only shrinks:
tests/config/pi-mock-importer-sweep.test.ts fails on a row whose file no
longer uses it and refuses a pending slice label. A row's class holds for the
whole file only while every case in it needs that class: a slice that edits an
admitted file re-checks it case by case, and its PR body records the split
(moved, or stays with the class). A file whose double replaces a spawned
process (a child spawn, a pipeline runner, or a language server binary) is
process-boundary, not internal-seam; a test-side copy of the in-process
clients/lsp service object stays internal-seam. The class counts reproduce
with grep -oE '^[[:space:]]*"[^"]*": *"[^"]+"' tests/support/pi-mock-importers.ts | sed -E 's/.*": *"([^"]+)"$/\1/' | sort | uniq -c.
A host claim names the host it ran on: the per-PR tiers run on the one
installed host (@earendil-works/pi-coding-agent, the dev pin), and the
certified spread is a manual or nightly run, never a per-PR gate
(node scripts/probe-pi-host-tiers.mjs after npm run build; its header and
tests/scripts/probe-pi-host-tiers.test.ts define host selection and
--require-all).
When the defect is an ordering of awaits on one seam (a coalescing queue, a
per-key serializer), or the seam has regressed before, write a scheduler
property with fc.scheduler() instead of one more replay:
tests/support/scheduler-properties.md, worked example
tests/clients/lsp/notify-queue-properties.test.ts.
Use tests/support/fault-injection.ts for wedged children, seam delays,
starved budgets, and gates; makeRunnerCtx, makeLspServiceDouble, and
makeRealRunnerEnv for dispatch, LSP, and rule behavior. Check tests/support/
before writing a helper: a host case uses a pi tier, and a JSON-lines sink is
read through readNdjsonRows (docs/pi-lens-fixer.md "Before code"). Mock only external binaries, network, host SDK
seams (not pi's own behaviour: see the pi tiers above), clocks, or fault
injection; an in-process store, registry or coordinator is never replaced by a
fake.
Test authoring screens:
- Enter through the production entry point, not a parallel helper path.
- Make unavailable prerequisites visible with
skipIf, never a bare return. EveryskipIf(process.platform …)names the lane that runs it or reads// lane: dev-box-only; prefer a cross-platform variant through the test's own seam when the divergence is a technique artifact. - Measure a platform skip for a case-variant fixture: probe the real filesystem for the collision first, and create the sibling fixture only after the probe confirms it (#3159).
- Pin the seam that broke, not a value supplied by the test.
- Make doubles depend on explicit arguments, never stack or caller inspection, and honour every input the production seam honours on the axis under test (a timeout, a budget, a generation).
- Restore env, timers, cwd, and module state; run the case in isolation.
- A case never reads state a sibling case or file wrote (a file, a latch, a
once-per-session claim).
Changed tests shuffled (advisory)runs a PR's added or changed test files alone and under two fixed shuffle seeds (scripts/ci-shuffle-changed-tests.mjs); the untouched tree is measured nightly byscripts/ci-shuffle-census.mjs(shuffle-census-nightly.yml; needsnpm run buildandnpm run build:distfirst). Its known members sit inscripts/shuffle-census-ratchet.json, which only shrinks: a new member reds, and an entry clean two nights running is deleted. A file whose subject IS the order its cases run in cannot be fixed by restoring state: thatdescribeopts out with{ shuffle: false }and a header saying why order is the behaviour under test, never the whole file, and is registered in the ratchet'sbyDesignrows (tests/scripts/ci-shuffle-census.test.tschecks both ways).tests/support/shuffle-opt-out-scan.tsadmits only the minimal marker: a spread, a computed key or a callee it cannot follow refuses. - A
vi.doMockis undone in the file's top-levelafterEach, not in the case that mocks it: a shuffle runs the next case on the leaked mock.vi.resetModules()does not clearglobalThisor pre-reset singletons, and everyawait import(...)after it is a second module copy, so import statically what the tool under test also reads. Per-case session cleanup callsresetSessionScopedStatefromclients/runtime-session.ts, rather than a hand-picked subset of resets; a test that proves a reset owner's own contract keeps the owner call and states why. - Keep performance bounds close to measured fixed and regressed values.
- Assert real behavior, not only mock calls or
not.toThrow. - Derive expected values independently; do not mirror production tables.
- Prefer behavioral assertions over snapshots.
- Name tests as declarative behavior, not hopes.
- Assert the guard's reason or sink record, not only its boolean outcome.
- A test that pins user-facing behaviour asserts the user-facing text or
effect, not only an internal status field. History:
HISTORY.md, 2026-10-14. - An admitted timing test states its contention model: it measures how long
the OS paused the worker, or asserts on CPU time rather than wall time, so a
descheduled worker cannot red it. History:
HISTORY.md, 2026-10-14.
Governance sweeps use tests/support/sweep-kit.ts, explicit source roots, and
comment/string-blanked source. Every sweep has a real floor and a checked
exemption reason. A population-deriving scanner fails closed: a needle hit that
matches none of its classified forms reds as an unclassified spelling with its
file and line, never a skip, and the scanner ships a case that plants one.
History: HISTORY.md, 2026-10-15. A detector that decides a test exists counts
only live declarations (testDeclarations in tests/support/sweep-kit.ts) in
files vitest collects (tests/support/vitest-collection.ts): a skipped, todo,
conditional (skipIf/runIf, an options key, a body skip) or uncalled test is
absent. A governance question with a data-flow answer is asked structurally, not
by extending a spelling list: tests/support/eviction-programs.ts owns the
eviction question over the S0 census seam's AST, admits four tabled forms, and
refuses everything else. New mock exports, fixture shapes, path rules, spawn
lanes, and durable fields must update their registered-or-fail coverage tests.
<important if="adding or changing a rule or analyzer">Rule and analyzer contracts
Ast-grep rules live under rules/ast-grep-rules/ and tree-sitter rules under
rules/tree-sitter-queries/; shared user rules use the same layout under
<PI_LENS_HOME>/rules/. The repo-only governance rules, which never ship,
live under scripts/governance-rules/rules/. Precedence is project > user >
bundled, and a shadowed rule is excluded from execution. Use AST patterns over regex where
possible. A rule with an unknown post-filter fails closed. Every shipped rule
has a real behavioral fixture; Java/Kotlin rules use the real CLI path because
NAPI lacks their grammars. The bundled ast-grep source census is recursive and
respects the same precedence.
The mutable rule corpus (project + user roots; bundled stays immutable per
process) has ONE identity seam, ruleCorpusFingerprintForCycle in
clients/custom-rule-locations.ts: a content fingerprint computed at most once
per dispatch cycle, keyed on getTurnId(), and shared by the tree-sitter
loader memo and the ast-grep source fingerprint, so both families refresh on
the same boundary. A per-call walk of that corpus is a measured regression
(#4212; the numbers are in HISTORY.md), and a per-cycle memo that omits the
turn identity never invalidates. A path that must see an edit inside its own
cycle passes force, which recomputes and republishes into the cycle; the
dispatch runner does, and its RuleCache key is the content fingerprint
recomputed per dispatched file. resolveBaselineSgconfig forces because #497
point 7 pins mid-session freshness for a spawned ast-grep LSP.
Tree-sitter queries compile against the grammar of the file, not the rule's language label. Alternative capture groups share capture names. An unsupported or blocked grammar produces visible bounded degradation, never a clean empty result.
</important>
## Commit, prose, issue, and observability conventionsCommit subjects use the repository conventional prefix, imperative mood, issue reference, and no trailing period. Non-trivial commits explain what and why in a wrapped body. Keep user-facing prose active, present-tense, concise, and consistent. Use sentence-case headings, Oxford commas, and no em-dash chains.
Issue bodies, labels, and priorities follow "Issue and PR design contract" and
"Issue triage & labels". A PR body follows .github/PULL_REQUEST_TEMPLATE.md
within the caps scripts/check-pr-body.mjs enforces.
Observability is part of correctness. Name the record, sink, ledger, or test that proves a change. If no telemetry is appropriate, state why. Keep records bounded and preserve the identity that distinguishes one degradation from another. A phase record's timer wraps only its own call; when two writers share one phase literal they share one stated semantic, not one writer's meaning attributed to the other's work (#3166 r1: a walk was reported as the policy phase).
<important if="triaging or labeling an issue">Issue triage & labels
Every issue should carry one TYPE label and at least one area: label.
- TYPE (pick one):
bug— broken behavior.feature— a net-new user or agent capability.enhancement— an improvement to an existing capability.documentation— documentation only.
- AREA (one or more, color
#0052cc):area:lsp,area:dispatch,area:installer,area:diagnostics,area:read-guard,area:project-intelligence,area:perf,area:observability,area:session,area:config,area:security,area:tests. - Reuse GitHub defaults as needed (
good first issue,help wanted,question,duplicate,wontfix). - New issues get labelled at creation with
gh issue create.
When a session touches the repository, triage open unlabelled issues. Assign one
honest priority: priority:p1 for release-blocking correctness, data loss,
crash, hang, or host impact; priority:p2 for normal contained work; and
priority:p3 for opportunistic polish or help-wanted work. Use the existing
labels from .github/labels.yml; never create labels only through GitHub.
</important>
<important if="changing host-mode or rendered UI behavior">Host-mode and UI rules
ExtensionContext.mode is read from the event context. Only tui supports raw
widgets. print and json suppress proactive user notifications; unknown
modes preserve existing behavior. Raw Component.render(width) output goes
through fitLine or fitLines from clients/tui-fit.ts. Never write directly
to the terminal from clients.
</important>
## ContributingBare-Node scripts import only .js/.mjs; type stripping is not assumed.
Every agent Bash call under Claude Code runs through
scripts/hooks/guard-bash.mjs (PreToolUse), which denies with its reason;
tests/scripts/guard-bash-hook.test.ts pins each rule, and CONTRIBUTING.md
"Local git hooks" is the human-facing version. Fix the command; never route
around it. The denied families: git stash outside the read-only list and
show; git reset --soft/--hard; a double-force git worktree remove, or
any remove over a symlinked node_modules or a path it cannot resolve
statically (every refusal names
node scripts/worktree-remove.mjs <path> [--force]; #3988); a mutating npm
verb where node_modules is a symlink out of the project, or a delete through
that link (rm node_modules without a slash or glob stays allowed; #4044); an
unpinned node probe loading clients/ or dist/; TMPDIR/TMP/TEMP aimed
at the harness home; bare pkill/killall patterns (#3556); worktrees,
clones, mktemp -d or a write under /tmp (#3526); a commit or push chained
after a check with ; or a pipe instead of && (#3471); and every hook bypass
(--no-verify, commit -n, core.hooksPath, HUSKY=0, PI_LENS_SKIP_HOOKS=;
#3778). kill $(pgrep -f …) is a known blind spot; kill your own recorded PID.
For a red that looks unrelated, prove it with node scripts/red-on-base.mjs and
stop.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

