* chore(#2798): declare the eleven reviewer lanes as manifest data
Phase 5a of epic #2782, delivering ADR-2782 D9 (roster half) and D3.
- Five reviewers GSD never installs into become lane-only role:reviewer
capabilities with no runtime body, no runtimeCompat and no install surface:
gemini, coderabbit, ollama, lm-studio, llama-cpp. Before this they had no
descriptor at all and lived as a hardcoded NON_RUNTIME_REVIEWER_SLUGS tail,
which is now deleted outright.
- The six hosts that are ALSO reviewers gain a reviewer body alongside their
runtime body. Their runtime bodies are byte-identical to next -- verified per
capability against the git blob, not asserted -- so no install behaviour moves.
- KNOWN_REVIEWER_SLUGS derives from declared bodies via an exported
deriveReviewerSlugs(registry). hostBehaviors.reviewerCli survives as a derived
legacy alias for one release; where a capability carries both, the body wins
and the slug appears once. Alias removal is Phase 7 (#2801).
THE KEYSTONE: the roster is the SAME ELEVEN SLUGS as before -- antigravity,
claude, coderabbit, codex, cursor, gemini, llama_cpp, lm_studio, ollama,
opencode, qwen. This phase changes HOW the roster is derived, not WHO is in it,
and the test asserts that literal list rather than a count.
kimi-code is deliberately NOT declared here. It is net-new with no
invoke_reviewers leg, so declaring it now would make it selectable but not
invocable -- present in --all, selected, emitting an empty section for the whole
5a-to-5b window -- and would break Phase 1's parity assertion. It lands in 5b
alongside the iteration that can run it. Legacy kimi (the Python CLI) is not a
reviewer at all and gains nothing.
The highest-value test is declaredManifestLanesMatchThePhase1Descriptor: it
deep-compares all eleven declared bodies against REVIEWER_LANES field-by-field,
including probe and invoke sub-fields. All eleven are byte-identical, key order
included. The epic's premise is that the manifest and the core descriptor
describe the same lane with NO translation layer, and Phase 2's review already
caught one divergence that every other test missed.
Two ADR corrections folded in, as Phases 1-3 each did:
1. PHASE ORDER. The ADR runs Phase 4 (federated config) before 5a and #2798
claims a dependency on 4. That is inverted and makes Phase 4 unsatisfiable:
D9 assigns review.<host>_host to lane capabilities that do not exist until
THIS phase creates them, and a federated config slice must live inside
capabilities/<id>/capability.json. Real graph: Phase 2 -> 5a -> 4.
2. #2798's INVENTORY acceptance item is vacuous. The inventory catalogs
bin/lib/*.cjs modules, not capability directories -- antigravity, opencode
and qwen appear zero times in it -- and gen-inventory-manifest --check passes
with the five new dirs and no edit.
Also corrected a stale line in Phase 2's own ADR amendment: it recorded the slug
pattern as /^[a-z][a-z0-9_-]*$/, but Phase 2's security review widened the
shipped pattern to /^[a-z0-9][a-z0-9_-]*$/ to match Phase 1's exported
LANE_SLUG_RE. The prose had not followed the code.
Closes#2798
* fix(#2798): catalogue reviewer capabilities in the generated matrix
The capability matrix rendered exactly two tables, feature and runtime, via
renderTable(caps, role) filtering on c.role === role. ADR-2782 D3 added a THIRD
role, so every role:"reviewer" capability was silently dropped from the
first-party catalogue.
The drift guard did not catch it, and could not: --check compares generated
output against the committed file, and both omitted the five lanes identically,
so it reported "up to date" while five shipped capabilities were invisible in
the one document that is supposed to list what ships. A guard blind to an entire
role is not guarding.
This phase is what exposed it -- it ships the first role:"reviewer"
capabilities -- so it is fixed here rather than deferred (CLAUDE.md: a defect
found while working is fixed in the current change, which overrides
one-concern-per-PR).
Verified red-before-green: with a lane row deleted from the matrix, --check now
exits 1; restored, it exits 0. Before this fix the lanes were absent entirely, so
there was nothing for the guard to compare.
Phase 6 (#2800) still owns enriching the matrix with lane-specific detail
(slug/flag/transport columns) and the locale parity gate. This is the narrower
fix: the capabilities APPEAR at all.
* fix(#2798): close two hardening gaps and record three limits durably
Isolated security review (5 targets, no blockers) reproduced two gaps in the new
deriveReviewerSlugs. Both are unreachable through the checked-in registry -- it is
generated, JSON-sourced and code-reviewed -- but the function is EXPORTED for
reuse and carries no other validation, so it must not depend on its caller.
- A whitespace-only slug passed the length>0 test verbatim and occupied a roster
entry it could never match. Slugs are now trimmed before the emptiness test. A
blank body correctly falls through to the legacy alias rather than DROPPING the
lane, which would have been worse than the blank slug.
- KNOWN_REVIEWER_SLUGS is computed at require() time, so an uncaught throw there
breaks import for EVERY consumer rather than degrading selection. It is now
guarded, yielding an empty roster on a malformed registry. That is a visible
degradation, not a silent one: under D4 an explicitly requested reviewer that
is unavailable is an ERROR, so /gsd:review --claude against an empty roster
fails loudly. This also removes an asymmetry -- the sibling capability-trust
module documents its collectors as TOTAL and wraps them for exactly this reason.
Also records three findings that previously existed ONLY in squash-merged PR
bodies, which is not a durable record:
- ADR-2782 D5 gains an implementation note explaining why the resolved host is
deliberately EXCLUDED from the disclosure signature. Rule 1 says consent binds
the resolved host; the loader has no config resolver, so folding it in would
make the loader and lifecycle compute different signatures for one manifest and
re-prompt forever. The binding is split: signature covers the SHA-pinned
manifest fields, the consent record stores the resolved host, and Phase 5b
re-resolves at invocation -- which is where rule 4 already puts the check. A
reader comparing rule 1 to the code would otherwise conclude it is unimplemented.
- CONTEXT.md's capability-trust entry still described THREE executable surfaces.
Phase 3 added the fourth and made that false; corrected here, since it is drift
this epic introduced rather than Phase 6's new-glossary-term work.
- stableJson documents the NaN/Infinity/undefined -> null signature collision and
why it is unreachable (JSON grammar has no such literal, so JSON.parse throws
first). Reachability rests entirely on the ingest path staying JSON.parse-only,
so the note lives where someone would break it.
* chore(#2798): backfill changeset pr number to 2837