UNPKG

pi-lens

Version:

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

362 lines (321 loc) • 26.1 kB
# Subagent-extension compatibility (#476) pi-lens's subagent-compatibility features — #475 (subagent light mode), #474 (instance registry + orphan LSP reaper), and #473 (concurrent-session guard) — were all built on **reverse-engineered facts** about two third-party pi extensions and the pi SDK itself. Nobody has promised us these stay true across their releases. This doc records exactly what we depend on, where, and how the nightly `compat-smoke` workflow (`.github/workflows/compat-smoke.yml`) verifies it. See issue #476 for the design rationale. ## Pinned contracts Versions below are what was installed and verified while building this smoke (2026-07). Re-verify against current versions any time the nightly alerts — `scripts/compat-contracts.mjs` prints the versions it actually installed on every run. | # | Contract | Depended on by | Third-party file (as of the versions below) | Verified against | |---|----------|-----------------|-------------------------------------------------|-------------------| | 1 | `PI_SUBAGENT_CHILD` is set to the literal string `"1"` on every process that hosts a child session. | `clients/subagent-mode.ts` (`isSubagentSession()`, `getSubagentIdentity()`) | `pi-subagents@0.34.0` — co-located in `src/runs/shared/pi-args.ts` (`SUBAGENT_CHILD_ENV` const + the `env[SUBAGENT_CHILD_ENV] = "1"` assignment); as of `pi-subagents@0.66.0` the const moved to `src/runs/shared/child-runtime-config.ts` and the assignment to `src/runs/background/subagent-runner.ts` (#2581); as of `0.70.0`, the published files are `src/runs/shared/child-runtime-config.js` and `src/runs/background/subagent-runner.js` — `compat-contract-locator.mjs`'s `locateContractSources` tries all layouts and concatenates whichever resolves. | `checkNicobailonChildEnv` | | 1b | avtc-pi-subagent sets `PI_SUBAGENT_CHILD_AGENT` + `PI_SUBAGENT_PARENT_PID` (never `PI_SUBAGENT_CHILD`) on the per-spawn subagent env; `isSubagentSession()` treats the PAIR (both non-empty) as an additional subagent signal (#507). | `clients/subagent-mode.ts` (`isSubagentSession()`, `getSubagentIdentity()`) | `avtc-pi-subagent@1.0.3` — `src/process-runner.ts` (`subagentEnv.PI_SUBAGENT_CHILD_AGENT = agent.name` + `subagentEnv.PI_SUBAGENT_PARENT_PID = String(process.pid)`) | `checkAvtcChildEnv` | | 2a | The pi SDK's extension loader keeps a **process-global** cache (`extensionCache = new Map()`). This is what makes an in-process `bindExtensions()` reuse pi-lens's own module-scope singletons instead of a fresh isolated instance. | `clients/session-lifecycle.ts` (the whole premise of the concurrent-session guard) | `@earendil-works/pi-coding-agent@0.80.6` — `dist/core/extensions/loader.js` | `checkSdkExtensionCache` | | 2b | `AgentSession.bindExtensions()` **unconditionally** emits a `session_start`-typed event (`this._extensionRunner.emit(this._sessionStartEvent)`). | Same as 2a — this is why an in-process subagent bind re-triggers pi-lens's `session_start` handler at all. | `@earendil-works/pi-coding-agent@0.80.6` — `dist/core/agent-session.js` (`bindExtensions()`, ~line 1717) | `checkSdkBindExtensionsEmitsSessionStart` | | 2c | `_extensionRunner.invalidate(...)` is called from the sequential session-replacement path (`newSession`/`fork`/`switchSession`/`reload`'s dispose route), never from a concurrent sibling bind. | `clients/session-lifecycle.ts` (`probeCtxActive()` — the asymmetry this whole guard relies on) | `@earendil-works/pi-coding-agent@0.80.6` — `dist/core/agent-session.js` (~line 551) | `checkSdkInvalidateCalled` | | 2d | The stale-ctx error thrown by an invalidated context's accessors contains the literal fragment `"stale after session replacement"`. | `clients/session-lifecycle.ts` (`probeCtxActive()` matches on this exact fragment) | `@earendil-works/pi-coding-agent@0.80.6` — `dist/core/agent-session.js` (the `invalidate(...)` message string) | `checkSdkStaleCtxMessage` | | 3 | tintinweb's subagent runner constructs a `DefaultResourceLoader` and calls `session.bindExtensions(...)` on a freshly created `AgentSession`, **inside the same Node process** as the parent pi session. | `clients/session-lifecycle.ts` (the concurrent-secondary case #473 exists to protect against) | `@tintinweb/pi-subagents@0.13.0` — `src/agent-runner.ts` (`new DefaultResourceLoader({...})` ~line 433, `await session.bindExtensions({...})` ~line 597) | `checkTintinwebInProcessBind` | All seven checks live in `scripts/lib/compat-contracts.mjs` as pure, unit-tested regex matchers against RESILIENT semantic shapes (never a line number — those drift on every third-party release). `scripts/compat-contracts.mjs` is the orchestration script: it `npm install`s the four packages (SDK, `pi-subagents`, `avtc-pi-subagent`, `@tintinweb/pi-subagents`) into a scratch directory, resolves each contract's source file(s) independently (see "Layer A" below) and runs every check. **Contract 1's identity vars were removed upstream, not relocated (#2581).** The doc used to also pin `PI_SUBAGENT_RUN_ID` / `PI_SUBAGENT_CHILD_AGENT` as part of contract 1 — nicobailon/pi-subagents set them alongside `PI_SUBAGENT_CHILD=1` for best-effort child identity. `pi-subagents@0.65.0`'s rewrite to run subagents as native, in-process `AgentSession`s (instead of spawning a separate `pi` CLI process per child) deleted both vars from the package entirely — grep-verified absent from the whole 0.66.0 source tree; child identity now travels through an in-process `ChildRuntimeConfig` object that never touches `process.env`. This is real, permanent upstream drift, but it needed no pi-lens code change: `getSubagentIdentity()` was already documented and tested as best-effort, returning `runId`/`agentName: undefined` when the vars are absent (`tests/clients/subagent-mode.test.ts`) — exactly the degraded state this vocabulary is now always in. `checkNicobailonChildEnv` no longer requires them; only the `PI_SUBAGENT_CHILD='1'` flag pi-lens's light-mode detection actually depends on behaviorally still gates the check. ## The three env levers | Env var | Default | Effect | |---------|---------|--------| | `PI_LENS_SUBAGENT_FULL` | unset (light mode auto-detects) | Set to `1` to force full (non-light) behavior even inside a detected subagent child session — nicobailon/pi-subagents (`PI_SUBAGENT_CHILD=1`) or avtc-pi-subagent (`PI_SUBAGENT_CHILD_AGENT` + `PI_SUBAGENT_PARENT_PID` pair, #507) — disables the light-mode heavyweight-scan skip for either vocabulary. | | `PI_LENS_CONCURRENT_SESSION_GUARD` | unset (guard enabled) | Set to `0` to disable the #473 concurrent-session guard entirely — every `session_start` classifies as sequential replacement (pre-#473 behavior: a concurrent in-process bind would run the full reset, tearing down the parent's live LSP fleet). | | `PI_LENS_INSTANCE_REGISTRY` | unset (registry enabled) | Set to `0` to disable the #474 cross-process instance registry — no orphan-LSP reaping happens at `session_start`, but also no new risk (registry writes are best-effort and already fail open). | ## What each smoke layer asserts ### Layer A — pinned-contract verification (`scripts/compat-contracts.mjs`) No `pi` process, no LLM turn. Installs the real packages (table above) and mechanically re-checks all seven contracts against the installed source. Each contract resolves independently to one of **three outcomes**, printed per-contract and rolled up into a run-level `outcome` (also exposed as a GITHUB_OUTPUT for the workflow's alert step, distinct from the GH step's own success/failure) and exit code: - **verified** (exit 0 when every contract is this) — the contract's source file(s) were found and their content matches the pinned shape. - **drift** (exit 1) — the source file(s) were found, but the content no longer matches. This is an actual upstream behavioral change worth investigating (see "What to do when the nightly alerts" below). - **infra** (exit 2, when nothing drifted but at least one contract hit this) — the contract's source file could not be located at ANY of its known candidate paths (each `CONTRACTS` entry's `parts` field in `scripts/lib/compat-contracts.mjs`, resolved via `compat-contract-locator.mjs` and `compat-contract-resolution.mjs`), or the `npm install` of the four packages itself failed. This means Layer A has NOT actually re-checked that contract's content at all — it is distinct from drift and must never be reported as one. #2581: before this three-way split, a single relocated file (`pi-subagents@0.65.0` moving `src/runs/shared/pi-args.ts`) threw inside one shared try/catch, aborted the whole run, and got reported — both in the run log and the tracking issue's body — as generic "failure" alongside the other six contracts that were never actually re-checked that run either. A contract's own outcome only becomes "infra" when ITS candidate paths are all missing; every other contract is still resolved and checked independently in the same run. Candidate paths are walked NEWEST-observed-layout first, not oldest (#2680 F1) — a stale leftover file at an old path (left behind by a partial publish, or an npm install that added files without pruning ones the package's current `files` list no longer references) must never outrank the package's actual current layout. When a candidate path list needs a NEW entry (the package relocated the file again), APPEND it to that contract's `parts` in the `CONTRACTS` list (`scripts/lib/compat-contracts.mjs`) — oldest-observed-layout first in how the list reads, rather than replacing the old entry — the locator resolves the newest EXISTING one regardless of authored order, so older installs stay covered too. `parts` and `package` live directly on each `CONTRACTS` entry, not a second table keyed by a string id (#2680 F2) — there is nowhere else to update. ### Layer B — real-pi behavioral smoke (`scripts/compat-smoke-behavioral.mjs`) Installs the packed pi-lens tarball into a real `pi` (the same "pi-load" mechanism `.github/workflows/install-smoke.yml` already uses) and drives `pi --mode rpc` so `session_start` fires and pi-lens loads — no LLM turn needed, matching `scripts/rpc-load-check.mjs`'s no-model-required design. **Every invocation sets `PI_LENS_STARTUP_MODE=full`.** pi-lens's cold-start-quick optimization (see AGENTS.md "Session-start critical path") forces the *first* `session_start` of any process onto the fast "quick" path regardless of `--print` — that path returns before the subagent-light -mode check (and the heavyweight-scan skip it gates) ever runs. This was confirmed empirically while building this smoke: without the override, the "quick mode active" line appears in `sessionstart.log` and no `subagent_light_mode` phase is ever logged, producing a false negative. With `PI_LENS_STARTUP_MODE=full`, the phase logs deterministically on the very first session. Assertions: 1. **Subagent light mode engages** — with `PI_SUBAGENT_CHILD=1` set, `subagent_light_mode` is logged to `$PI_LENS_HOME/latency.log` (a `type: "phase"` entry) and none of the seven heavyweight-scan phases (`knip`/`jscpd`/`madge`/`dead-code`/`govulncheck`/`gitleaks`/`trivy`) are logged for that run. 2. **`PI_LENS_SUBAGENT_FULL=1` overrides it off** — with both `PI_SUBAGENT_CHILD=1` and `PI_LENS_SUBAGENT_FULL=1` set, no `subagent_light_mode` phase is logged for that run. 3. **Zero surviving LSP-server processes after a clean exit** — pi is asked to shut down gracefully (closing its RPC stdin, which pi's own RPC mode treats as a shutdown trigger — NOT a `SIGKILL`, which would skip `session_shutdown` and make this assertion meaningless). A grace period (a few seconds) is given for the async teardown (`session_shutdown` → LSP fast teardown → child `SIGTERM`) to complete, then the process table is diffed against a pre-run snapshot for any *new* process whose command line matches a narrow LSP-server marker list (`typescript-language-server`, `ast-grep lsp`, `pyright-langserver`, …, `scripts/lib/process-scan.mjs`). This is the #472 orphan class #474 fixed. 4. **avtc-pi-subagent PAIR engages light mode** (#507/#518) — with only `PI_SUBAGENT_CHILD_AGENT` + `PI_SUBAGENT_PARENT_PID` set (no `PI_SUBAGENT_CHILD`), `subagent_light_mode` is logged and none of the seven heavyweight-scan phases are — same detection as assertion 1, exercised through the second vocabulary. 5. **avtc-pi-subagent LONE var does NOT engage light mode** (#507/#518) — the inverse guard: with only `PI_SUBAGENT_CHILD_AGENT` set (no `PI_SUBAGENT_PARENT_PID`), no `subagent_light_mode` phase is logged. `subagent-mode.ts`'s doc comment requires the PAIR — a lone var set by some unrelated tool must not trigger light mode. 6. **`concurrent_session_bind` (#473) and its `concurrent_session_bind_rollup` session-end summary (#2249) — NOT asserted, documented TODO.** The guard is fully wired on master (PR #477): `index.ts`'s `session_start` handler calls `decideSessionStart()` and logs a `concurrent_session_bind` latency phase for a concurrent-secondary bind — so the phase exists to observe. `clients/session-start-observability.ts` also keeps a bounded, process-singleton-backed tally of declined binds by classification (`concurrent-secondary` / `secondary-root`), which `index.ts`'s `session_shutdown` handler logs as one `concurrent_session_bind_rollup` row (primary sessions only) and clears — same primary-only placement as `session_end_bus_rollup`/`path_attribution_verified_rollup`. The blocker is DRIVING either keylessly: reproducing tintinweb's in-process model for real (mirroring `agent-runner.ts`'s `createAgentSession()` + `DefaultResourceLoader` + `bindExtensions()` sequence) requires full session construction, which in turn needs model/provider config — not cheaply stubbable without a real model key, and #476 explicitly asks not to ship something flaky here. The unit + behavioral coverage in `tests/clients/session-lifecycle.test.ts` guards the classifier and the no-reset contract, and `tests/clients/session-start-observability.test.ts` plus `tests/index-integration.test.ts` guard the rollup's counting, process-singleton survival, and primary-only reset/emit wiring, all in-repo; what Layer B cannot yet add is the end-to-end SDK-driven variant. **Revisit if the pi SDK grows a model-free session constructor or stub provider** — at that point add a Layer B assertion analogous to 1-3 checking the `concurrent_session_bind`/`concurrent_session_bind_rollup` phases and the absence of a second LSP fleet teardown. ## What to do when the nightly alerts `compat-smoke.yml` never reds itself on a contract/behavioral failure — both layers run under `continue-on-error: true`. Instead, a failure opens (or refreshes the body of) a single tracking issue titled **"compat-smoke: third-party contract drift detected"** — search for it by title before assuming a NEW investigation is needed; the workflow already create-or-updates it, never duplicates. 1. Read the linked run log — the alert issue body now says which of Layer A's three outcomes fired (**verified** / **drift** / **infra**, #2581 — never just a generic "failure"); Layer A/B each print a `[PASS]`/`[FAIL]` or `[INFRA]` line per check with a one-line detail on exactly what didn't match or couldn't be found. 2. **If Layer A says infra**: the run log's `INFRA` line names every candidate path tried for that contract. Read the installed package at the printed version and find where the file actually lives now, then append it to that contract's `parts` in the `CONTRACTS` list (`scripts/lib/compat-contracts.mjs`) — don't replace the old candidate, the locator resolves whichever existing one is newest regardless of authored order (#2680 F1), so older installs stay covered too. This is a relocation, not a confirmed content change; only move to step 3 once the check actually runs against the relocated file. 3. **If Layer A says drift**: find the failed check's row in the pinned-contracts table above and read the current third-party source at the referenced file — the semantic shape genuinely changed upstream (a renamed env var, a moved `emit()` call, a reworded error message, ...). Update the corresponding matcher in `scripts/lib/compat-contracts.mjs` (and its test in `tests/scripts/compat-contracts.test.ts`) to match the new shape, update the pinned-contracts table's version/reference above, and fix whichever pi-lens module (`subagent-mode.ts` / `session-lifecycle.ts` / `instance-reaper.ts`) actually depended on the old shape if the drift broke real behavior — not just the check. If pi-lens's own dependent code already tolerates the drift gracefully (as with contract 1's now-removed identity vars, #2581), relaxing the check to stop requiring what upstream removed IS the fix — don't keep a check that can only ever report drift forever for something pi-lens never needed to work. 4. A Layer B failure (an assertion, not an infra error) means real pi-lens behavior regressed under a real `pi` — treat it like any other bug: write a fixture-level test if the gap wasn't otherwise covered, then fix. 5. Close the tracking issue once the underlying check is green again — it will reopen (well, get a fresh create — see the note in the issue body about not manually closing while still failing) if it recurs. ## Known-extension assessments (no contract pinned) Extensions assessed against the #473/#474/#475 interaction surfaces and found to need NO nightly contract check. Recorded so nobody re-derives this; re-assess if their execution model visibly changes. ### plannotator (`@plannotator/pi-extension`, assessed 2026-07-10 at ~v0.22) Browser-based plan/review UI; ~30k npm downloads/mo. **Benign coexistence.** Never binds an `AgentSession` in-process (#473 class N/A) and never spawns child `pi`s at session start (#475 class N/A). Namespaces fully disjoint (`PLANNOTATOR_*`, `~/.pi/plannotator*.json`). Its planning-phase `tool_call` blocking coexists additively with the read-guard (either may block; plan files are newly-created so never trip `zero_read`). Two second-order notes: (a) its own spawned agent jobs are cleaned up via `process.once("exit")` — its own #472-class orphan risk on hard kills, internal to plannotator (our reaper cleans pi-lens's servers inside any orphaned child `pi` it leaves); (b) its on-demand "run agent: pi" jobs spawn child pis WITHOUT a subagent marker, so pi-lens runs at full weight there — low-frequency, user-triggered, accepted. ### pi-dynamic-workflows (`@quintinshaw/pi-dynamic-workflows`, assessed 2026-07-10 at v2.12.0) vm-sandboxed orchestration scripts fanning out to up to 16 in-process subagents. **Benign coexistence via a THIRD execution model**: it calls `createAgentSession()` directly and never `bindExtensions()`/`reload()` — the only two SDK paths that emit `session_start` — so pi-lens's handler never runs for its subagents. No #473 hazard by construction; no #475 child processes; no #472 kills (disposal via `session.dispose()` in `finally`); zero namespace overlap (`~/.pi/workflows/`). **Known gap, by their design, not a bug**: because pi-lens never binds into those subagent sessions, subagent-written edits get NO pi-lens diagnostics, read-guard, or dispatch — pi-lens is a bystander until the parent session touches the files. Worktree-isolated agents (`.pi/worktrees/<slug>`) are additionally out of the parent's project scope. If a user reports "pi-lens didn't catch X in my workflow run", this is why. ### pi-subagents-worktrees (`@gotgenes/pi-subagents` + `@gotgenes/pi-subagents-worktrees`, assessed 2026-07-10 at ~v0.x) Friendly fork of `@tintinweb/pi-subagents` with worktree isolation extracted into a pluggable `WorkspaceProvider`: each opted-in subagent runs in a detached `git worktree` (`git worktree add --detach <tmpdir> HEAD`; unchanged worktrees are removed, changed ones are committed to a `pi-agent-<id>` branch). **A fourth execution model, mechanically the tintinweb one with a relabeled cwd**: in-process `AgentSession` + `bindExtensions({})` per child — the same SDK mechanics the tintinweb contracts (2a/2b/2c/2d) already pin, so **no new Layer A contract and no Layer B addition is warranted**. Findings that matter to pi-lens: - **No `PI_SUBAGENT_CHILD` env marker is ever set** — this fork identifies its own children via a session-id-keyed `globalThis` registry (`Symbol.for("@gotgenes/pi-permission-system:subagent-registry")`), not env vars. `subagent-mode.ts`'s `isSubagentSession()` therefore never fires for it — **correctly**: the #473 concurrent-session guard classifies the child's `bindExtensions` `session_start` as `concurrent-secondary` and suppresses `handleSessionStart` entirely, so light mode has nothing to throttle. Do not "fix" light-mode detection to catch this case. - The #473 classifier is cwd-agnostic by construction (ctx-liveness + session-id only), so the different-cwd child binds safely; the `concurrent_session_bind` phase already logs `sameCwd` for observability. - **Known-acceptable gap, same class as pi-dynamic-workflows**: worktree children get zero pi-lens coverage (no diagnostics, read-guard, or LSP — the worktree is never a pi-lens project root because pi-lens never runs there). Edits surface to pi-lens only if/when the parent session touches the resulting branch/files. ### pi-delegate (`drsh4dow/pi-delegate`, Codeberg, assessed 2026-07-10) Single-tool isolation extension: one fresh child agent per `delegate` call, bounded task, distilled report back; explicitly not a workflow engine. **Execution model = the pi-dynamic-workflows shape**: bare `createAgentSession({...})` with a `DefaultResourceLoader` — it **never calls `bindExtensions()`** (grep-verified in source + tests), so `session_start` never fires in the child and pi-lens is a bystander there (no read-guard, diagnostics, or dispatch on the child's edits). Nuance worth recording: the loader's `additionalExtensionPaths` makes pi-lens's TOOLS discoverable in the child's `getAllTools()` — discoverable-but-inert, since no lifecycle binding ever happens. No #473 hazard, no #475 relevance, child disposed via `dispose()` in a `finally`. **No contract pinned, no smoke coverage needed.** Version-drift flag: imports the pre-rename `@mariozechner/*` SDK scope — re-assess if this extension gets adopted against a current pi. ### avtc-pi-feature-flow / avtc-pi-subagent (assessed 2026-07-10 at avtc-pi-subagent@1.0.3) Feature pipeline (design→plan→implement→verify→review→UAT) fanning out child agents via its bundled `avtc-pi-subagent` engine: **real child-process spawns of `pi --mode rpc` / `--mode json -p` — the nicobailon shape — but with its OWN env vocabulary**: sets `PI_SUBAGENT_CHILD_AGENT` + `PI_SUBAGENT_PARENT_PID`, **never `PI_SUBAGENT_CHILD=1`** (grep-verified). `subagent-mode.ts` `isSubagentSession()` now detects this vocabulary too (#507, fixed 2026-07-10): the pair `PI_SUBAGENT_CHILD_AGENT` + `PI_SUBAGENT_PARENT_PID` (both non-empty) is treated as an additional subagent signal, alongside nicobailon's `PI_SUBAGENT_CHILD=1`. The pair is required rather than either var alone — a lone var set by some unrelated tool must not trigger light mode by itself. `PI_LENS_SUBAGENT_FULL=1` remains the universal opt-out for both vocabularies. `getSubagentIdentity()` now also reports which vocabulary matched (`marker: "pi-subagents" | "avtc-pi-subagent"`), surfaced in the `subagent_light_mode` latency phase so dogfooding can tell the ecosystems apart. Its per-feature worktree isolation is the same bystander situation as pi-dynamic-workflows' isolated worktrees — no separate row needed for that. Closed (#518, #507 itself was already CLOSED by PR #508): `avtc.child-env` is now a Layer A pinned contract in `scripts/lib/compat-contracts.mjs` (`checkAvtcChildEnv`, verified against `src/process-runner.ts`'s `subagentEnv.PI_SUBAGENT_CHILD_AGENT = agent.name` + `subagentEnv.PI_SUBAGENT_PARENT_PID = String(process.pid)` assignments), and Layer B (`scripts/compat-smoke-behavioral.mjs`) gained two behavioral assertions: the PAIR engages light mode, and a LONE `PI_SUBAGENT_CHILD_AGENT` (without `PI_SUBAGENT_PARENT_PID`) correctly does NOT. ### pi-swarm (`gjczone/pi-swarm`, assessed 2026-07-11) Agent-swarm orchestration ported from MoonshotAI/kimi-code: a `Swarm` tool spawning 1–128 parallel subagents in isolated git worktrees, plus a collaborative "Team" mode with a file-based JSONL mailbox (polling, no bus). **Execution model = real out-of-process children — but with NO subagent vocabulary at all**: `spawnSubagent` (`src/shared/spawner.ts:141-168`) builds `pi --print <task>` CLI args and spawns via `child_process.spawn` with `env: { ...process.env }` (`spawner.ts:397`) — a verbatim spread, zero additions. No `PI_SUBAGENT_CHILD=1`, no `PI_SUBAGENT_CHILD_AGENT`/ `PI_SUBAGENT_PARENT_PID` pair, no `bindExtensions`/`createAgentSession` in-process shape (grep-verified). Extensions DO activate in the children (they are full `pi` processes), so pi-lens runs there — but `classifySubagentSession()` sees no marker, light mode (#475) never engages, and every child pays full startup cost (LSP pre-warm + heavyweight scans), multiplied by up to 128 parallel worktrees. This is a **detection gap pi-lens cannot close unilaterally**: there is nothing in the child environment to key on. Fix is a one-line upstream change (adopt either existing vocabulary at `spawner.ts:397`); upstream ask is approval-gated per house policy. Active, well-maintained project (strict CI, last commit 2026-07-10). Its subagent tool-blocklist (`pi-invoke.ts:29-35`, prevents recursive swarms) is sound design, unrelated to pi-lens. **No contract to pin until upstream adds markers.**