@storybloq/lenses
Officialby Storybloq
README.md
# @storybloq/lenses
Multi-lens code review MCP server — 8 specialized reviewers run in parallel, with findings deduplicated, confidence-filtered, and rolled into a single verdict.
## Installation
```sh
npm install -g @storybloq/lenses
```
## Register with Claude Code
```sh
claude mcp add lenses -s user -- lenses --mcp
```
After registration, three tools become available in your Claude Code session:
- `lens_review_start` -- Returns `{reviewId, agents: [{id, model, promptHash, expiresAt}], cached}`. Refs-not-prompts shape keeps the hop-1 payload small; fetch the actual prompt for each agent via `lens_review_get_prompt` before spawning. The `agents[].expiresAt` is provisional: the prompt fetch anchors the authoritative deadline.
- `lens_review_get_prompt` -- Looks up the full prompt for one lens in an active review (stateless per `(reviewId, lensId)`) and returns the lens's authoritative `expiresAt`: the first attempt-1 fetch starts the lens's timeout clock, once per attempt.
- `lens_review_complete` -- Accepts the subagent outputs (with optional `attempt` for retry) incrementally (partial batches and empty polls are fine) and returns the merged verdict. The envelope includes `parseErrors[]`, `deferred[]`, `suppressedFindingCount`, `hadAnyFindings`, `nextActions[]` for the cooperative retry protocol (each retry carries a fresh per-attempt `expiresAt`), plus the coverage disclosure: `lensCoverage[]`, `coverage` (`full`/`partial`), `errorCodes` (`PARTIAL_RESULTS` when a lens expired), and `reviewComplete` (`false` marks an interim envelope; the review is still open). A late result diverts its lens to `expired` coverage instead of rejecting the call, and an uncovered core lens caps the verdict below `approve`.
## Architecture
See `CLAUDE.md` in the source repository for the two-hop flow, lens activation logic, merger semantics, and session caching.
## Stable library API
The package root (`@storybloq/lenses`) is a stable library surface, not just an MCP server entry point. It is consumed directly by the storybloq autonomous review backend, so existing exports are not renamed or removed across minor versions. The exported core is:
- `LENSES`, `getLens`, `SURFACE_RULES` - the lens registry projection and surface-activation routing rules
- `activate` - decide which lenses fire for a review (with `LensConfigSchema`, `LensIdSchema`)
- `buildLensPrompt`, `buildAgentPrompts` - construct the complete self-contained prompt(s) for activated lenses
- `renderLensBody`, `renderSharedPreamble` - lower-level prompt-body and shared-preamble renderers
- `runMergerPipeline` - the single merger entry that runs dedup, blocking policy, tension detection, and verdict computation over per-lens outputs
- `coreLensApplicability`, with the `CORE_LENS_NOT_APPLICABLE_EXTENSIONS`, `CORE_LENS_NOT_APPLICABLE_DIRS`, `SECURITY_SURFACE_PATHS`, `CONCURRENCY_SURFACE_TOKENS` and `CONCURRENCY_TOKEN_COVERED_EXTENSIONS` tables it reads - the server-side check for whether a lens had anything in its domain to review
- `buildNewSideIndex`, `diffTouchedPaths` (aliased as `newSideFilePaths`), `changedDiffLines`, `addedDiffLines`, `normalizeRepoPath` - the shared unified-diff parser, so a consumer building its own coverage computes the same file union the server does
- the schema, verdict, and blocking-policy types and Zod validators re-exported from `src/schema`, including `FindingOriginSchema`, `FindingOriginClassSchema` and `isCoveredEntry`
### Coverage, skips, and the approve cap
An uncovered core lens caps the verdict below `approve`. As of 0.6.0 "covered" is decided by `isCoveredEntry`, not by status alone: a core lens that reports `skipped` is covered only when the SERVER can verify the lens had nothing in its domain.
`coreLensApplicability` makes that call over the UNION of the caller-declared `changedFiles` and every path the diff touches, because a declared list alone is a claim, not evidence. "Touches" is read from five places, not just the `+++` header: a deletion names its file only on `--- a/path` and `diff --git`, a rename names both `rename from` and `rename to`, and a mode change or binary patch carries no hunk at all. Reading `+++` alone let a caller declare a docs-only change, delete a source file in the same diff, and have every core lens excuse itself. Where a diff SECTION carries no `diff --git` line, a leading `a/` or `b/` cannot be confirmed as git's prefix, so BOTH spellings join the union. Confirmation belongs to the one section that earned it and is spent there: a section is confirmed only when a `diff --git` opened it AND its own old-side header reads `--- a/...`, `--- "a/..."` or `--- /dev/null`, which is every form git writes and no form it writes for a following section. That second test is what a flag alone cannot do, because a header-only git section (a pure rename, a mode change, a binary patch) ends without a `---`/`+++` pair, so the NEXT section's header is the first one after that `diff --git`. Unconfirmed, a guessed `b/docs/engine.ts` was stripped into a root-anchored `docs/` path and excused every core lens over a file that added a lock: stripping unconditionally would promote a repository's real root `b/` directory into `docs/`. A change is not applicable to a lens when every file in that union is non-code, which means an extension in `CORE_LENS_NOT_APPLICABLE_EXTENSIONS` (documentation, data, config and image formats) or a path under a repository-ROOT `docs/` or `.story/` directory. Two lenses narrow that further:
- `security` stays applicable whenever any file matches `SECURITY_SURFACE_PATHS`: CI definitions (anything under a root `.github/`, `.circleci/` or `.buildkite/`, plus `.gitlab-ci.yml`, `Jenkinsfile`, `Dockerfile*`), dependency manifests and locks (`package.json`, `package-lock.json`, `pnpm-lock.yaml`, `yarn.lock`, any `*.lock`, `Cargo.toml`, `pyproject.toml`, `requirements*`, `Pipfile`, `Pipfile.lock`, `poetry.lock`, `Gemfile`, `Gemfile.lock`, `go.mod`, `go.sum`), and any `*.svg`, which is XML that can carry script. Config-as-code is not code by extension and is squarely the security lens's business.
- `concurrency` may additionally be excused on a code change when no CHANGED diff line carries any row of `CONCURRENCY_SURFACE_TOKENS`. Changed means added OR removed: deleting a `mutex.acquire()` is a change to the concurrency surface, and reading only the added side excused the lens over exactly the edit most likely to introduce a race. A removal counts whatever its position in the hunk, including the two shapes a new-side line budget alone cannot see: a `git diff -U0` deletion hunk (`@@ -12 +11,0 @@`), which adds nothing at all, and a removal that is the last line of a hunk at default context. Hunk membership comes from the header's old-side count, so a hunk stays open after its new-side lines run out. In that tail the parser accepts removals, `\ No newline` punctuation, and four header forms it can recognise there. `+++ `, `diff --git ` and `@@` are recognised by prefix, since a removal line always starts with `-`. A `--- ` line is recognised as a header on either of two signals: what follows is a spelling git writes (`a/...`, `"a/..."` or `/dev/null`), or the very next line opens with `+++ `, since a unified diff always pairs the two. One signal alone is not enough in either direction. An ordinary removal of a source line beginning with `-- ` (a SQL or Lua comment, usage text in a template literal) renders with the header prefix, and reading it as a header drops that removal and every one after it; a genuine header spelled `--- b/docs/engine.ts` or `--- src/pool.ts` is not in git's own form, and reading it as a removal drops a deleted file from the union, since its `--- ` line is the only place it is named. Two residual cases remain, in opposite directions, both accepted because every alternative is worse. Under-review: a removed source line whose own text begins `-- a/`, `-- "a/` or `-- /dev/null` reads as a git-spelled header, closes the hunk, and hides the removals after it. Reading no `--- ` as a header instead would drop deleted files from the union, since their `--- ` line is the only place they are named, and reading one as a header on the `+++ ` pairing alone would swallow a truncated header and a split `---`/`index`/`+++` pair. Over-review: the second case needs a malformed hunk header to reach. two removals can never trigger the pairing signal, because a removed line carrying `++ y` renders as `-++ y` rather than `+++ y`, so the second line has to be an ADDED line whose text begins `++ `, and an added line only reaches the tail when the header's new-side count understates what the hunk carries. Under a count that short, a removed line beginning `-- ` followed by that added line is read as a header pair. The counts would separate the two, and the parser declines to trust them there because reaching the tail on an added line is already proof they are wrong. The failure direction is the safe one: the union gains a spurious path, and a larger union can only make a lens more applicable. Context lines are excluded, since untouched surrounding code is not this change's doing. The carve-out needs two further conditions: every code file in the union must have an extension in `CONCURRENCY_TOKEN_COVERED_EXTENSIONS`, and every code file must actually have contributed a new-side line to the diff. An ecosystem the table does not model, or a file the diff shows no new lines for, defaults to review: "the table found nothing", "the table cannot see" and "there was nothing to look at" are three different statements, and only the first excuses a lens.
Line endings are normalized by the walk, so a CRLF diff and its LF twin produce the same paths, index keys and line content.
Only the concurrency token rule reads the diff's added lines; every other rule reads the file set. Every uncertain case resolves to applicable, because over-reviewing is the safe failure.
A skip on an applicable change keeps the `self-reported` basis and still caps, so a lens cannot claim a skip by prompt alone; a lens that never submitted is `no-submission` and never covered. A `basis` supplied by a caller is trusted DOWNWARD only: `no-submission` and `self-reported` are kept as given, while a supplied `not-applicable` is recomputed and demoted to `self-reported` when the server disagrees or has no diff to check it against.
A core lens that reports `skipped` on a change the server judges APPLICABLE is re-dispatched: `lens_review_complete` returns a `nextActions[]` entry for it, within the same `maxAttempts` budget that governs error and parse-failure retries, carrying a retry prompt that names the reason. That prompt says only what the server actually established: with a non-empty file union it found files in the lens's domain, and with an empty one (a plan review, or an artifact carrying no parseable diff) it could only fail to rule the domain out. Either way it permits `ok` with zero findings when there is genuinely nothing to report. That is the cap's actual remedy, and it is what the `skipped` status costs. A NOT-APPLICABLE skip is already coverage and is never re-dispatched; a non-core skip caps nothing, so it is not re-dispatched either.
An entry marked `relabeled` is never covered whatever its status, and the rule is about the DISPATCH rather than the pair of statuses. Within one review session the server sets the flag itself: a lens that submitted `skipped` and then submits `ok` with ZERO findings against the SAME dispatch is renaming its old answer, not producing a new one. Issuing the re-dispatch clears the flag, so the answer that comes back from it counts as the honest re-review it is. Without that escape the remedy was a trap: the cap fired, nothing re-dispatched the lens, and no sequence of honest behaviour could clear the mark. A flip the server had no budget left to ask for is still a relabel. An `ok` that actually carries findings is real work and is never flagged. Detection ACROSS separate review calls belongs to the caller, which is the only side that can see them; such a caller sets `relabeled` on the entry it supplies, and the server honours it.
When a cap fires, `capReasons` names it, one string per cap, so a caller can route a coverage-only `revise` to a lens re-run rather than another full round.
`LensCoverageEntrySchema` is `.strict()` and these entries now carry `basis` and `relabeled`, so a consumer that validates verdicts against its own bundled 0.5.x schema must upgrade together with the lens server: a 0.5.x strict parse of a 0.6.0 verdict rejects the whole payload rather than ignoring the two unknown keys.
### Optional finding fields, and one direction of compatibility
As of 0.5.0 a finding may also carry `principle` (the review-contract principle it violates, supplied by the lens) and the reporter-supplied provenance fields `dispositionReason`, `origin`, `originClass` and `sinceRound`. All are optional and none is defaulted: absence means no claim was made, which is a different statement from a known-empty one, and `principle` additionally rejects blank values so that absence stays the only way to name no principle.
The addition is one-directional. A payload produced before 0.5.0 parses on 0.5.0 unchanged. The reverse does NOT hold: `LensFindingSchema` is `.strict()`, so a consumer still on 0.4.x that receives a finding carrying `principle` loses the WHOLE payload to the error-placeholder path, not just the field. Upgrade the consumer before anything starts producing these fields.
A merge carries the REPRESENTATIVE's claim for all five and never borrows, fills or escalates from another member of the same dedup group. Members may well describe the same defect, which is what dedup is for, but group membership does not establish it, so one member's claim is never treated as interchangeable with another's. One consequence is deliberate and worth knowing: a non-representative's `originClass: "reintroduced"` does not survive a merge.
### Mutability boundary
All registry objects exported from the package root (`LENSES`, each lens projection, `SURFACE_RULES`, its nested rule objects and arrays) are frozen projection copies. The Zod schema instances (`optsSchema`) and `renderBody` implementations that live on the internal lens definitions are internal and unreachable from the public surface. Mutating any exported registry object throws in strict mode and can never alter prompt construction: the internal registry that `buildLensPrompt` and `activate` read is a separate, unfrozen object, disjoint from the frozen public projection.
## License
[PolyForm Shield 1.0.0](https://polyformproject.org/licenses/shield/1.0.0/) - source-available, non-compete (not OSI open source). Any use is permitted (personal, internal, commercial) except building a product that competes with storybloq or lenses. Competing, hosted, or white-label use requires a separate license: contact shayegh@me.com. See [LICENSE](./LICENSE) and [NOTICE](./NOTICE).
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues