# ADR-0065: Marketplace catalog and directory discovery

- **Status**: Accepted (2026-07-09), amended 2026-07-30 — see Amendment history
- **Kind**: Retrospective
- **Area**: cli-surface
- **Date**: 2026-07-09 (amended 2026-07-30 — D2 and D4 by Amendment 1; D2's MCP flag
  and delta #3 by Amendment 2, see Amendment history)
- **Relations**: supersedes v0-0003, v0-0007

## Amendment history

**2026-07-30 — Amendment 1: D2's support-file claim was false when written; D4's
agent-description sentence and D2's tree are re-recorded after the bundle changed.** This
document's Kind is Retrospective, so most of its prose describes the tree as shipped and goes
stale by construction whenever the tree changes. Two of the three corrections below are that
ordinary staleness. The first is not. It was a claim about a foreign product's behavior that
the foreign product already contradicted on the day the claim was written, and no change of
ours made it wrong.

1. **The "invisible to discovery" claim was wrong about the world, not made wrong by this
   change.** D2's tree preamble stated that per-role `agents/<name>/*.md` reference docs
   "exist on disk but are invisible to discovery, which globs only direct `agents/*.md`".
   That is true of *Studio's* discovery, which is the only discovery this document defines,
   and false of the plugin host's own validator. Measured with `claude plugin validate`
   (Claude Code v2.1.220): it walks `agents/<subdir>/*.md` and reported each of the six
   support docs as an agent with "no frontmatter block found" — eight `Validating agent:`
   lines for a bundle holding two agents. The unqualified word "discovery" is what let a
   statement about our own scanner read as a statement about the world.

   Fixed at the source rather than by rewording: the six files moved to `reference/<role>/`,
   outside `agents/` entirely. Measurement after the move, taken on the real bundle rather
   than a probe — `claude plugin validate marketplace/orchestrate --strict` returns a bare
   `✔ Validation passed` at rc 0 with no warnings line at all. Before the move the same
   command returned `✔ Validation passed with warnings`, and `✘ Validation failed` under
   `--strict`, on eight warnings.

   **Open question, recorded rather than resolved**: whether the plugin host's *runtime* also
   loaded those six as agents, which would mean every installing user got six phantom agents.
   The validator's behavior is measured; the runtime's is not. The relocation makes the
   question moot going forward and does not answer it for anything already published.

2. **Both agents now declare a `description`, so D4's closing sentence no longer holds.** It
   read: "The current `critic.md` and `orchestrator.md` have no description field, so Studio
   reports empty descriptions for both even though their bodies describe their roles." Both
   files now declare one. Studio's behavior is unchanged; its input changed.

3. **D2's tree is re-drawn** to show `reference/` and an `agents/` holding exactly two direct
   files. D2's rule that files outside the direct `skills/<directory>` and `agents/*.md`
   patterns are support material is unchanged, and is now enforced rather than only
   documented.

**Enforcement added with this amendment, because a corrected sentence is not a mechanism.**
`marketplace/scripts/validate_manifests.py` now fails when any markdown under `agents/` is
not a direct child of it, and when a direct agent file carries no frontmatter `description`.
Both rules were verified by breaking the subject on purpose: a clean run cannot distinguish a
rule that passed from a rule that never looked. That is partial movement on delta 1 below,
which stays open because it also asks for source-path and catalog-name validation.

The first-party validator is deliberately **not** wired into CI. It is not installed on the
runners, and pinning it by a floating version would put a moving external reference on our own
gate. The equivalent checks are implemented locally instead, where they are deterministic and
version-stable.

**What this amendment does not change.** P2 and the "Explicit `skills` and `agents` arrays"
alternative stand exactly as written. The plugin schema permits such arrays and this record
refuses them; where a permissive foreign schema meets a restrictive local decision, the local
one governs. Adding them would be its own amendment with its own argument, not a rider on
this one.

**Context this amendment does not decide, recorded because it bears on the next one.** The
bundle's declared version has been `1.0.0` since it was introduced on 2026-05-19, while 12
commits changed its contents afterward, the last on 2026-06-15. A capability change that does
not move the version is not installable as a change, so the version bump belongs to whichever
change next alters what the bundle does. *(Both figures in this sentence were miscounted;
corrected in Amendment 2 below, with the span each one belongs to.)*

**2026-07-30 — Amendment 2: the bundle now declares an MCP server, which closes delta 3 and
falsifies D2's flag sentence a second time; two figures in Amendment 1's own context
paragraph are corrected here rather than carried forward.**

1. **D2's exact-semantics sentence is re-recorded.** It read: "The current official bundle
   has neither hooks nor MCP configuration, so both flags are false." As of the commit that
   lands this amendment, `plugin.json` declares a truthy `mcpServers`, so `has_mcp` is now
   true; `has_hooks` is unaffected and stays false, since no `hooks/hooks.json` exists. The
   sentence is rewritten below rather than left to go stale, matching the Retrospective kind's
   own rule: this document describes the tree as shipped.

2. **The single carrier is named, which is delta 3's stated acceptance.** `mcpServers` is
   declared inline in `plugin.json`, keyed `lion`, and the bundle ships no `.mcp.json`. D6's
   `.mcp.json`-over-`plugin.json.mcpServers` read precedence therefore never engages: exactly
   one file defines the server, by construction, not by which file happens to win when both
   exist.

3. **The resolved scoped tool name is recorded, so a plugin user has the determinate
   string.** A plugin-provided MCP server's tools are namespaced
   `mcp__plugin_<plugin-name>_<server-name>__<tool>`, never the bare `mcp__<server-name>__`
   form this fleet types internally. This bundle is the plugin named `orchestrate`, and the
   server's own reported `serverInfo.name` is `lion` — matching the `plugin.json` key, which
   is what makes the tool name determinate rather than dependent on an unresolved question
   about which of the two the vendor's scoping actually reads. The tool a plugin user's Claude
   session calls is:

   ```text
   mcp__plugin_orchestrate_lion__request
   ```

   `mcp__lion__request` is not a name any plugin user's session resolves.

4. **Delta 3 is closed.** Its stated acceptance was that this document names the single
   carrier and the flag sentence agrees with the tree; both are done by points 1 and 2 above.

**Two figures in Amendment 1's context paragraph were miscounted, corrected here with the
span each one belongs to rather than swapped in place.** That paragraph read: "the bundle's
declared version has been `1.0.0` since it was introduced on 2026-05-19, while 12 commits
changed its contents afterward, the last on 2026-06-15." Measured at the git record:

- The manifest was introduced at `0.1.0` on 2026-05-19 (`a29a140fc`) and moved to `1.0.0` on
  2026-05-23 (`0aab5a9ed`); it has not moved since. "`1.0.0` since it was introduced on
  2026-05-19" conflated the introduction commit with the bump four days later.
- Commits touching `marketplace/orchestrate/` since the version froze at `0aab5a9ed`: **2**
  — `27d09726f` (2026-06-15) and the commit that landed Amendment 1 above. One of those two
  predates this lane's own merge: `27d09726f`, 2026-06-15. Counting instead from the
  2026-05-19 introduction, which is the span the original "12" was actually bound to, gives
  **13** by direct count — a different span than the one the sentence's claim was making, not
  a rounding error.

The paragraph's argument is unaffected by either correction: one content change nobody
received is still a change nobody received, and a capability change that does not move the
version is not installable as a change. The version bump lands with this amendment's own
commit, to `1.1.0`.

## Context

The repository publishes one official marketplace catalog in
`.claude-plugin/marketplace.json`. Its current inventory contains one `orchestrate`
plugin sourced from `marketplace/orchestrate`; the earlier four-plugin catalog is not
present.

**P1 — Publication and capability inventory are different questions.** The root
manifest answers which repository bundles are official. Skills and agent profiles are
files that can change independently inside a published bundle.

**P2 — Plugin metadata is not a second capability registry.** The current
`marketplace/orchestrate/.claude-plugin/plugin.json` carries package name, description,
version, attribution, links, license, and keywords. It has no `skills` or `agents`
arrays. Requiring such arrays would duplicate the directory tree.

**P3 — Studio needs a safe, current projection of both sources.** Studio reads official
source paths from the root manifest, scans each plugin directory, and exposes summary
and detail payloads. A malformed or escaping catalog path must not become arbitrary
filesystem access.

**P4 — Installed third-party plugins are discoverable but not official inventory.**
They live under a user cache with marketplace, plugin, and version directories. Studio
lists them as a separate source and does not write them into the repository manifest.

**P5 — Filesystem defects must degrade locally.** Missing directories, unreadable
Markdown, malformed/missing JSON metadata, and absent optional hook/MCP/readme files
should omit or default that field rather than make the entire plugin list unavailable.

| Concern | Decision |
|---|---|
| Official publication boundary | D1: `.claude-plugin/marketplace.json` is the sole official bundle inventory. |
| Bundle contract | D2: `plugin.json` supplies metadata; `skills/` and `agents/` supply capability inventory. |
| Catalog path safety | D3: official source paths must be relative, traversal-free, and contained under the repository root. |
| Directory scanning | D4: deterministic filesystem scans derive skill/agent summaries and optional capability flags. |
| Installed plugin source | D5: third-party cache discovery is appended separately, with lexicographic latest-version selection and no deduplication. |
| Studio projection | D6: list/detail/content endpoints expose fixed dictionary shapes assembled from those sources. |

This ADR deliberately does **not** decide:

- Plugin installation, activation, or update behavior; this is catalog and discovery
  only.
- Skill or agent execution semantics; discovery exposes their metadata/content but does
  not run them.
- A future split of `orchestrate` into additional installable bundles; the current
  catalog is recorded as shipped.
- Marketplace README generation; consistency with the manifest and directory scan is a
  retrospective delta.
- General filesystem publication outside plugin roots; D3 deliberately bounds official
  sources to the repository.

## Decision

### D1 — The root marketplace manifest is the official bundle inventory

The inventory-bearing portion of the current file is:

```json
{
  "$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
  "name": "lionagi",
  "version": "1.0.0",
  "plugins": [
    {
      "name": "orchestrate",
      "source": "./marketplace/orchestrate",
      "description": "Multi-agent orchestration for lionagi: plan DAG workflows, author playbooks, run multi-play shows with quality gates."
    }
  ]
}
```

Catalog-level descriptive and attribution metadata is present in the file but does not
participate in Studio's plugin iteration. The discovery contract consumes only
`plugins[*].name`, `source`, and `description`.

The loader is:

```python
# lionagi/studio/services/plugins.py
def _iter_marketplace_plugins() -> list[tuple[Path, str, str]]: ...
```

Each tuple is `(resolved_plugin_dir, manifest_name, manifest_description)`.

**Exact semantics**:

- The primary manifest path is `<repo-root>/.claude-plugin/marketplace.json`.
- If that path is missing and the resolved marketplace directory has a parent manifest,
  the service uses that fallback.
- A missing manifest returns an empty official list.
- JSON read failure or a falsey decoded value returns an empty official list through the
  shared `_read_json` helper.
- A syntactically valid non-mapping JSON value is not rejected by `_read_json`; the
  subsequent `manifest.get(...)` access can raise. The checked-in manifest is a mapping,
  but runtime degradation is not total for every JSON shape.
- Entries are visited in manifest order.
- An entry with an empty/missing name is skipped.
- An empty, unsafe, missing, or non-existing source path is skipped.
- The loader does not scan arbitrary `marketplace/` children looking for additional
  official bundles.
- The present official inventory is exactly one bundle, `orchestrate`; installed cache
  copies with the same plugin name do not alter that statement.

**Why this way.** Publication is an intentional act. An explicit catalog prevents an
experimental or incomplete directory from becoming official merely because it exists.
The single manifest also gives external installers one stable inventory source.

### D2 — Plugin metadata and capability files have separate authorities

The current official bundle tree relevant to discovery is (per-skill supporting files omitted —
they exist on disk and Studio's discovery does not enumerate them, because it globs only direct
`agents/*.md` and each skill's `SKILL.md`). "Discovery" throughout this document means Studio's;
the plugin host's own validator walks a wider set, which is why per-role reference docs live
outside `agents/` — see Amendment 1:

```text
marketplace/orchestrate/
├── .claude-plugin/
│   └── plugin.json
├── README.md
├── agents/
│   ├── critic.md
│   └── orchestrator.md
├── reference/
│   ├── critic/
│   │   ├── evaluation-guide.md
│   │   └── finding-format.md
│   └── orchestrator/
│       ├── dag-decomposition.md
│       ├── instruction-templates.md
│       ├── role-routing.md
│       └── synthesis-and-teams.md
└── skills/
    ├── debug/SKILL.md
    ├── orchestrate/SKILL.md
    ├── playbook/SKILL.md
    ├── pr-review/SKILL.md
    ├── review/SKILL.md
    ├── security-review/SKILL.md
    ├── show/SKILL.md
    ├── summarize/SKILL.md
    └── tdd/SKILL.md
```

The plugin metadata file's discovery-relevant fields are:

```json
{
  "name": "orchestrate",
  "description": "Multi-agent orchestration for lionagi: plan DAG workflows, author playbooks, run multi-play shows with quality gates, and monitor execution in Lion Studio.",
  "version": "1.0.0",
  "license": "Apache-2.0",
  "keywords": [
    "multi-agent",
    "orchestration",
    "dag",
    "playbook",
    "show",
    "lionagi"
  ]
}
```

`plugin.json` does not enumerate skills or agents. The current directory-derived
inventory is nine skills and two agents.

**Exact semantics**:

- Summary `name`, `description`, and `version` prefer truthy `plugin.json` values over
  manifest/cache fallback values.
- As with the root manifest, a syntactically valid non-mapping `plugin.json` can raise at
  `.get`; missing, unreadable, or malformed JSON instead becomes `{}`.
- Missing/falsey version defaults to the string `0.0.0`.
- Skill and agent counts are recomputed from directory scans each time a summary is
  built.
- `hooks/hooks.json` existence sets `has_hooks`; the file need not parse for the summary
  flag to be true.
- `.mcp.json` existence sets `has_mcp`. If absent, a truthy `plugin.json.mcpServers`
  also sets it.
- The current official bundle has no `hooks/hooks.json`, so `has_hooks` is false. It
  declares `mcpServers.lion` inline in `plugin.json` and ships no `.mcp.json`, so
  `has_mcp` is true and the `plugin.json` fallback path is the one that sets it — see
  Amendment 2.
- Files outside the direct `skills/<directory>` and `agents/*.md` patterns are support
  material, not separately enumerated capabilities.

**Why this way.** Package metadata changes at release cadence, while capability files
are the executable/document content itself. Using the directory as inventory removes a
second list that would need to change every time a skill or agent file is added,
renamed, or removed.

### D3 — Official source resolution is contained under the repository root

The path contract is:

```python
# lionagi/studio/services/plugins.py
def _resolve_marketplace_source(source_rel: str) -> Path | None: ...
```

Resolution applies:

```text
source_rel must be non-empty
and Path(source_rel) must be relative
and must not contain traversal
and resolve(repo_root / source_rel) must remain under resolve(repo_root)
```

**Exact semantics**:

- Absolute paths return `None`.
- Parent traversal returns `None` before resolution.
- A symlink that resolves outside the repository returns `None` at the containment
  check.
- `OSError` or `ValueError` during resolution/containment returns `None`.
- A safely resolved path is returned even if it does not exist; the caller then requires
  `plugin_dir.exists()` before adding the official entry.
- Containment is against the repository root, not only `MARKETPLACE_DIR`. The current
  catalog still points under `marketplace/`, but the safety contract allows another
  repository-contained source directory.

For named content lookup, the service additionally calls:

```python
safe_path_join(plugin_dir / "skills", skill_name)
safe_path_join(plugin_dir / "agents", agent_name)
```

before constructing the requested child path. A traversal attempt therefore raises
through the path-safety helper rather than reading outside the plugin root.

**Why this way.** The marketplace manifest is data read from disk and cannot be trusted
as an unrestricted path map. Repository containment permits flexible in-repository
layout while blocking absolute, traversal, and symlink escape paths.

### D4 — Skills and agents are discovered by deterministic directory scans

The scanners are:

```python
# lionagi/studio/services/plugins.py
def _scan_skills(plugin_dir: Path) -> list[dict[str, Any]]: ...
def _scan_agents(plugin_dir: Path) -> list[dict[str, Any]]: ...
```

Skill summary shape:

```python
{
    "name": str,
    "description": str,
}
```

Agent summary has the same shape.

**Skill scan semantics**:

- Missing `skills/` returns `[]`.
- Direct entries are sorted by filesystem name; non-directories are ignored.
- The Markdown candidate order is `SKILL.md`, `<directory-name>.md`, then the first
  `*.md` yielded by the directory glob.
- A directory with no candidate Markdown is omitted.
- An unreadable candidate is omitted.
- Frontmatter is parsed. Name is truthy frontmatter `name` or the directory name;
  description is truthy frontmatter `description` or `""`, converted to string and
  stripped.
- Malformed/absent frontmatter behavior follows the shared frontmatter parser; the
  scanner does not reject an otherwise readable file for missing fields.

**Agent scan semantics**:

- Missing `agents/` returns `[]`.
- Only direct `agents/*.md` files are considered, sorted by path.
- An unreadable file is omitted.
- Name is always the file stem. Agent frontmatter `name` is not consulted.
- Description is frontmatter `description` or `""`, converted to string and stripped.
- `critic.md` and `orchestrator.md` each declare a frontmatter `description`, so Studio
  reports both rather than empty strings. The earlier state, in which neither declared one,
  is recorded in Amendment 1.

Full content lookup has additional shapes:

```python
# get_plugin_skill(...)
{
    "name": str,
    "description": str,
    "path": str,
    "allowed_tools": list[Any],
    "content": str,
}

# get_plugin_agent(...)
{
    "name": str,
    "description": str,
    "path": str,
    "content": str,
}
```

For a skill, scalar `allowed-tools` is normalized to a one-element list, an existing list
is retained, and absence becomes `[]`. `content` is the post-frontmatter body. Agent
content uses the file stem after removing an optional `.md` suffix from the request.

**Why this way.** Sorted direct-child scans are deterministic and make the file tree the
source of truth. Lenient metadata fallback keeps a partially documented capability
visible while unreadable or misplaced files do not create stale phantom entries.

### D5 — Installed third-party discovery is separate and append-only

The installed-cache contract is:

```python
# lionagi/studio/services/plugins.py
THIRDPARTY_DIR = Path.home() / ".claude" / "plugins" / "cache"

def _iter_thirdparty_plugins(
) -> list[tuple[Path, str, str, str]]: ...
```

The expected layout is:

```text
~/.claude/plugins/cache/
└── <marketplace_name>/
    └── <plugin_name>/
        └── <version>/
            ├── .claude-plugin/plugin.json
            ├── skills/
            └── agents/
```

Each tuple is `(plugin_dir, name, description, marketplace_name)`.

**Exact semantics**:

- A missing cache root returns `[]`.
- Marketplace and plugin directories are iterated in sorted order; non-directories are
  ignored.
- Direct version subdirectories are sorted lexicographically, and the final entry is
  selected. This is not semantic-version comparison.
- A plugin directory with no version subdirectory is skipped.
- Name and description prefer plugin metadata, with the cache plugin-directory name and
  empty string as fallbacks.
- `list_plugins` emits official summaries first, then third-party summaries. It does not
  deduplicate equal names.
- `get_plugin`, skill lookup, and agent lookup search official entries first, then
  third-party entries. An official plugin therefore shadows an installed plugin with the
  same name for detail/content lookup even though both may appear in the list.
- The third-party tuple's marketplace name becomes the summary `source`; official
  summaries use the literal source `marketplace`.
- Cache shape is trusted structurally: an unexpected directory tree may be projected as
  a plugin with metadata defaults. It still remains separate from official inventory.

**Why this way.** Installed plugins are useful to Studio users, but local cache contents
must not redefine what the repository publishes. Appending sources preserves visibility;
official-first lookup gives the root manifest deterministic precedence.

### D6 — Studio exposes summary, detail, and content payloads

The internal builders are:

```python
def _plugin_summary(
    plugin_dir: Path,
    name: str,
    description: str,
    source: str,
) -> dict[str, Any]: ...

def _plugin_detail(
    plugin_dir: Path,
    name: str,
    description: str,
    source: str,
) -> dict[str, Any]: ...

def list_plugins() -> list[dict[str, Any]]: ...
def get_plugin(name: str) -> dict[str, Any] | None: ...
def get_plugin_skill(plugin_name: str, skill_name: str) -> dict[str, Any] | None: ...
def get_plugin_agent(plugin_name: str, agent_name: str) -> dict[str, Any] | None: ...
```

Summary payload:

```python
{
    "name": str,
    "description": str,
    "version": str,
    "source": str,
    "skill_count": int,
    "agent_count": int,
    "has_hooks": bool,
    "has_mcp": bool,
    "path": str,
}
```

Detail payload extends the summary:

```python
{
    **summary,
    "skills": list[{"name": str, "description": str}],
    "agents": list[{"name": str, "description": str}],
    "hooks": Any | None,
    "mcp": Any | None,
    "readme": str | None,
}
```

The HTTP projection is:

| Route | Success | Miss |
|---|---|---|
| `GET /plugins` | `{"plugins": list_plugins()}` | Empty list is valid |
| `GET /plugins/{name}` | Detail object | 404 `Plugin <name> not found` |
| `GET /plugins/{plugin_name}/skills/{skill_name}` | Skill content object | 404 |
| `GET /plugins/{plugin_name}/agents/{agent_name}` | Agent content object | 404 |

Filesystem work is moved to a worker thread with `anyio.to_thread.run_sync` before the
async route returns.

**Exact optional-file semantics**:

- Detail reads `hooks/hooks.json` only when it exists; read/parse failure follows the
  shared JSON helper and yields its falsey/default result.
- MCP detail prefers `.mcp.json`; only when absent does it fall back to
  `plugin.json.mcpServers`; a falsey value becomes `None`.
- README is `README.md` text when readable, otherwise `None`.
- Response paths pass through `public_path`, so the service exposes a bounded display
  path rather than an arbitrary absolute host path.
- Summary scanning and detail scanning are separate calls; a concurrent directory
  change can make counts and arrays differ within one detail build. No snapshot lock is
  taken.

**Why this way.** Small plain dictionaries keep the Studio service independent of plugin
execution code. Summary payloads are cheap for lists; details and full content are read
only when requested. Thread offload prevents synchronous filesystem access from blocking
the async server loop.

## Consequences

- Adding or removing a skill/agent file changes discovery without a second manifest
  edit.
- The official publication boundary remains explicit even when other directories or
  installed cache entries exist.
- Consumers that read only `plugin.json` cannot obtain capability inventory; they must
  follow the directory convention or call Studio discovery.
- Missing or unreadable capability files are omitted rather than represented by stale
  arrays. This favors current readable content over diagnostic completeness.
- Official and installed plugins with the same name can both appear in lists, while
  official-first detail lookup returns only the official one.
- Lexicographic version selection is simple but can choose a different directory than
  semantic-version ordering.
- Reversing the publication boundary would be high cost because arbitrary directories
  could become official. Changing scan fallback order is moderate and can change names
  or counts. Adding explicit arrays would require dual-source reconciliation or a
  migration away from directory authority.

## Current-vs-ideal delta

| # | Delta | Size | Issue |
|---|---|---|---|
| 1 | Add a marketplace consistency check that validates every official source path, matches each catalog name to its plugin metadata, and confirms directory-discovered skills and agents are readable; acceptance: catalog or layout drift fails the focused test. | S | (filled at issue-open time) |
| 2 | Generate or validate the marketplace README inventory from the root manifest and directory scan; acceptance: the documented official plugins and capability lists cannot disagree with repository discovery. | S | (filled at issue-open time) |
| 3 | **Closed by Amendment 2 (2026-07-30).** Re-record the MCP state in D2 at the commit where the bundle first declares a server: `has_mcp` and the "neither hooks nor MCP configuration" sentence both stop being true at that commit, and neither is amended before it. The bundle is to declare `mcpServers` in `plugin.json` and ship no `.mcp.json`, so D6's `.mcp.json`-over-`plugin.json.mcpServers` precedence never engages and exactly one carrier defines the server; acceptance: this document names the single carrier and the flag sentence agrees with the tree. | S | closed |

## Alternatives considered

### Explicit `skills` and `agents` arrays in plugin.json

Arrays would make inventory available without filesystem scanning and could validate
intentional ordering. They lost because they duplicate the executable directory
inventory and require two edits for every capability change. Drift would produce an
unanswerable question about whether the manifest or file is authoritative.

### Treat every marketplace subdirectory as official

Directory enumeration would remove the root manifest and make new bundles zero-config.
It lost because mere presence is not publication intent: incomplete, experimental, or
support directories could become official. The explicit catalog is the required
boundary.

### Put all metadata in the root manifest

One file could describe bundle version, hooks, MCP, skills, and agents. It would make
catalog reads self-contained. It lost because bundle-local metadata would be separated
from the installed unit and the root file would repeat both `plugin.json` and directory
content.

### Merge official and installed entries by plugin name

Deduplication would simplify Studio lists. It lost because equal names can represent
different sources or versions, and silently choosing one would hide installed state.
The current design shows both and makes official-first detail precedence explicit.

### Semantic-version selection in the installed cache

Parsing versions would choose `10.0.0` over `9.0.0` correctly and reject non-version
directories. It was not selected in the shipped implementation because the cache scan
uses no packaging dependency and accepts hash-like version directories. Lexicographic
latest is simple but must not be described as semantic latest.

### Fail the complete plugin list on one malformed entry

Strict failure would surface catalog and file defects immediately. It lost for the
runtime surface because one unreadable optional file should not hide every healthy
plugin. The consistency-test delta is the correct place for strict repository
validation; runtime discovery remains locally degrading.

### Split the current bundle into multiple hypothetical plugins

A finer catalog could install review, orchestration, and development methods
independently. It is deferred because the repository currently ships one coherent
installable bundle and no separate publication contract exists. The ADR records current
architecture rather than inventing products not present in source.

## Notes

The old multi-plugin inventory is historical. Current truth is the one-entry root
manifest plus the capability files reachable beneath that entry's source directory.
