UNPKG

pi-lens

Version:

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

286 lines (243 loc) • 24.8 kB
# Release-QA baseline matrix The feature × modality matrix a release candidate is witnessed against before a tag is cut. `scripts/release-qa.mjs` reads THIS FILE — the table below is the runner's row list, not a copy of one — exports the committed tree, packs and installs the candidate into a scratch `HOME`, drives each row's entry point against a real `pi`, and writes `release-qa-report.md` plus one witness file per row under `release-qa-evidence/`. Every child process the runner spawns — `pi`, the MCP server, `node`, `npm` — runs under a pinned environment: `HOME`, `USERPROFILE`, `PI_LENS_HOME`, `PILENS_DATA_DIR`, `PI_LENS_INSTALL_LOG` and `npm_config_cache` all inside the scratch root. `PI_LENS_INSTALL_LOG` is on that list by hard experience: it is what `scripts/warm-loader-cache.mjs` keys its install log on, with `PI_LENS_HOME/install.log` as the fallback when it is unset, and the runner's first six runs — which pinned the pi-lens home but passed no environment to `npm` at all — put 41 records into the maintainer's real `~/.pi-lens/install.log` (#2619 review F1). The pack runs in a `git archive HEAD` export, never the live checkout, because `npm pack` fires our own `prepack` (rewrites `package.json` + `package-lock.json`) and `prepare`. When regenerating the lockfile, use the exact npm version in `package.json`'s `packageManager` field (`npm@11.18.0`), matching the production-install CI job. (rebuilds `dist/`, downloads grammars, reinstalls git hooks). Why it exists: #2587. The four shipped skills were suspected of never registering for four releases because **no check ever asked a real pi what it loaded**. The unit suite (~10.8k tests) and the nightly smokes (install, compat, tool, lifecycle, parser) are per-seam; nothing composed them into a release verdict with counted coverage, so a missing row read as absence rather than arithmetic. ## Reading the table - **row id** — the runner's key. Every id here must have a probe in `scripts/release-qa.mjs`, and every probe there must appear here; the tie is enforced by `tests/scripts/release-qa.test.ts`, so neither list can drift into a hand-maintained mirror of the other. - **entry point** — the command or RPC a **user path** actually takes. Never a raw internal function: a row whose entry point is an internal call proves the function works, not the product. - **pass criterion** — the concrete thing the witness must SHOW (a status, a count, a named record). "It ran without throwing" is not a criterion. - **witness** — the artifact captured under `release-qa-evidence/`. A witness that merely exists is not a witness; the report quotes the line that shows the asserted result. - **reuse** — the existing script that already produces this witness, or the smoke that would have caught this row's regression. `new` means nothing in the repo covers it, and the cell says so rather than leaving the gap implicit. - **umbrella** — the open smoke-umbrella issue whose scope this row overlaps (#1605 lifecycle/real-host lanes, #1829 tool-contract lanes), with one line on where the two differ. `—` means neither umbrella claims this ground. This column exists so a reader can tell at a glance which rows are a down-payment on an already-filed umbrella and which are genuinely uncovered — and so the umbrella issues are not re-litigated row by row when they are eventually built. ## Modalities | modality | what it stands for | | --- | --- | | `npm-pack` | the published artifact itself — what `npm publish` uploads | | `npm-install` | a user installing the tarball into a project's `node_modules` | | `pi-rpc` | pi loading the installed package, driven headless via `pi --mode rpc` | | `mcp-stdio` | an MCP client speaking JSON-RPC to `dist/mcp/server.js` | | `git-install` | `pi install git:...` against a pushed ref | Static packaging shape under `npm-pack` is NOT a matrix row: CI's `ci.yml` `prod-install-build` job already runs `publint` (gating) on every PR's packed tarball (#2700, the check #2587 was missing). `attw` (arethetypeswrong) is not run there or here — the package ships no `.d.ts` at all (`tsconfig.dist.json` sets `"declaration": false`, no `types`/`exports` in `package.json`), so there is nothing for it to grade. ## The matrix | row id | feature | modality | entry point | pass criterion | witness | reuse | umbrella | | --- | --- | --- | --- | --- | --- | --- | --- | | pack-skills-payload | the four shipped skills and the compiled entry are IN the published artifact | npm-pack | `npm pack --json` on the release candidate | packed file list contains `dist/index.js` and at least 4 `skills/**/SKILL.md` | pack listing JSON | new — `tests/packaging.test.ts` asserts `files[]` NAMES `skills/`, never that the pack carries SKILL.md files | — | | install-selftest | the runtime dependency graph and the `pi.skills` manifest resolve AS INSTALLED | npm-install | `node <installed>/scripts/install-selftest.mjs --allow-soft` | process exits 0 and no `[FAIL]` line | selftest stdout | `scripts/install-selftest.mjs` verbatim — install-smoke's `smoke` job | #1605 lane 5 (real-host install), narrower: this row asserts the packaged artifact resolves, not that a host classifier parses | | skills-registered | a real pi registers the four pi-lens skills from the installed package | pi-rpc | `pi install <installed pkg>` then `pi --mode rpc` plus `{"type":"get_commands"}` | at least 4 commands with `source` `skill`; every `sourceInfo.path` inside the installed package AND no `sourceInfo.source` is `extension:index` (pi's `pi.skills` manifest is the only registrar; the extension's `resources_discover` handler used to re-add the whole directory and undo settings package filters, #1416) | get_commands response JSON | new probe on the #2589 mechanism — the recurrence is #2587; #1416 widened it to the manifest-registrar direction | — | | commands-registered | the extension loads and registers its `lens-*` slash commands | pi-rpc | same RPC session as above | at least 1 command with `source` `extension` named `lens-*`, and zero `extension_error` events | same get_commands response plus the event stream | `scripts/rpc-load-check.mjs` assertion — install-smoke's `pi-load` job, which runs it against the PUBLISHED package only | #1605 lane 1 (real-host): the same real-host principle, applied to extension registration rather than stderr classification | | mcp-tools-registered | the MCP mirror advertises the `pilens_*` tool surface | mcp-stdio | `node <installed>/dist/mcp/server.js` then `initialize` plus `tools/list` | every advertised tool name starts `pilens_`, and the set contains analyze, diagnostics, turn_end, lsp_navigation, health | tools/list response JSON | new — no smoke drives the MCP mirror from an install | — | | mcp-diagnostics-full | `lens_diagnostics` full mode answers on a fixture repo | mcp-stdio | `tools/call` `pilens_diagnostics` with `mode` `full` and `refreshRunners` `cheap`, POLLED | text carries a `Summary (N files diagnosed this session)` line with N at least 1 | tool result text | new — `tests/clients` covers the handler, nothing covers it through a packaged install | — | | mcp-turn-end | the turn-end pipeline runs over the turn's files and returns an advisory | mcp-stdio | `tools/call` `pilens_turn_end` with the fixture file | text carries `Turn-end over N file(s).` with N at least 1 | tool result text | new — `scripts/smoke-availability-lifecycle.mjs` covers lifecycle availability, not the packaged turn-end path | #1605 lane 2 (availability-lifecycle): overlaps the turn-end half; #1605 asserts latch recovery, this row asserts the packaged turn-end path answers at all | | mcp-lsp-navigation | LSP navigation answers on a fixture | mcp-stdio | `tools/call` `pilens_lsp_navigation` with operation `documentSymbol` and the fixture path | result is not an error and names the fixture's exported `releaseQaFixtureSymbol` | tool result text | `scripts/smoke-tools.mjs --lsp` is the per-server sibling; this row is the packaged-path variant | #1829 lane 2/3 (gated real-binary + the rotating install lane): #1829 pins each tool's own contract, this row pins that the packaged LSP path answers | | config-provenance | a project config is LOADED and its provenance is reportable | mcp-stdio | `tools/call` `pilens_effective_config` with the fixture file | result names the fixture's `.pi-lens.json` as a contributing document | tool result text | new — `tests/config/pi-lens-config-schema.test.ts` covers the schema, not the packaged load | — | | degradation-visible | a silently-ignored input is RECORDED as a degradation instead of vanishing | mcp-stdio | `tools/call` `pilens_health` with the fixture's project-tier `lsp.enabled` (a global-only setting) loaded | health text carries a `config-ignored` degradation line naming the fixture's `.pi-lens.json` | health tool result text | `clients/degradation-ledger.ts` is the reused machinery; no smoke asserts it end to end | #1605 lane 2 (availability-lifecycle): the degradation-recorded half; #1605 additionally asserts RECOVERY, which this row does not | | global-config-location | the agent-dir global config file supplies the global tier when it exists and the legacy default does not | mcp-stdio | `tools/call` `pilens_effective_config` on a real pi-lens MCP server whose env sets `PI_CODING_AGENT_DIR` at a scratch dir holding `extensions/pi-lens.json` | the tool result NAMES the agent-dir file as a contributing config document (the `pi-coding-agent-dir` winner) | tool result text | `tests/clients/global-config-location.test.ts` covers the resolution in-process; no smoke drives the packaged MCP path with the env set | — | | config-shadow-record | a lower-precedence global config file beside the winner is recorded once per session | mcp-stdio | `tools/call` `pilens_effective_config` then `pilens_health` twice, on a real MCP server whose env has BOTH `~/.pi-lens/config.json` and `PI_CODING_AGENT_DIR/extensions/pi-lens.json` present | health carries a `config-location-shadowed: 1` line naming the shadowed agent-dir file, and the count stays 1 on a second health read after a further config load | the two health tool result texts | `clients/degradation-ledger.ts` `recordDegradationOnce` and `tests/clients/global-config-location.test.ts`; no smoke asserts the record end to end | #1605 lane 2 (availability-lifecycle): the degradation-recorded half, as `degradation-visible`; this row additionally pins the once-per-session count across two loads | | git-install-loads | a `git:` install of a pushed ref builds and loads in a real pi | git-install | `pi install git:github.com/apmantza/pi-lens@<ref>` then `get_commands` | at least 1 `lens-*` command and at least 4 skills | get_commands response JSON | `scripts/rpc-load-check.mjs` assertion, re-run against the git layout | — | | publish-toolchain-pinned | the RELEASE WORKFLOW's publish job runs the npm it pins, and that npm validates the tarball | npm-pack | `npx -y "npm@<packageManager pin>" --version` then `npx -y "npm@<packageManager pin>" publish --dry-run`, both in the scratch export | the pinned invocation reports the pin version AND the dry run exits 0, or npm reports the version is already published after packing; a pinned invocation resolution or registry failure leaves the row UNMEASURED | the two commands with their output | new — `tests/config/release-npm-pin-gate.test.ts` pins release.yml's TEXT; this row is the only thing that RUNS the publish job's toolchain before a tag exists (#2940) | — | | codemode-nested-guard | a nested edit-without-read made through pi's codemode tool is blocked by read-guard, and the turn_end check for a licensed nested edit still reaches a later request | pi-rpc | `pi --mode rpc --no-session --no-lsp --provider scripted --model harness -e tests/fixtures/real-harness/scripted-provider.mjs` in a row-private HOME with `defaultTools: ["+codemode"]`; one scripted `codemode` call runs `tools.edit` on an unread `c.ts`, then `tools.read` + `tools.edit` (adding `debugger;`) on `b.ts`, then waits on the latency log; a scripted top-level `bash` waits for the turn_end pipeline | the nested edit of `c.ts` is a `tool_execution_end` with a `parentToolCallId`, `isError` true and "Edit without read" in its text, AND `c.ts` on disk is unchanged; the nested edit of `b.ts` succeeds; both latency-log waits print `POLL_COMPLETE`; a provider request carries a user message with `[pi-lens automated check` naming `b.ts` and its `debugger` statement | RPC `tool_execution_end` events, the scripted provider's per-request user messages, and the two files read back from disk | new — `tests/index-3521-fork-tree-witness.test.ts` drives the real runtime without codemode; nothing drove a nested call through the hooks until #3805 | — | | tool-smoke-install | every npm/pip entry in the installer registry resolves on a real install | npm-install | `node <export>/scripts/smoke-tools.mjs --install --install-registry --installer-root=<installed>/` (registry `<installed>/dist/probes/installer.js`; about 1m 29s cold on this box for 33 entries; harness from export root) | the report shows every npm/pip entry resolved or a named legitimate skip (toolchain absent, declined), and no genuine install failure; a registry-unreachable classification leaves the lane UNMEASURED; requires network access to the npm and pip registries | install-registry JSON report | `classifyInstallOutcome` from the #2661 fixture lanes — this lane sweeps the whole npm/pip registry, where fixture lanes exercise only the entries their fixtures name | — | ## Why `publish-toolchain-pinned` runs on every candidate #2940: 9183f39c6 moved `release.yml`'s npm pin from a global install to `npx -y "npm@<pin>"` and left `npm publish` bare, so the publish job ran Node 22's bundled npm — no OIDC trusted publishing — and the v4.1.6 run created the tag and the GitHub release before the registry answered E404. The `prepare` job's `--dry-run` publish could not see it: it ran a different npm. A `release.yml` change since the last tag is the obvious trigger for this row, and it is the one the skill's DIFF-row step (step 2) resolves to. The row runs on **every** tree run anyway, because the trigger has a hole: the pin's VALUE lives in `package.json`'s `packageManager`, not in the workflow, so a pin bump changes exactly the toolchain the publish job uses while leaving `release.yml` untouched. A release candidate whose publish path was never executed is the whole defect; gating the row on the workflow file would reproduce it one `packageManager` bump later. The row is SKIPPED — never a verdict — under `--from npm:<version>`: there is no exported tree to publish, and a dry-run publish fires this package's own `prepack`/`prepare`, so it may only ever run in the scratch export. ## Why `codemode-nested-guard` needs a scripted turn, and what it does not time Every other row is model-free. This one cannot be: a nested call exists only inside a model's `codemode` tool call, and pi's hooks for it (`tool_call`, `tool_result`, `parentToolCallId`) are reachable no other way. The model is the repo's scripted provider (`tests/fixtures/real-harness/scripted-provider.mjs`), loaded into the REAL pi with `-e`; it emits the tool calls a model would, and everything under test -- codemode's script host, pi's tool pipeline, pi-lens's hooks -- is the shipped code. The provider is not a stub of pi-lens's own assumption: a pi that stopped routing nested calls through the hooks fails the row. The row is SKIPPED, never PASS, on a pi older than 0.99.0 (no codemode tool) and on a pi whose own `pi-ai` cannot be located from the binary (the provider must be built on the host's stream class, not this repo's devDependency). What it deliberately does not time: pi-lens's dispatch races turn_end's bounded wait on a loaded box (#3796), for top-level edits as much as nested ones. The scenario therefore warms the first dispatch up with a throwaway edit of `d.ts` and waits, inside the scenario, for the `b.ts` dispatch and then for the turn_end pipeline by reading the latency log; both waits are capped, and an expired wait is UNTESTED, never PASS. pi does not hold the next request for turn_end either, so the check is required on a LATER request (the turn after a scripted `bash` that waits for turn_end), not the immediate next one. ## The supported pi window (#2682, #3805) pi-lens declares both host packages, `@earendil-works/pi-coding-agent` and `@earendil-works/pi-tui`, as optional peers with the range `"*"`. That is intentional and is pi's own rule for host-provided packages: pi warns when an extension lists one any other way, and a narrower pi-tui range turned a raw `npm i` with a current pi-tui at the top level into a hard ERESOLVE while `pi install` (which runs `--legacy-peer-deps`) showed users nothing. The window pi-lens actually supports is therefore not in `package.json`. It is `PI_HOST_SUPPORTED_RANGE` in `.github/workflows/install-smoke.yml` (the newest-in-range lane reads it, bounded above so `"*"` can never mean `@latest`), and `tests/packaging.test.ts` pins it against the hosts this matrix has passed on. A newer host enters the window in ONE change: run this matrix on it, add it to that test's verified list, and move the workflow's ceiling together. P1 is one verified host per minor line from 0.86 onward; 0.81–0.83 are in-window without a dedicated host, while 0.80, 0.84, and 0.85 are represented by the existing host list and ratchet. A ceiling past an unrun minor still claims that minor, so the verified-minor ratchet names the admitted lines explicitly. ### Verified hosts The certified policy is `>=0.80.10 <1.2.0`. Each listed minor line has a release-QA witness; the workflow ceiling and `tests/packaging.test.ts` list are updated together. | host | date | pi-lens commit | result | | --- | --- | --- | --- | | 0.86.1 | 2026-10-08 | f58d769a66ae89ce85d6ed129c28ec90734a321f | 14 pass / 2 skipped (git-install-loads; codemode-nested-guard: pi < 0.99) | | 0.87.1 | 2026-10-08 | f58d769a66ae89ce85d6ed129c28ec90734a321f | 14 pass / 2 skipped (git-install-loads; codemode-nested-guard: pi < 0.99) | | 0.99.2 | 2026-10-08 | 08abd60e193e5a17ea28dd4fbf3603cc9a0cdbe0 | 15 pass / 1 skipped (git-install-loads) | | 1.0.4 | 2026-10-08 | 08abd60e193e5a17ea28dd4fbf3603cc9a0cdbe0 | 15 pass / 1 skipped (git-install-loads) | | 1.1.0 | 2026-10-08 | 2fda17a53d62267cd9035fa04380bbf0e2de4318 | 15 pass / 1 skipped (git-install-loads) | Re-run any row with `node scripts/release-qa.mjs --pi <host>` at the listed commit. ## Why `skills-registered` pins the registrar pi-lens's skills are declared by the `pi.skills` manifest (`./skills`) and were ALSO registered by `index.ts`'s own `resources_discover` handler (#205, `resolvePackagePath(import.meta.url, "skills")`), which never reads the manifest. #2587 is the proof that one half can be broken for four releases while the other silently covers for it — driving published pi-lens 4.1.3, with the broken `["../../skills"]` manifest, through a real pi registers all four skills, via `extension:index`. #1416 then made the second registrar a defect rather than a safety net: pi applies the settings package filters (`packages[].skills`) only to manifest-declared resources, and merges a handler-contributed path raw, so a user's `-skills/<name>/SKILL.md` was re-added at bind time. The handler now contributes no skill paths, the manifest is the only registrar, and the row asserts `sourceInfo.source !== "extension:index"` on every skill, not merely that four skills appeared. A surviving `extension:index` skill means a raw handler contribution came back and the filters are being undone again. The observed registrar values are printed in the witness either way, so a pi-side rename reads as a diagnosable mismatch rather than a mystery. ## Outcomes Every row ends in exactly one of four states, and the four partition the discovered set — the report asserts the identity `discovered = pass + fail + untested + skipped` and says ARITHMETIC MISMATCH if it does not hold. The printed arithmetic is `discovered / rows / untested`, and the three words mean different things: - **`discovered`** — rows in the matrix above. The denominator. - **`rows`** — of those, the ones this run actually DROVE a probe for. A row the runner has no probe for is not counted; a BLOCKED run drives none, so `rows` reads 0. A SKIPPED row IS counted: its probe ran and decided the row unreachable. - **`untested`** — rows that produced no witness. - **PASS** — the witness shows the pass criterion. - **FAIL(cause)** — the witness shows something else. The cause is the observed value, not a category. - **UNTESTED(reason)** — no witness was produced. An async row that did not reach a terminal state before its polling cap expires **UNTESTED, never PASS**; a row with no probe implementation is UNTESTED too, so an unimplemented row is arithmetic rather than silence. - **SKIPPED(reason)** — the row is unreachable in this run by construction (a `git:` install with no ref given, for instance). Reachability is decided in planning, not discovered mid-run. **BLOCKED is about the HOST, not the candidate.** The runner probes `pi --mode rpc` **before anything is installed**; only a pi that cannot start a bare session makes the run BLOCKED, because then nothing about the candidate was measured. A candidate that will not pack, will not install, or stops a working pi from booting is a **result** — DO-NOT-SHIP — not an untestable state. A blocked run is not a failed run and must never be reported as one. **A run that witnessed nothing is INCONCLUSIVE, never ship-with-caveats.** If pi booted and no row reached PASS, "no row disagreed" is not evidence; the run gets no ship verdict either. Ship line, from the outcomes: | condition | verdict | exit | rows | | --- | --- | --- | --- | | pi did not boot with NO candidate installed | BLOCKED — no verdict | 3 | 0 | | pi booted, the candidate would not install or activate | do not ship, cause named on the verdict | 1 | 0 | | any row FAILED | do not ship | 1 | N | | any row classified registry-unreachable (its lane UNMEASURED) | INCONCLUSIVE — no verdict, the skips are not green | 3 | N | | pi booted but zero rows PASSED | INCONCLUSIVE — no verdict | 3 | N | | any UNTESTED or SKIPPED, at least one PASS | ship with caveats, each named | 2 | N | | all PASS | ship | 0 | N | A candidate that never activated leaves **every row UNTESTED, none FAILED, and `rows` at 0** — no probe ran, so nothing was witnessed and nothing can honestly be called a failure of that row. The activation failure is the verdict's own cause. (An earlier revision copied it onto all eleven rows as FAILs and wrote an empty evidence dir — a verdict with no witness, against the first hard rule.) **A row is PASS only if its witness has something in it.** A probe that reports success and produces no artifact — or an EMPTY one — is recorded UNTESTED, not PASS, and the report's excerpt column carries the downgrade reason rather than the pass line. The empty case is the reachable one: every probe attaches a witness object on its pass path, but `install-selftest` passes on "exit 0 with no `[FAIL]` line", which a packaged selftest that printed nothing satisfies vacuously, and its witness is that same empty stdout. The check is on CONTENT, not on the presence of a file. A usage or self-check error (bad option, unparseable matrix, arithmetic mismatch) exits **4**. **Exit 2 is the EXPECTED verdict for a working-tree run**: `git-install-loads` is SKIPPED without `--git-ref`. A CI lane should treat 2 as a warning and 1/3/4 as failures. ## Deliberately out of scope - **Anything needing a real model turn.** Every row above is model-free by construction except `codemode-nested-guard`, whose turn is scripted (see "Why `codemode-nested-guard` needs a scripted turn"); `get_commands` and the MCP tool calls never reach a provider. A row that needs a real LLM turn cannot be a release gate on an unfunded key, and a stubbed turn that stands in for pi-lens's own behaviour would be a double that mirrors our own assumption (AGENTS.md, external contracts). - **The `concurrent_session_bind` guard.** Observing it needs a second in-process `createAgentSession()`, which needs model config. `docs/subagent-compat.md` carries the same TODO; duplicating it here would add a row that can only ever be UNTESTED. - **Per-language tool and LSP coverage.** `scripts/smoke-tools.mjs` sweeps the whole registry nightly. This matrix asserts the packaged path answers at all, not that every server answers well.