diff --git a/docs/extensions/plugin-kit-ai-integration-plan.md b/docs/extensions/plugin-kit-ai-integration-plan.md new file mode 100644 index 00000000..1960ea8a --- /dev/null +++ b/docs/extensions/plugin-kit-ai-integration-plan.md @@ -0,0 +1,2880 @@ +# plugin-kit-ai Integration Plan for Extensions Plugins + +**Status**: Draft +**Date**: 2026-04-18 +**Owner repos**: + +- `claude_team` +- `plugin-kit-ai` + +## Purpose + +Replace the current Claude-only plugin backend in `claude_team` with a provider-aware backend powered by `plugin-kit-ai`, while keeping the existing `Extensions -> Plugins` UI. + +The integration must support two different truths at the same time: + +- **Universal plugins** managed through `plugin-kit-ai` +- **Native external installed plugins** that already exist in Claude or Codex and are not yet part of universal managed state + +Those are different objects and must remain different in UI, state, and actions. + +## One-Page Summary + +### What we are building + +- keep the current `Extensions -> Plugins` UI in `claude_team` +- bundle `plugin-kit-ai` as a backend engine +- use `plugin-kit-ai` for: + - universal catalog + - native discovery + - universal lifecycle actions + +### What we are not building + +- not embedding a second plugin UI +- not parsing prose CLI output +- not scraping repo layout from `claude_team` +- not pretending native installed plugins are the same thing as universal managed plugins +- not promising `local` scope before backend really supports it + +### User-visible outcome + +- installed plugins come first +- universal plugins are the main storefront +- native installed Claude/Codex plugins stay visible and are labeled honestly +- install/update/remove/repair results stay target-granular + +### Safe delivery order + +1. add stable JSON contracts to `plugin-kit-ai` +2. add universal catalog in `plugin-kit-ai` +3. add native discovery in `plugin-kit-ai` +4. integrate read-only mixed plugin view in `claude_team` +5. add universal lifecycle actions in `claude_team` +6. consider optional native convenience flows only later + +### Non-negotiable no-go items + +- no auto-merge by display name +- no silent `local -> project` downgrade +- no destructive actions on native external entries unless backend explicitly declares them safe +- no app-side inference where backend truth is missing + +## Glossary + +### Universal plugin + +A plugin from the universal plugin catalog that can be managed through `plugin-kit-ai`. + +### Native external plugin + +A plugin that already exists in a native agent surface such as Claude or Codex, but is not part of `plugin-kit-ai` managed state. + +### Managed universal plugin + +A universal plugin that `plugin-kit-ai` has installed or is tracking in `~/.plugin-kit-ai/state.json`. + +### Catalog + +The backend surface that answers: + +- what universal plugins exist +- what targets and scopes they support +- what storefront metadata should be shown + +### Discover + +The backend surface that answers: + +- what native plugins already exist outside managed universal state +- what target and scopes they belong to +- whether the app may safely manage them + +### List + +The backend surface that answers: + +- what universal plugins are already managed + +### Doctor + +The backend surface that answers: + +- which managed universal plugins need attention because of drift, auth, or activation state + +## Hard Product Decisions + +These are fixed unless a new ADR explicitly changes them. + +### 1. Two plugin classes + +The page shows: + +- `Universal` +- `Native external installed` + +They are never silently merged. + +### 2. Installed-first ranking + +Ranking order: + +1. installed universal +2. installed native external +3. available universal + +### 3. Universal is the main storefront + +Universal plugins are the default source for new installs. +Native external plugins are primarily visibility and compatibility surfaces. + +### 4. `discover` before `adopt` + +Phase 1 needs visibility, not ownership conversion. + +### 5. No fake scope parity + +If the backend target does not support a scope, the UI must not pretend it does. + +## Definition of Done + +This migration is done only when all of the following are true: + +- `plugin-kit-ai` exposes stable machine-readable `catalog`, `discover`, and lifecycle contracts +- `claude_team` renders universal and native external entries as distinct classes +- direct Claude mode works end-to-end for universal install/update/remove/repair +- multimodel Anthropic + Codex mode works end-to-end with target-granular results +- native external plugins remain visible and truthfully labeled +- the page stays useful when one backend view fails or is stale +- rollback is possible through a feature flag without destructive cleanup + +If any of these is false, the migration is still in progress. + +## What Is Already True in plugin-kit-ai + +This plan should build on real current code, not on an imagined backend. + +### Already present today + +- `integrationctl` already exposes a public lifecycle facade +- target adapters already expose: + - `Capabilities` + - `Inspect` + - `Plan*` + - `Apply*` + - `Repair` +- post-apply verification already re-inspects the target and rejects false-positive installs +- managed lifecycle state already exists in `~/.plugin-kit-ai/state.json` +- read-only managed views already exist conceptually: + - `list` + - `doctor` +- lifecycle update/remove already re-resolve the source and reject identity drift if the resolved manifest no longer matches the stored `integration_id` + +### Important current gaps + +- `integrations` CLI currently prints prose, not versioned JSON +- there is no public `catalog` surface yet +- there is no public `discover` surface yet +- current managed `Report.Targets` do not carry enough integration-level context for the app +- current service composition still depends on `os.Getwd()` for workspace semantics + +### Important current model split + +There are two useful metadata layers today: + +- the richer authored plugin model used by `pluginmodel` / `pluginmanifest` +- the narrower `integrationctl.IntegrationManifest` + +Current `integrationctl` manifest loading preserves: + +- name +- version +- description +- targets +- derived deliveries +- derived capability surface + +But it currently drops richer authored metadata such as: + +- homepage +- repository +- keywords +- author +- license + +Practical consequence: + +- lifecycle can already use the current `IntegrationManifest` +- storefront catalog cannot get all desired detail fields from the current `IntegrationManifest` alone + +### Packaging nuance from current code + +- evidence registry already has an embedded fallback, which lowers packaging risk +- workspace-lock storage is still repo-root oriented + +Practical consequence: + +- `list`, `doctor`, `add`, `update`, `remove`, and `repair` are the right first app surfaces +- `sync` is not a phase-1 or phase-2 app surface +- `enable` and `disable` can stay out of the first app rollout + +## Current Adapter Truth From Code + +These are backend facts the plan must respect. + +### Claude adapter + +- install mode: `native_cli` +- supports native update: yes +- supports native remove: yes +- supports scopes: `user`, `project` +- does not currently advertise `local` +- requires reload after install + +### Codex adapter + +- install mode: `marketplace_prepare` +- supports native update: no +- supports native remove: no +- supports scopes: `user`, `project` +- does not currently advertise `local` +- requires restart and a new thread +- current inspect logic distinguishes: + - fully installed + - disabled + - prepared but not activated + - degraded + +### Consequence for the app + +The app must treat scope support as backend-owned truth. + +That means: + +- phase 1 and phase 2 should expose only `user` and `project` for plugin-kit-backed universal installs +- if `local` is important later, it must be added as a real backend capability first + +## Architecture Boundary + +### Correct boundary + +- `plugin-kit-ai` = lifecycle engine, universal catalog backend, native discovery backend +- `claude_team` = frontend, state, UX, feature-flagged rollout + +### Wrong boundaries + +- do not embed a second plugin UI +- do not parse human CLI output +- do not scrape universal repo layout directly in `claude_team` +- do not link Go code directly into Electron instead of using the CLI contract + +## Recommended Backend Basis By Surface + +Different backend surfaces should be built on different existing code paths. +Trying to force one internal model to answer every question would make the result worse. + +| Surface | Best current basis in `plugin-kit-ai` | Why | +|---|---|---| +| `catalog` | `pluginmanifest.Inspect` + `publicationmodel` + `targetcontracts` | richer authored metadata and stronger target/output truth | +| `list` | `integrationctl` managed state and current `list` service | managed universal truth already exists here | +| `doctor` | `integrationctl` current `doctor` service | managed drift/auth/activation truth already exists here | +| lifecycle mutate | `integrationctl` facade and adapters | real install/update/remove/repair engine already exists here | +| `discover` | new dedicated native discovery layer built using native surface readers and inspect helpers | external observed truth is different from managed lifecycle truth | + +### Recommended decision + +- build `catalog` from the richer authored-model path +- build `list`, `doctor`, and lifecycle mutations from `integrationctl` +- build `discover` as a new surface that may reuse inspect helpers, but is not just `Inspect` + +This is the cleanest way to avoid overloading one narrow model with too many jobs. + +## Recommended plugin-kit-ai Implementation Ownership + +One of the highest-risk failure modes is building the right contract in the wrong layer. +This section fixes ownership up front. + +### Chosen seam by package + +| Concern | Recommended home in `plugin-kit-ai` | Why | +|---|---|---| +| CLI flags, `--format json`, envelopes, exit semantics | `cli/plugin-kit-ai/cmd/plugin-kit-ai` | command boundary belongs here, not business truth | +| catalog projection for app consumption | new helper near `cli/plugin-kit-ai/internal/pluginmanifest` such as `internal/catalogview` | catalog truth comes from authored inspection and needs a stable projection layer | +| managed lifecycle grouping and integration-level fields | `install/integrationctl/domain` + `install/integrationctl/usecase` | lifecycle truth already lives here and should not be reconstructed in the CLI | +| native discovery orchestration | new discovery usecase under `install/integrationctl/usecase` with types in `install/integrationctl/domain` | discovery is backend truth, not a frontend heuristic | +| target-specific native enumeration and evidence collection | existing adapter packages under `install/integrationctl/adapters/` | adapters already own target-specific path and native-surface knowledge | +| source resolution | `install/integrationctl/adapters/source` | canonical source resolution already lives here | +| workspace-root normalization into target-specific native roots | `install/integrationctl/adapters/pathpolicy` plus adapter path helpers | app must not duplicate `ProjectRoot` vs `EffectiveGitRoot` logic | + +### Raw internal models must not leak as app contracts + +The app should not consume any of these raw internal shapes directly: + +- raw `pluginmanifest.Inspection` +- raw `integrationctl` lifecycle `domain.Report` +- raw adapter `InspectResult` + +Instead: + +- CLI commands should project them into stable app-facing JSON contracts +- projection should happen once in `plugin-kit-ai` +- `claude_team` should consume only those projected contracts + +### Why this chosen seam is safer + +- it keeps target/path/source truth backend-owned +- it avoids Electron reconstructing lifecycle groupings or source provenance +- it allows `plugin-kit-ai` to change internal models without breaking the app contract +- it makes E2E failures easier to localize to one layer + +## Which Backend Surface Answers Which UI Question + +| UI question | Backend surface | Why | +|---|---|---| +| What universal plugins can I install? | `catalog` | storefront availability | +| What universal plugins are already managed? | `list` | managed truth | +| What managed universal plugins need attention? | `doctor` | drift, auth, activation | +| What native plugins already exist outside plugin-kit management? | `discover` | observed external truth | +| Can this plugin be installed for Claude? | `catalog` capability + scope metadata | install intent | +| Can this plugin be installed for Codex? | `catalog` capability + scope metadata | install intent | +| Can I safely uninstall this native external plugin from the app? | `discover.manageability` | destructive authority must come from backend | +| What exactly happened after install/update/remove? | lifecycle result JSON | target-granular mutation truth | + +Rule: + +- if the answer is not available from one of these surfaces, the app should not invent it + +## Backend View Consistency Matrix + +This section makes cross-surface ownership explicit. +It should be possible to answer every “which surface wins?” question from this table alone. + +| Field or question | Winning surface | Allowed fallback | Forbidden fallback | +|---|---|---| +| managed existence | `list` | none | `catalog`, `discover`, app heuristics | +| managed health / degraded / auth-pending | `doctor` | `list` only for neutral installed state | `catalog`, `discover` | +| universal availability | `catalog` | stale cached catalog with explicit stale marker | `discover`, app heuristics | +| native external existence | `discover` | stale cached discovery with explicit stale marker | `catalog`, `list` | +| destructive authority for native external entries | `discover.manageability` | none | app heuristics | +| installability by target/scope | `catalog` plus lifecycle capability projection | conservative disable in app | inferred support from authored target alone | +| managed target result after mutation | lifecycle mutate result | immediate `doctor` refresh | app optimism without payload evidence | +| storefront detail metadata | `catalog` detail projection | explicit missing-detail UI | generated operational docs | + +### Required contradiction handling + +If surfaces disagree: + +- `list` vs `catalog` + - keep the managed entry + - mark catalog/detail support degraded if needed +- `discover` vs `catalog` + - keep both truths + - do not rewrite ownership +- `doctor` vs `list` + - prefer `doctor` for health state + - prefer `list` for managed existence +- lifecycle mutate result vs stale cached `list` + - prefer the fresh mutate payload, then refetch + - do not let stale cache overwrite the mutation outcome + +## Data Flow + +```mermaid +flowchart LR + A["universal-plugins-for-ai-agents"] --> B["plugin-kit-ai catalog"] + C["Claude native surfaces"] --> D["plugin-kit-ai discover"] + E["Codex native surfaces"] --> D + F["plugin-kit-ai state.json"] --> G["plugin-kit-ai list"] + F --> H["plugin-kit-ai doctor"] + I["plugin-kit-ai lifecycle actions"] --> F + B --> J["claude_team Plugins UI"] + D --> J + G --> J + H --> J + J --> I +``` + +## Target Naming and Mapping + +Authored target names and app-facing provider labels are not always the same thing. + +Examples from current code: + +- authored `claude` maps to app/runtime target `claude` +- authored `codex-package` maps to app/runtime target `codex` +- authored `gemini` maps to app/runtime target `gemini` +- authored `cursor` maps to app/runtime target `cursor` +- authored `opencode` maps to app/runtime target `opencode` + +### Surface-specific target id rule + +The same plugin may be described through different target vocabularies depending on the backend surface: + +- authored/catalog truth: + - `claude` + - `codex-package` + - `codex-runtime` + - `gemini` + - `cursor` + - `cursor-workspace` + - `opencode` +- lifecycle-manageable truth: + - `claude` + - `codex` + - `gemini` + - `cursor` + - `opencode` +- app-facing provider labeling: + - `Anthropic` + - `Codex` + - optionally later other providers + +This is already visible in current code: + +- authored plugin metadata and `pluginmanifest` preserve `codex-package` +- `integrationctl` normalizes that into lifecycle target `codex` +- the UI should render a provider lane label such as `Codex`, not leak raw lifecycle ids everywhere + +Recommended rule: + +- never force one single `target` field to carry all three meanings +- preserve target ids separately by surface +- use explicit fields such as: + - `authored_targets` + - `manageable_targets` + - `available_app_targets` + +### Recommended catalog rule + +Catalog entries should preserve both: + +- authored target identifiers +- normalized app/runtime target identifiers + +Why: + +- authored compatibility and generated outputs still care about authored target names +- the app needs stable provider-level labels like `Anthropic` and `Codex` +- this avoids lossy translation + +## App-Facing Target Subset + +`plugin-kit-ai` understands more targets than the `claude_team` plugin page needs to action directly. + +For this integration, the primary app-facing actionable subset should be: + +- `claude` +- `codex-package` + +These map to the current app-facing provider lanes: + +- `Anthropic` +- `Codex` + +### Out of scope for first actionability + +These may still exist in authored metadata, but they should not drive primary install buttons in the first rollout: + +- `codex-runtime` +- `gemini` +- `cursor` +- `cursor-workspace` +- `opencode` + +### Recommended UI rule + +- the backend catalog may preserve full authored target support +- `claude_team` should derive primary provider labels and actions only from the app-relevant subset +- broader target support may appear as secondary detail later, but should not confuse the main install surface + +### Important nuance + +This is not because those other targets are “fake”. +It is because this app rollout has a narrower action surface than the full authored/plugin backend target space. + +For example: + +- `plugin-kit-ai` lifecycle already knows targets like `gemini`, `cursor`, and `opencode` +- `pluginmanifest` and `targetcontracts` know even broader authored/runtime distinctions such as `codex-package` vs `codex-runtime` + +But the first plugin page rollout in `claude_team` should optimize for a clear and reliable main surface, not for exposing the entire backend target universe at once. + +## Catalog Support Projection Rules + +Catalog generation should preserve three different truths without collapsing them: + +### 1. Authored targets + +What the plugin repo declares in `plugin.yaml`. + +Examples: + +- `claude` +- `codex-package` +- `codex-runtime` +- `gemini` +- `cursor` +- `cursor-workspace` +- `opencode` + +### 2. Backend-manageable lifecycle targets + +What the current `integrationctl` lifecycle can actually manage today. + +From current code, that target set is: + +- `claude` +- `codex-package` +- `gemini` +- `cursor` +- `opencode` + +Notably, it does **not** include: + +- `codex-runtime` +- `cursor-workspace` + +### 3. App-primary action targets + +What `claude_team` should expose as first-class install lanes in this rollout. + +Recommended set: + +- `claude` +- `codex-package` + +### Current public target universe in `plugin-kit-ai` + +From current `platformmeta` code, the public target universe is already split into: + +- packaged profiles: + - `claude` + - `codex-package` + - `codex-runtime` +- tooling profiles: + - `gemini` + - `cursor` + - `cursor-workspace` + - `opencode` + +This is useful context because it shows that the backend target universe is intentionally broader than the first plugin-page rollout. + +### Required contract rule + +A catalog entry should be able to preserve all three layers separately, for example: + +- `authored_targets` +- `manageable_targets` +- `primary_action_targets` + +This keeps the system honest: + +- authored truth stays intact +- backend actionability stays explicit +- app UI stays focused + +## Product Model + +### Universal plugins + +Source: + +- [universal-plugins-for-ai-agents/plugins](https://github.com/777genius/universal-plugins-for-ai-agents/tree/main/plugins) + +Properties: + +- installable through `plugin-kit-ai` +- available for one or more targets +- primary browse/search source +- explicit target support labels + +### Native external installed plugins + +Properties: + +- already installed in a native agent surface +- not necessarily managed by `plugin-kit-ai` +- still visible in UI +- clearly labeled by target ownership + +Example labels: + +- `Installed - Anthropic only` +- `Installed - Codex only` + +## Source of Truth Model + +| Surface | Owner | Meaning | +|---|---|---| +| `catalog` | `plugin-kit-ai` | what universal plugins are available | +| `discover` | `plugin-kit-ai` | what native plugins are already observed | +| `list` | `plugin-kit-ai` | what universal plugins are managed | +| `doctor` | `plugin-kit-ai` | health and drift for managed universal plugins | +| renderer cache | `claude_team` | temporary UI cache only | + +### Consistency rules + +- every rendered `universal_installed` entry must be explainable by `list` +- every degraded managed universal entry must be explainable by `doctor` +- `discover` may overlap conceptually with universal entries, but never redefines managed ownership +- `catalog` must not advertise target/scope support that lifecycle will reject under normal supported conditions + +### Freshness and partial-view rules + +These rules matter because `catalog`, `discover`, `list`, and `doctor` do not have the same source or refresh cost. + +- `list` is authoritative for managed existence, even if `catalog` is stale or temporarily missing an entry +- `doctor` is authoritative for managed health, even if `catalog` is stale +- `discover` is authoritative for observed native external existence, unless suppressed by stronger managed-overlap evidence +- failed or stale `catalog` must not make a managed entry disappear from the page +- failed or stale `discover` must not invent that native external entries were removed +- the app may mark data stale, but it must not rewrite ownership because one backend view is temporarily unavailable + +### Consistency invariants the backend contract should preserve + +1. every managed entry returned by `doctor` must also be representable in `list` by the same managed grouping key +2. `discover` must either suppress managed overlap or mark it explicitly, but must not silently contradict `list` +3. `catalog` may omit optional storefront metadata, but must not change canonical `integration_id` +4. `claude_team` must never resolve a contradiction by guessing - it should preserve both truths and degrade the UI honestly + +## Identity, Matching, and Dedupe + +### Universal identity + +Canonical key: + +- `integration_id` + +### Native external identity + +Canonical key: + +- `native_target + native_plugin_id + scope-set` + +### Matching rule + +`matched_integration_id` is advisory only. +It does not convert a native external entry into a universal entry. + +### Match confidence + +Recommended values: + +- `match_confidence`: `exact | heuristic | none` +- `match_basis`: `same_repo_same_plugin_id | same_marketplace_identity | manual_mapping | name_heuristic | unknown` + +### Target-specific matching ladder + +The backend should classify matches conservatively. + +Recommended ladder: + +#### Claude native external -> universal + +`exact` only when the backend can prove the same marketplace identity, for example: + +- same native plugin ref +- or same marketplace identity pair such as: + - plugin id + - marketplace name + +`heuristic` only when: + +- display name matches strongly +- and target is the same +- and there is no stronger conflicting candidate + +`none` when: + +- only loose name similarity exists +- or more than one universal plugin could plausibly match + +Important note: + +- current managed Claude installs use synthetic refs such as `integration_id@integrationctl-` +- native external Claude installs from official or third-party marketplaces may use different marketplace identities +- therefore name equality alone is not enough for `exact` + +#### Codex native external -> universal + +`exact` only when the backend can prove the same native plugin identity, for example: + +- marketplace entry name equals integration id +- and the same plugin reference is observed consistently in: + - marketplace catalog + - config toggle ref + - or managed plugin root path + +`heuristic` only when: + +- marketplace entry name strongly matches integration id +- but not every supporting surface is available + +`none` when: + +- only title-level similarity exists +- or multiple universal integrations could map to the same observed native name + +Important note: + +- current Codex-managed installs use `integration_id` as the marketplace entry name +- they also create related evidence in: + - `.agents/plugins/marketplace.json` + - plugin root path under `plugins/` + - config plugin ref `@` +- that is good raw evidence, but the backend should still keep external discovery conservative + +### Preferred matching evidence by target + +| Target | Strongest evidence | Weaker evidence | Unsafe alone | +|---|---|---|---| +| Claude | plugin ref, marketplace name + plugin id pair | stable display name + target | display name alone | +| Codex | marketplace entry name + config plugin ref + plugin root agreement | marketplace entry name only | title similarity alone | + +Recommended rule: + +- only the strongest evidence column may justify `exact` +- weaker evidence may justify `heuristic` +- the unsafe column must map to `none` + +### Matching invariants + +- `exact` must be explainable from stable identity-bearing fields, not from display text +- `heuristic` must never unlock destructive actions +- `heuristic` must never collapse two entries into one +- if confidence is below `exact`, the UI should treat the relation as advisory only +- the app must not recalculate confidence differently from the backend +- overlap suppression and matching are different decisions +- an entry may be suppressed as managed overlap without ever being exposed as a native external match candidate + +Renderer rule: + +- only strong bases may drive stronger UI hints +- heuristic matches must never auto-merge entries or unlock stronger actions + +## Catalog Production Model + +This must be explicit because the universal repo is the source of truth for available universal plugins, but it must not become the app contract directly. + +### Source of universal entries + +Universal catalog entries should be generated from: + +- `777genius/universal-plugins-for-ai-agents/plugins/*` + +using `plugin-kit-ai`, not using app-side parsing. + +### Recommended pipeline + +1. select a pinned revision of `universal-plugins-for-ai-agents` +2. enumerate plugin directories under `plugins/*` +3. load each plugin through the richer authored plugin model, ideally the same `pluginmanifest.Inspect` path that already exposes: + - manifest metadata + - publication model + - target contract details +4. derive normalized catalog entries +5. write a bundled snapshot for app packaging +6. optionally refresh from the same source later + +### Why `publicationmodel` is helpful but not enough on its own + +Current `publicationmodel.Model` is useful for catalog generation because it already normalizes: + +- package targets +- package families +- channel families +- install model +- authored docs +- managed artifacts + +But by itself it does **not** carry the full storefront metadata set the app wants, such as: + +- homepage +- repository +- keywords +- author +- license + +Those live in the richer authored manifest path exposed through `pluginmanifest.Inspection.Manifest`. + +Recommended rule: + +- catalog generation should use `pluginmanifest.Inspect` as the main authored inspection entry point +- then combine: + - `Inspection.Manifest` for storefront metadata + - `Inspection.Publication` for publication/channel/package projection + - `Inspection.Targets` plus target contracts for support and surface details + +The app must not try to reconstruct that combination on its own. + +### Why this should not use the current narrow lifecycle loader only + +The current `integrationctl` manifest loader is enough for lifecycle planning, but it preserves only: + +- name +- version +- description +- targets +- derived deliveries + +That is not enough on its own for the desired storefront contract. + +Recommended resolution: + +- treat catalog generation as its own backend translation layer +- allow that layer to read richer authored metadata +- still keep the final catalog output normalized and versioned + +### Why `pluginmanifest.Inspect` is a better catalog basis than the current lifecycle loader + +From current code, `pluginmanifest.Inspect` already carries much richer input than `integrationctl.Loader`, including: + +- authored manifest metadata +- publication model +- target contract fields such as install model, activation model, native root, portable kinds, native surfaces, and managed artifacts + +That makes it a much better source for storefront and support badges. + +### Performance boundary + +Current source resolution for lifecycle work may clone GitHub or git URL sources. +That is acceptable for install/update style mutations. + +It is not acceptable as the way to build the storefront catalog. + +Catalog generation should work from: + +- a pinned local checkout +- a prepared snapshot +- or another batch-friendly backend path + +It should not resolve each storefront entry by cloning sources independently at runtime. + +### Alias rule + +The first-party alias map is useful for CLI shortcuts like `plugin-kit-ai add notion`. +It is not the storefront contract. + +## Source Reference Semantics + +Source semantics must stay explicit. +This is another place where the app should not invent meaning. + +### Current lifecycle source truth + +From current `integrationctl` code, lifecycle source resolution already distinguishes: + +- requested source ref +- resolved source ref +- local materialized path +- source digest + +Examples: + +- local path request: + - requested kind `local_path` + - resolved kind `local_path` +- GitHub repo-path request: + - requested kind `github_repo_path` + - resolved kind `git_commit` +- git URL request: + - requested kind `git_url` + - resolved kind `git_commit` + +This is good and should be preserved in the app-facing lifecycle contract. + +### Alias semantics + +Current first-party aliases are only convenience input forms such as: + +- `context7` +- `stripe` +- `notion` + +They resolve to concrete GitHub repo-path refs under the universal plugin repository. + +Recommended rule: + +- aliases are accepted CLI input +- aliases are not canonical identity +- aliases should not become the only stored source value in app state + +### Catalog source semantics + +Catalog source semantics are different from lifecycle source semantics. + +For the first app rollout, catalog entries should be treated as coming from a curated catalog snapshot with its own provenance, for example: + +- snapshot source kind +- catalog revision +- generated-by backend version + +The catalog should not pretend that every card was individually resolved through runtime lifecycle source resolution. + +### Recommended contract rule + +Keep these source layers separate: + +- `requested_source_ref` + - lifecycle input truth +- `resolved_source_ref` + - lifecycle resolved truth +- `catalog_source` + - storefront snapshot provenance + +The app should never collapse those into one ambiguous `source` string. + +### Recommended UI rule + +- install detail may show requested and resolved source refs for managed universal installs +- storefront cards should usually show catalog provenance only when needed for debugging or advanced detail +- aliases may be accepted in user-facing install flows, but the stored and rendered lifecycle truth should remain normalized requested/resolved refs + +## Workspace Root and Project Scope Semantics + +Project-scoped installs need one more rule-set because current target adapters do not all interpret project roots the same way. + +### Current code reality + +`plugin-kit-ai` already distinguishes: + +- user scope +- project scope +- stored `workspace_root` on managed installation records + +But current adapters derive effective native roots differently: + +- `Claude` + - project settings path uses `ProjectRoot(workspace_root, project_root)` +- `Codex` + - project marketplace root uses `EffectiveGitRoot(workspace_root, project_root)` +- `OpenCode` + - project assets/config roots also use `EffectiveGitRoot(workspace_root, project_root)` +- `Cursor` + - project config path currently uses `ProjectRoot(workspace_root, project_root)` + +This means the same raw workspace path can lead to different effective native roots depending on target semantics. + +### Why this matters + +If the app assumes one global meaning for `workspace_root`, it can easily: + +- install into the wrong repo root +- render the wrong project target path in detail +- refresh the wrong context after mutation +- mis-explain project scope differences between providers + +### Recommended rule + +- the app passes the raw user-selected `workspace_root` +- the backend owns target-specific effective-root normalization +- the app must not try to emulate `EffectiveGitRoot` or `ProjectRoot` logic itself + +### Recommended contract additions + +Where useful, lifecycle and discovery payloads may expose explicit derived fields such as: + +- `workspace_root` + - raw project context that was requested or persisted +- `effective_native_root` + - target-specific derived root actually used for native files +- `native_scope_root` + - optional friendlier alias if that reads better in the contract + +Phase-1 minimum: + +- `workspace_root` is required for project-scoped managed installs +- missing project `workspace_root` must remain a hard backend error +- target-specific effective root may be additive if not ready immediately + +### Recommended app rule + +- UI selection should talk in terms of the chosen project/workspace +- backend detail and diagnostics may show the effective native root when it differs +- app logic for mutation, refresh, and cache keys should continue to use the raw workspace context plus target, not a home-grown rewritten root + +### Why this should stay backend-owned + +Current target adapters already encode platform-specific expectations. + +Examples: + +- Codex project installs intentionally anchor to effective git root +- Claude project installs intentionally target project-local settings path + +Trying to centralize those rules in Electron would duplicate platform policy and create drift. + +## Metadata Truth Table + +One of the biggest ways this migration can go wrong is promising metadata that the backend does not actually know reliably. + +### Metadata that already exists today in authored source + +From current authored plugin source, `plugin-kit-ai` can already obtain: + +- `integration_id` +- `version` +- `description` +- `homepage` +- `repository` +- `license` +- `keywords` +- declared `targets` + +### Important caveat + +The current `integrationctl` manifest loader does not carry all of those fields forward today. + +So there are two safe paths: + +1. extend `integrationctl` manifest loading to preserve richer metadata +2. build `catalog` from the richer authored-model path and translate it into the catalog contract + +What must not happen: + +- the app inventing those fields +- the app scraping raw repo files directly +- two different backend paths returning contradictory storefront metadata + +### Metadata the backend can derive safely + +The backend can also derive: + +- generated `delivery_kinds` +- `available_targets` +- `supported_scopes_by_target` from target adapter capabilities +- `readme` location using a stable default rule +- provenance fields such as source ref, revision, manifest digest, generated-by version + +### Metadata that is not a safe phase-1 assumption + +These fields are optional curation data, not phase-1 requirements: + +- `category` +- `icon_url` +- `install_count` +- `popularity` +- `featured_rank` + +### Recommended default + +Phase 1 should require only: + +- name +- description +- version +- homepage / repository +- keywords +- target support +- scope support +- README/detail + +If `category`, `icon`, or popularity are absent, the UI should hide or degrade those features honestly. + +## Storefront Detail and README Semantics + +The plugin detail surface must use the authored human guide, not arbitrary generated root docs. + +### Current code reality + +`plugin-kit-ai` already generates root-facing docs such as: + +- `README.md` +- `GENERATED.md` +- boundary guidance docs + +But those are operational/generated root entrypoints. +They are not the best source of storefront detail content. + +Current code also makes the authored README explicit: + +- the managed root `README.md` points readers back to `plugin/README.md` +- generated docs inventory also treats root docs differently from managed outputs + +That is a strong signal that the authored README remains the source of truth for human-facing plugin detail. + +### Recommended detail rule + +For universal catalog entries, the default detail source should be: + +- authored `plugin/README.md` + +Not: + +- generated root `README.md` +- `GENERATED.md` +- boundary docs like `AGENTS.md` + +### Why this matters + +If the app accidentally uses generated root docs as storefront detail: + +- the detail view becomes noisy and operational +- it may emphasize generate/normalize workflows instead of plugin value +- the same plugin can appear to have unstable detail content depending on packaging mode + +### Recommended contract shape + +The catalog contract should prefer an explicit detail reference such as: + +- `detail_kind: "authored_readme"` +- `detail_ref` + +Recommended phase-1 default: + +- `detail_kind = "authored_readme"` +- `detail_ref` points to the authored README location within the catalog source snapshot + +### Recommended app rule + +- plugin cards use catalog summary fields +- plugin detail loads the authored detail reference when available +- if detail content is missing, the app should degrade honestly instead of substituting generated operational docs + +## Catalog Field Ownership Matrix + +This makes the contract much easier to implement because it is explicit about where each field should come from. + +| Catalog field | Recommended source in `plugin-kit-ai` | Notes | +|---|---|---| +| `integration_id` | authored manifest `name` | stable universal identity | +| `display_name` | authored manifest `name` initially | future curation can improve presentation | +| `description` | authored manifest `description` | required | +| `version` | authored manifest `version` | required | +| `homepage_url` | richer authored model | not present in current narrow lifecycle manifest | +| `repository_url` | richer authored model | not present in current narrow lifecycle manifest | +| `keywords` | richer authored model | phase-1 safe metadata | +| `authored_targets` | authored manifest `targets` | preserve exact authored truth | +| `manageable_targets` | lifecycle target mapping / registered adapters | what backend can actually act on | +| `primary_action_targets` | app rollout policy | narrower than full backend target universe | +| `available_app_targets` | backend target projection | provider-facing labels for `claude_team` | +| `supported_scopes_by_target` | target adapter capabilities | backend-owned truth | +| `capabilities` | delivery mapping and/or target contract data | do not invent in app | +| `readme_url` | catalog translation layer | use a stable default rule | +| `category` | optional curation metadata | do not block phase 1 | +| `icon_url` | optional curation metadata | do not block phase 1 | +| `catalog_revision` | catalog generation pipeline | provenance | +| `generated_by_plugin_kit_version` | CLI/backend build info | provenance | + +### Important rule + +If a field does not have a trustworthy backend source yet, phase 1 should omit or degrade it instead of synthesizing it in the app. + +## Effective Metadata Projection + +Catalog generation must distinguish between: + +- shared plugin metadata +- target-specific effective metadata + +This matters because current `plugin-kit-ai` code already allows target-specific metadata overlays, especially for package-style targets such as `codex-package`. + +### Shared metadata + +Shared metadata should come from the authored manifest layer: + +- `name` +- `version` +- `description` +- base `homepage` +- base `repository` +- base `license` +- base `keywords` +- base `author` + +This is the safest metadata for: + +- mixed-target storefront cards +- cross-target search +- universal identity + +### Target-specific effective metadata + +For some targets, especially `codex-package`, the effective generated package metadata is: + +- base manifest metadata +- plus allowed target-specific overrides from `targets//package.yaml` + +Current code already proves this path exists: + +- `codex-package` generation merges base manifest metadata with optional `targets/codex-package/package.yaml` +- validation checks the generated Codex package metadata against that merged expectation + +### Recommended catalog rule + +The catalog contract should preserve both layers explicitly: + +- shared metadata for the universal entry itself +- optional `effective_target_metadata` for targets that project different package metadata + +Example shape: + +- `shared_metadata` +- `effective_target_metadata.codex` + +At minimum, per-target effective metadata may include: + +- `homepage` +- `repository` +- `license` +- `keywords` +- `author` + +### Recommended UI rule + +- list cards should use shared metadata +- provider-specific effective metadata should appear only in target detail sections or provider-specific support details +- the app must not silently replace universal card metadata with one target's override + +Why: + +- otherwise a `codex-package` override could accidentally become the visible truth for a plugin that is still conceptually universal +- that would make shared cards unstable and misleading across providers + +### Recommended phase-1 default + +If effective target metadata is not yet emitted in the contract: + +- use shared metadata only +- do not guess target-specific homepage/repository/license in the app +- add effective target metadata later as an additive contract field + +### Conservative phase-1 metadata rule + +For phase 1, treat target-specific effective metadata as enhancement, not as a dependency. + +That means: + +- search, ranking, and primary cards use shared metadata only +- target-specific metadata appears only when backend emits it explicitly +- absence of effective target metadata must never block installability rendering +- the app must not read target-specific docs or package manifests directly to reconstruct this layer + +## Native Discovery Model + +`discover` is a genuinely new backend surface. +It must not be implemented as a thin wrapper over current `List` or current per-target `Inspect`. + +### Why current per-target `Inspect` is not a safe discovery backend + +This needs to be explicit because reusing current adapter `Inspect` can look tempting, but it is the wrong abstraction for external discovery. + +#### Claude + +Current Claude inspect logic is still strongly lifecycle-oriented: + +- it resolves inspect identity from: + - `in.IntegrationID` + - or `in.Record.IntegrationID` +- native plugin-list confirmation then looks for a specific plugin ref: + - defaulting to `integration_id@integrationctl-` + - or a managed plugin ref from record metadata +- if that specific confirmation path does not resolve, current inspect can still fall back to `installed` when native files or CLI availability make the managed candidate look plausible + +That is appropriate for managed lifecycle verification. +It is not appropriate for general native external discovery, because external installs may use: + +- a different marketplace name +- a different plugin ref +- or no managed lifecycle record at all + +#### Codex + +Current Codex inspect logic is also lifecycle-oriented: + +- inspect inputs derive `integration_id` from the managed record +- scope/path construction depends on that managed integration identity +- state classification assumes it is inspecting a known candidate plugin root +- current observed-surface logic is built around a specific expected catalog path, plugin root, and config path +- current lifecycle classification then reasons from managed cache presence plus that expected surface bundle + +That is useful for verifying a managed installation. +It is not enough for general native external enumeration, which first needs to discover candidates before it can classify them. + +### Recommended backend rule + +- `discover` must be its own scanner-oriented surface +- it may reuse helper functions from adapters where useful +- but it must not simply loop over current adapter `Inspect` without an independent candidate-enumeration layer + +### Critical discovery anti-patterns + +These are explicitly forbidden: + +1. calling current adapter `Inspect` on arbitrary filesystem hits and treating the result as native discovery truth +2. suppressing a discovered entry before comparing it against managed lifecycle evidence +3. upgrading a heuristic name match into an exact overlap suppression signal +4. deriving native manageability from UI assumptions instead of backend evidence +5. hiding a discovered entry only because catalog lookup failed or was stale + +If implementation pressure pushes toward any of these shortcuts, the correct fix is to extend backend discovery evidence, not to make the app smarter. + +### Practical implementation shape + +Recommended backend structure: + +1. enumerate native candidates from target-specific sources +2. derive native identity and evidence for each candidate +3. suppress candidates already explained by managed lifecycle state +4. classify observed state +5. compute advisory relation to universal catalog +6. emit normalized discovery entries + +This keeps `discover` honest: + +- enumeration first +- classification second +- matching last +- read-only throughout + +### Claude discovery sources + +- `~/.claude/plugins/installed_plugins.json` +- `~/.claude/settings.json` +- `/.claude/settings.json` +- `/.claude/settings.local.json` + +### Codex discovery sources + +- `~/.agents/plugins/marketplace.json` +- `/.agents/plugins/marketplace.json` +- `~/.agents/plugins/plugins/` +- `/.agents/plugins/plugins/` +- `~/.codex/plugins/cache///local` +- `~/.codex/config.toml` + +### Required observed states + +- `observed_active` +- `observed_disabled` +- `observed_prepared` +- `observed_degraded` + +### Recommended observed-state derivation rules + +Observed-state classification should be target-specific and evidence-driven. + +#### Codex + +Current adapter code already implies a practical state ladder: + +- `observed_active` + - cache bundle exists + - and marketplace catalog + plugin root are present + - and config does not mark the plugin disabled +- `observed_disabled` + - cache bundle exists + - and config toggle is present and disabled +- `observed_prepared` + - marketplace entry exists and plugin root exists + - but activation evidence such as cache bundle is not present yet +- `observed_degraded` + - only part of the expected prepared/install surface exists + - or cache exists while managed marketplace source is missing or drifted + +Important rule: + +- `discover` should keep this richer observed-state truth +- the app should not collapse everything into plain `installed/not installed` + +### Codex evidence mapping table + +The first implementation should stay conservative and evidence-driven. + +| Evidence seen by discovery | Recommended observed state | Why | +|---|---|---| +| marketplace entry + plugin root + installed cache, config not disabled | `observed_active` | strongest “prepared and activated” signal available today | +| installed cache + config toggle present and disabled | `observed_disabled` | disable state is explicit | +| marketplace entry + plugin root, but no installed cache yet | `observed_prepared` | package is staged but native activation is not complete | +| only one of marketplace entry or plugin root exists | `observed_degraded` | partial native surface | +| installed cache exists but marketplace entry or plugin root is missing | `observed_degraded` | drifted or partially removed managed/native surface | +| config references plugin, but marketplace entry and plugin root are both absent | `observed_degraded` | stale toggle or orphaned config | + +### Conservative phase-1 defaults for Codex discovery + +Until discovery evidence is richer, prefer these defaults: + +- if evidence is ambiguous, downgrade to `observed_degraded` +- do not claim `observed_active` from config evidence alone +- do not infer exact universal matching from marketplace entry name alone +- do not infer safe removal from discovered Codex paths alone +- do not suppress a discovered Codex entry unless managed-overlap evidence includes owned objects or stable lifecycle evidence + +#### Claude + +For phase 1, Claude discovery may stay simpler: + +- `observed_active` + - plugin appears in native plugin list and is enabled +- `observed_disabled` + - plugin appears in native plugin list and is disabled +- `observed_degraded` + - settings or install evidence exists but plugin list cannot confirm a clean state +- `observed_prepared` + - optional future state only if backend gains a meaningful pre-install or staged marketplace concept for external Claude installs + +Recommended rule: + +- do not force artificial state parity between Claude and Codex +- preserve richer Codex states where the backend can actually justify them + +### Required extra fields + +- `native_target` +- `native_plugin_id` +- `installed_scopes` +- `detected_source` +- `manageability` +- `matched_integration_id` +- `match_confidence` +- `match_basis` +- `identity_evidence` +- `activation_hint` + +### Discovery manageability rule + +The backend must declare whether a native external entry is: + +- `display_only` +- `safe_remove` +- `safe_adopt` +- or another explicit future mode + +The app must not infer destructive authority. + +### Discovery overlap suppression rule + +`discover` must not blindly report every observed native install as `native_external_installed`. + +Why this is necessary: + +- managed universal installs also materialize into native agent surfaces +- a naive scanner would rediscover those same installs and duplicate them as native external entries + +Recommended backend rule: + +- `discover` should load managed lifecycle state, or equivalent managed evidence, before finalizing external entries +- if an observed native install is already explained by managed lifecycle state with high confidence, it should either: + - be suppressed from discovery output, or + - be explicitly marked as managed overlap for app-side filtering + +Recommended default: + +- suppress managed-overlap entries in the discovery payload +- discovery should describe only installs that are not already explained by managed lifecycle state + +### Managed-overlap evidence examples + +Claude examples from current code: + +- synthetic marketplace name pattern `integrationctl-` +- managed plugin ref recorded in lifecycle metadata +- managed materialized marketplace root under `~/.plugin-kit-ai/materialized/claude/` + +Codex examples from current code: + +- managed plugin root under `.agents/plugins/plugins/` +- managed catalog entry name equal to `integration_id` together with lifecycle-owned native objects +- managed config ref `@` when that catalog name is already tied to a managed installation + +Important rule: + +- suppression should prefer owned native object evidence and managed lifecycle state over name heuristics +- name equality alone is not enough to classify something as managed overlap + +## Managed Lifecycle Model + +### Existing lifecycle surfaces + +- `list` for managed installations +- `doctor` for managed drift / activation / auth attention +- `add`, `update`, `remove`, `repair` for mutations + +### Important current gap + +Current `Report.Targets` do not identify which integration a target belongs to. + +The app needs lifecycle JSON to include integration-level context such as: + +- `integration_id` +- `managed_entry_key` +- source refs +- policy scope +- workspace root + +### Why the current raw lifecycle report is not enough for app integration + +Today the raw `integrationctl` lifecycle query shape is still too flat for the plugin page. + +In current code: + +- `domain.Report` contains only: + - `summary` + - `targets` + - `warnings` +- `domain.TargetReport` contains per-target state such as: + - `target` + - `delivery_kind` + - `state` + - `activation_state` + - `environment_restrictions` + - `manual_steps` + +What it does **not** preserve at the same level: + +- `integration_id` +- `requested_source_ref` +- `resolved_source_ref` +- `resolved_version` +- `policy.scope` +- `workspace_root` +- a stable grouping boundary between one integration and another + +That matters because the app needs to render cards and detail views at the integration-entry level, not as an ungrouped stream of target facts. + +Recommended rule: + +- `plugin-kit-ai` should keep its current internal normalized lifecycle model +- but the app-facing JSON contract must expose managed entries grouped by integration +- `claude_team` must not try to reconstruct integration grouping from flat target rows by heuristics + +### Required grouped lifecycle identifiers + +For app-facing lifecycle JSON, each managed entry should include at minimum: + +- `managed_entry_key` +- `integration_id` +- `requested_source_ref` +- `resolved_source_ref` +- `resolved_version` +- `policy.scope` +- `workspace_root` + +Recommended rule: + +- `managed_entry_key` should be stable for one stored installation record +- it should not depend on target row order +- it should be safe for the app to use as the primary cache and merge key for managed lifecycle entries + +Without this, the frontend will eventually drift into reconstructing groups from target arrays, which is fragile and unnecessary. + +### Conservative phase-1 grouped lifecycle rule + +Until the backend exposes a more formal record identifier, phase 1 should still require: + +- one grouped managed entry per stored installation record +- stable ordering of `managed_entries` +- stable ordering of nested `targets` +- explicit grouping keys in payload, not implied grouping by adjacent rows + +The app must treat missing grouping keys as a compatibility problem, not as an invitation to reconstruct them heuristically. + +### Critical CLI semantic to freeze + +Current `integrations` mutating commands default to `--dry-run=true`. + +That is good for humans in a terminal, but dangerous for app integration. + +Recommended rule: + +- machine-readable mutating calls from `claude_team` must always pass explicit execution mode +- either: + - `--dry-run=false`, or + - a future clearer flag such as `--apply` + +The app must never rely on CLI defaults for mutating behavior. + +## JSON Contract Style + +`plugin-kit-ai` already has a public JSON contract style in surfaces like `validate` and `publication`. +The new integrations contracts should follow that style instead of inventing a second JSON dialect. + +### Required envelope rules + +- top-level `format` +- top-level `schema_version` +- explicit request context fields where relevant +- top-level `warning_count` +- top-level `warnings` +- one canonical payload field rather than many competing summary shapes + +### Requested context rule + +Current public JSON reports in `plugin-kit-ai` already use request-context fields such as: + +- `requested_target` +- `requested_platform` + +Recommended rule for the integrations surfaces: + +- include explicit request-context fields when the command accepts them +- examples: + - `requested_targets` + - `requested_scope` + - `requested_workspace_root` + - `requested_integration_id` + +This makes automation and debugging much safer than inferring invocation context from payload shape. + +### Required array guarantees + +In schema version `1`, the following fields should be arrays, never `null`: + +- `warnings` +- `entries` +- `managed_entries` +- `targets` + +### Compatibility rules + +- additive fields are allowed within the same `schema_version` +- semantic changes to existing fields require a new `schema_version` +- removing a field the app depends on requires a new `schema_version` +- enum meaning changes require a new `schema_version` + +### App behavior on unsupported versions + +If the backend returns a newer unsupported schema: + +- read-only views may continue only if safe +- lifecycle actions must be disabled +- the UI must explain the compatibility mismatch clearly + +### Process exit and payload semantics + +This must be explicit because current `plugin-kit-ai` already has public JSON commands that can: + +- print a valid JSON payload to stdout +- then still exit non-zero because the payload describes a failing or issue-bearing report + +Current examples in code: + +- `validate --format json` +- `publication doctor --format json` + +Recommended rule for the integrations surfaces: + +- stdout JSON is the canonical machine-readable payload +- process exit code is still meaningful, but it must not be the only signal the app uses +- `claude_team` should: + - first attempt to parse a valid JSON payload from stdout + - then interpret payload-level fields such as `outcome`, `ok`, `warning_count`, `failure_count`, `issue_count` + - only fall back to process-exit-only handling when no valid contract payload exists + +Without this rule, the app will misclassify structured partial failures as transport failures. + +### Recommended outcome semantics + +For machine-readable integrations surfaces, outcome should be explicit in the payload instead of inferred only from process exit: + +- read-only reports: + - may expose `ok`, `warning_count`, and optional `issue_count` +- mutating results: + - should expose explicit `outcome` + - recommended values: + - `planned` + - `applied` + - `partial_success` + - `failed` + +The exact enum may still evolve, but the contract must keep payload-level outcome explicit. + +### Recommended identifiers + +- `plugin-kit-ai/integrations-report` +- `plugin-kit-ai/integrations-result` +- `plugin-kit-ai/integrations-catalog` +- `plugin-kit-ai/integrations-discovery` + +## Recommended Contract Drafts + +These drafts are intentionally close to the current `integrationctl` domain model. +They should extend the existing normalized result shape, not invent a second unrelated response model. + +### Managed lifecycle list + +```json +{ + "format": "plugin-kit-ai/integrations-report", + "schema_version": 1, + "report_kind": "managed_list", + "requested_targets": [], + "warning_count": 0, + "warnings": [], + "summary": "1 managed integration(s) in state.", + "managed_entries": [ + { + "managed_entry_key": "project:/repo:context7", + "integration_id": "context7", + "requested_source_ref": { + "kind": "github_repo_path", + "value": "github:777genius/universal-plugins-for-ai-agents//plugins/context7" + }, + "resolved_source_ref": { + "kind": "git_commit", + "value": "https://github.com/777genius/universal-plugins-for-ai-agents@abc123" + }, + "resolved_version": "0.1.0", + "workspace_root": "/repo", + "policy": { + "scope": "project", + "auto_update": true, + "adopt_new_targets": "manual" + }, + "targets": [ + { + "target_id": "claude", + "delivery_kind": "claude-marketplace-plugin", + "capability_surface": ["mcp"], + "state": "installed", + "activation_state": "reload_pending", + "source_access_state": "ok" + } + ] + } + ] +} +``` + +### Universal catalog + +```json +{ + "format": "plugin-kit-ai/integrations-catalog", + "schema_version": 1, + "requested_targets": ["claude", "codex"], + "warning_count": 0, + "warnings": [], + "source": { + "kind": "bundled_snapshot", + "fetched_at": "2026-04-18T12:00:00Z", + "revision": "abc123", + "stale": false + }, + "entries": [ + { + "entry_kind": "universal_catalog", + "integration_id": "context7", + "display_name": "Context7", + "description": "Shared MCP plugin for documentation lookup.", + "authored_targets": ["claude", "codex-package"], + "manageable_targets": ["claude", "codex-package"], + "primary_action_targets": ["claude", "codex-package"], + "available_app_targets": ["claude", "codex"], + "keywords": ["mcp", "docs"], + "category": null, + "homepage_url": "https://context7.com", + "repository_url": "https://github.com/upstash/context7", + "readme_url": "https://raw.githubusercontent.com/777genius/universal-plugins-for-ai-agents/main/plugins/context7/plugin/README.md", + "version": "0.1.0", + "effective_target_metadata": { + "codex": { + "homepage_url": "https://context7.com", + "repository_url": "https://github.com/upstash/context7", + "keywords": ["mcp", "docs"] + } + }, + "supported_scopes_by_target": { + "claude": ["user", "project"], + "codex": ["user", "project"] + }, + "capabilities": ["mcp"], + "source_ref": "github:777genius/universal-plugins-for-ai-agents//plugins/context7", + "catalog_revision": "abc123", + "generated_by_plugin_kit_version": "0.0.0" + } + ] +} +``` + +### Native discovery + +```json +{ + "format": "plugin-kit-ai/integrations-discovery", + "schema_version": 1, + "requested_targets": ["claude", "codex"], + "requested_workspace_root": "/repo", + "warning_count": 0, + "warnings": [], + "entries": [ + { + "entry_kind": "native_external_installed", + "native_target": "claude", + "native_plugin_id": "context7@claude-plugins-official", + "display_name": "Context7", + "description": "Installed from Claude marketplace.", + "installed_scopes": ["user"], + "detected_source": "claude_marketplace", + "manageability": "display_only", + "matched_integration_id": "context7", + "match_confidence": "exact", + "match_basis": "same_marketplace_identity", + "identity_evidence": [ + "native_plugin_ref=context7@official-marketplace", + "marketplace_name=official-marketplace" + ], + "observed_state": "observed_active", + "activation_hint": "none" + } + ] +} +``` + +### Mutating lifecycle result + +```json +{ + "format": "plugin-kit-ai/integrations-result", + "schema_version": 1, + "requested_integration_id": "context7", + "requested_targets": ["claude", "codex"], + "requested_scope": "project", + "requested_workspace_root": "/repo", + "ok": false, + "warning_count": 0, + "warnings": [], + "outcome": "partial_success", + "report": { + "operation_id": "add-context7-...", + "summary": "Managed targets processed for integration \"context7\".", + "integration_id": "context7", + "targets": [ + { + "target_id": "claude", + "action_class": "install_target", + "state": "installed", + "activation_state": "reload_pending", + "environment_restrictions": ["reload_required"], + "manual_steps": ["reload Claude plugins"] + }, + { + "target_id": "codex", + "action_class": "install_target", + "state": "activation_pending", + "activation_state": "restart_pending", + "environment_restrictions": ["native_activation_required", "new_thread_required"], + "manual_steps": ["restart Codex", "open a new thread"] + } + ] + } +} +``` + +## Entry Derivation and Conflict Resolution + +This is the most important renderer rule-set in the whole integration. + +The app must derive the plugin list deterministically from four backend surfaces: + +- `catalog` +- `discover` +- `list` +- `doctor` + +It must **not** invent entries or merge classes by guesswork. + +### Backend surface ownership + +Each backend surface owns a different truth: + +- `catalog` + - universal storefront truth + - metadata for universal plugins + - target support projection + - provider-facing installability projection +- `list` + - managed universal installed truth + - policy scope + - workspace root + - resolved source + - installed targets +- `doctor` + - managed universal health augmentation + - degraded/auth/activation attention + - target-level restrictions and manual steps +- `discover` + - native external installed truth + - observed scopes + - detected source + - explicit manageability + - optional relation hints to universal entries + +### Entry derivation algorithm + +Recommended deterministic algorithm: + +1. Load `list` and build a managed map keyed by `integration_id`. +2. Overlay `doctor` onto that managed map by `integration_id + target_id`. +3. Load `catalog` and build a universal catalog map keyed by `integration_id`. +4. For every managed entry: + - create one `universal_installed` entry + - enrich it from matching catalog metadata when available + - keep lifecycle-owned fields from `list/doctor` +5. For every catalog entry without a managed match: + - create one `universal_available` entry +6. For every discovery entry: + - create one `native_external_installed` entry + - keep it separate even if it matches a universal integration +7. Attach relation metadata between `native_external_installed` and universal entries only as advisory linkage, never as a merge. +8. Sort using the installed-first ranking rules already defined in this plan. + +### Non-negotiable derivation invariants + +- `doctor` may augment managed entries, but must never create standalone entries +- `catalog` may create only universal entries +- `discover` may create only native external entries +- `catalog` must never mark an entry as installed +- `discover` must never mark an entry as managed +- advisory matching must never change `entry_kind` +- if two surfaces disagree, the surface that owns that truth wins + +### Field precedence rules + +For `universal_installed` entries: + +- identity: + - from `list` +- installed state: + - `doctor` if present + - otherwise `list` +- lifecycle actions: + - from lifecycle capabilities and current runtime support +- scope and workspace root: + - from `list` +- display metadata: + - `catalog` first + - lifecycle fallback only when catalog is missing + +For `universal_available` entries: + +- identity and metadata: + - from `catalog` +- supported targets and scopes: + - from `catalog` +- installed state: + - none + +For `native_external_installed` entries: + +- identity: + - from `discover` +- observed state and scopes: + - from `discover` +- manageability: + - from `discover` +- relation to universal entries: + - advisory only + +### Conflict resolution rules + +If `catalog` says a universal plugin exists, but `list` has no managed installation: + +- render `universal_available` + +If `list` has a managed installation, but `catalog` entry is missing because the catalog snapshot is stale or incomplete: + +- still render `universal_installed` +- mark catalog metadata as unavailable +- do not hide the installed entry + +If `discover` finds a native external install that strongly matches a universal plugin: + +- show both entries +- add relation hints such as `Also available as universal plugin` +- do not collapse them into one card + +If `doctor` returns a degraded target while `list` shows the same target as installed: + +- `doctor` wins for health/status presentation +- `list` remains the source of integration ownership and policy context + +### Renderer field ownership + +Recommended renderer ownership matrix: + +- `entry_kind` + - derived by the app from surface class, never from heuristics +- `integration_id` + - `catalog` or `list` + - never guessed from discovery display name alone +- `display_name` + - `catalog` for universal entries + - `discover` for native external entries +- `description` + - `catalog` for universal entries + - `discover` for native external entries +- `supported_targets` + - `catalog` +- `manageable_targets` + - `catalog` +- `primary_action_targets` + - `catalog` +- `installed_targets` + - `list`, augmented by `doctor` +- `health_state` + - `doctor` +- `observed_scopes` + - `discover` +- `manageability` + - `discover` +- `resolved_source_ref` + - `list` +- `workspace_root` + - `list` + +### Why this section matters + +Without these derivation rules, the app will almost certainly drift into one of the following failure modes: + +- silently merging universal and native external installs +- showing `available` when something is already installed +- losing managed entries when the catalog snapshot is stale +- inventing manageability for discovered native installs +- letting `catalog` or `discover` override lifecycle truth they do not own + +## Worked Examples + +These examples are intentionally concrete. +They should be used as golden fixtures for both backend contracts and app normalization. + +### Example 1 - managed universal install with overlap suppression + +Situation: + +- `catalog` contains universal `context7` +- `list` contains managed installation `context7` +- `doctor` says target is healthy +- native surfaces also visibly contain the installed plugin because managed lifecycle already materialized it there + +Expected result: + +- render one `universal_installed` entry for `context7` +- do **not** render a second `native_external_installed` copy for the same managed install +- health comes from `doctor` +- metadata comes from `catalog` + +Why: + +- discovery overlap suppression should remove the duplicate native observation + +### Example 2 - native Claude marketplace install with no managed lifecycle record + +Situation: + +- `catalog` contains universal `context7` +- `list` does not contain `context7` +- `discover` finds a Claude-native install from a marketplace source +- backend can only establish an advisory relation to universal `context7` + +Expected result: + +- render one `native_external_installed` entry +- optionally render the universal `context7` catalog card separately if not installed through `plugin-kit-ai` +- show relation hint such as `Also available as universal plugin` +- do not collapse them into one entry +- do not show destructive managed actions unless discovery explicitly says they are safe + +### Example 3 - stale or partial catalog snapshot + +Situation: + +- `list` contains managed `demo-plugin` +- `doctor` contains managed `demo-plugin` +- `catalog` snapshot does not contain `demo-plugin` + +Expected result: + +- still render `demo-plugin` as `universal_installed` +- preserve lifecycle actions and managed state +- degrade catalog-derived metadata gracefully +- do not hide the entry just because storefront metadata is missing + +Why: + +- managed installation truth belongs to `list` +- catalog absence is not permission to erase installed truth + +### Example 4 - Codex prepared but not activated yet + +Situation: + +- `discover` finds: + - marketplace entry + - plugin root + - no cache bundle yet +- backend classifies the Codex plugin as prepared but not active + +Expected result: + +- render `native_external_installed` +- show observed state `prepared` +- do not claim it is fully active +- if the backend has an activation hint, show it +- do not pretend this equals a healthy managed universal install + +### Example 5 - degraded Codex native install + +Situation: + +- cache bundle exists +- but expected marketplace source or plugin root is missing or drifted + +Expected result: + +- render `native_external_installed` +- show observed state `degraded` +- do not auto-remove or auto-adopt +- keep relation to universal entries advisory only + +### Example 6 - heuristic match only + +Situation: + +- `discover` finds native plugin display name `Notes` +- `catalog` contains more than one plausible universal candidate with similar wording + +Expected result: + +- either no match or `heuristic` +- no merge +- no stronger action unlock +- no `exact` badge or stronger “same plugin” copy + +## Command Semantics Matrix + +The app integration should treat command classes differently. + +| Surface | Command class | Side effects | App expectation | +|---|---|---|---| +| `catalog` | read-only | none | safe to retry, safe to cache | +| `discover` | read-only | none | safe to retry, safe to cache | +| `list` | read-only | none | safe to retry, source of managed ownership | +| `doctor` | read-only | none | may report issues and still return structured JSON | +| `add` | mutating | yes | must pass explicit apply mode and explicit context | +| `update` | mutating | yes | must pass explicit apply mode | +| `remove` | mutating | yes | must pass explicit apply mode | +| `repair` | mutating | yes | must pass explicit apply mode | + +### Read-only command rules + +- read-only commands must never mutate native state +- read-only commands may return structured issues without that meaning a transport failure +- the app may cache read-only payloads +- the app may retry read-only commands automatically + +### Mutating command rules + +- mutating commands must never rely on CLI defaults for apply behavior +- mutating commands must always receive explicit context: + - target selection where relevant + - scope where relevant + - workspace root where relevant +- post-mutation refresh in the app must use the origin operation context, not global last-view state + +### Payload vs process-failure rule + +For app integration there are two different failure classes: + +- transport/process failure + - no valid contract payload + - process spawn failure + - timeout + - malformed JSON +- structured domain failure + - valid contract payload exists + - payload says `ok=false`, `outcome=failed`, `issue_count>0`, or equivalent + +The app must distinguish those two classes clearly. + +### Why this section matters + +Without a command semantics matrix, the app will eventually do one or more of these: + +- treat every non-zero exit as an unstructured crash +- lose useful report payloads on domain failures +- accidentally rely on `--dry-run=true` +- retry mutating commands as if they were read-only + +## UI and Entry Model in claude_team + +### Normalized entry kinds + +- `universal_available` +- `universal_installed` +- `native_external_installed` + +### Ranking order + +1. installed universal +2. installed native external +3. available universal + +### Card labels + +Universal: + +- `Universal` +- `Anthropic + Codex` +- `Anthropic only` +- `Codex only` + +Native external: + +- `Installed - Anthropic only` +- `Installed - Codex only` + +### Detail requirements + +Universal detail must show: + +- keywords or tags +- category only when curated metadata exists +- target support +- scope support +- README/detail content +- lifecycle actions where supported + +Native external detail must show: + +- native target +- detected source +- observed scopes +- manageability +- relation to universal plugin when matched + +### Empty and degraded states + +If `catalog` fails but `discover` works: + +- show native external entries +- show a warning for universal catalog unavailability + +If `discover` fails but `catalog` works: + +- show universal entries +- do not claim “no installed plugins” +- show a warning that native discovery is unavailable + +If backend version is unsupported: + +- keep read-only view if safe +- disable lifecycle actions +- show explicit compatibility message + +## App Integration Mode + +`claude_team` should integrate with `plugin-kit-ai` as a bundled CLI binary with versioned JSON I/O. + +Not as: + +- parsed human terminal output +- direct repo scraping +- an embedded second UI +- a Go library linked into Electron + +Why: + +- Electron already knows how to run bundled binaries +- JSON contracts are versionable and testable +- rollout and rollback stay simple +- the same backend surface can be exercised in dev and packaged builds + +## plugin-kit-ai Changes Required + +### Must-have for phase 0 + +1. `integrations --format json` around the current normalized lifecycle model + 🎯 10 🛡️ 10 🧠 4 + Approximate change size: `150-300` lines + +2. integration-level fields in managed lifecycle JSON + Needed because current `Report.Targets` do not identify which integration a target belongs to. + 🎯 10 🛡️ 10 🧠 5 + Approximate change size: `120-240` lines + +3. `integrations catalog --format json` + 🎯 10 🛡️ 9 🧠 5 + Approximate change size: `180-350` lines + +4. `integrations discover --format json` + 🎯 10 🛡️ 10 🧠 6 + Approximate change size: `220-450` lines + +5. `--workspace-root` + 🎯 9 🛡️ 10 🧠 4 + Approximate change size: `80-180` lines + +6. capability and scope metadata in catalog/discovery + 🎯 9 🛡️ 9 🧠 4 + Approximate change size: `80-160` lines + +7. stable detail path or detail endpoint + 🎯 8 🛡️ 8 🧠 4 + Approximate change size: `60-140` lines + +8. discovery trust/manageability metadata + 🎯 8 🛡️ 9 🧠 5 + Approximate change size: `80-180` lines + +9. provenance metadata + 🎯 8 🛡️ 9 🧠 4 + Approximate change size: `60-140` lines + +### Explicitly not required for the first app rollout + +- `integrations sync` +- workspace-lock driven desired-state workflows +- `enable` / `disable` UI +- native convenience uninstall +- adopt + +Reason: + +- they are either repo-lock oriented, lower-value than core lifecycle, or too risky before `discover` is proven out + +## claude_team Changes Required + +### Main-process additions + +- `PluginKitBinaryResolver` +- `PluginKitService` +- `PluginKitCatalogService` +- `PluginKitDiscoveryService` +- `PluginKitLifecycleService` + +### Current app touchpoints this migration must replace or bypass + +Current plugin flow in `claude_team` is still Claude-marketplace-shaped: + +- `src/main/services/extensions/catalog/PluginCatalogService.ts` +- `src/main/services/extensions/state/PluginInstallationStateService.ts` +- `src/main/services/extensions/install/PluginInstallService.ts` +- `src/main/services/extensions/ExtensionFacadeService.ts` +- `src/shared/types/extensions/plugin.ts` +- `src/renderer/store/slices/extensionsSlice.ts` +- `src/renderer/components/extensions/plugins/*` + +Recommended rule: + +- do not keep stretching the current `EnrichedPlugin` model until it represents two different product classes badly +- introduce a new normalized plugin-entry layer for the plugin-kit-backed flow +- keep the legacy Claude-only model behind the feature flag until rollout is complete + +### Required app-side model split + +Current `EnrichedPlugin` is shaped around one catalog plus installed counts: + +- one canonical `pluginId` +- one marketplace-oriented metadata shape +- one merged installed-state view + +That is not a safe long-term shape for mixed: + +- `universal_catalog` +- `universal_installed` +- `native_external_installed` + +Recommended rule: + +- phase 1 should add a new normalized entry model for the plugin page +- old `EnrichedPlugin` can remain only inside the legacy backend path +- the new renderer/store layer should be built around explicit `entry_kind`, not around legacy marketplace assumptions + +### Responsibilities + +#### `PluginKitBinaryResolver` + +- resolve bundled binary +- resolve dev binary +- report version + +#### `PluginKitService` + +- execute commands +- validate `format` and `schema_version` +- apply timeouts +- normalize errors +- redact diagnostics + +#### `PluginKitCatalogService` + +- call `catalog` +- cache normalized results + +#### `PluginKitDiscoveryService` + +- call `discover` +- normalize native external entries + +#### `PluginKitLifecycleService` + +- call `add/update/remove/repair/list/doctor` +- normalize target-level results + +### Store and cache rules + +- only one mutating operation per `entryId + scope + projectPath` +- `catalog` and `discover` may refresh in parallel +- stale responses must never overwrite newer state +- post-mutation refresh must use the origin operation context + +### Feature flag + +Recommended app flag: + +- `extensions.plugins.backend = legacy | plugin-kit` + +## Rollout Phases + +### Phase 0 - plugin-kit-ai contracts first + +🎯 10 🛡️ 10 🧠 5 +Approximate change size: `400-900` lines + +Ship: + +- JSON envelopes +- `catalog` +- `discover` +- `--workspace-root` +- schema docs +- source/provenance metadata + +Acceptance: + +- contracts are versioned and testable +- command classes are clearly read-only vs mutating +- managed lifecycle JSON includes integration-level context +- views are internally consistent across `catalog`, `discover`, `list`, and `doctor` + +### Phase 1 - read-only app integration + +🎯 9 🛡️ 9 🧠 5 +Approximate change size: `300-650` lines + +Ship: + +- bundled binary +- catalog rendering +- discovery rendering +- mixed-entry normalization +- ranking and labels +- feature-flagged backend switch + +Acceptance: + +- native external entries render truthfully +- universal catalog renders truthfully +- no misleading install button on native external entries +- page remains useful when `catalog` or `discover` partially fail +- warm-load performance remains acceptable +- plugin-kit-backed renderer state uses the new normalized entry model instead of overloading legacy `EnrichedPlugin` + +### Phase 2 - universal lifecycle actions + +🎯 9 🛡️ 9 🧠 6 +Approximate change size: `300-700` lines + +Ship: + +- install +- update +- remove +- repair +- target-level result rendering + +Acceptance: + +- direct Claude path green +- multimodel Anthropic + Codex path green +- user/project installs stable +- partial target results rendered truthfully +- safe retries do not corrupt state + +### Phase 3 - optional native convenience flows + +🎯 7 🛡️ 8 🧠 7 +Approximate change size: `150-400` lines + +Optional: + +- native uninstall where backend declares safe +- adopt where backend declares safe + +Acceptance: + +- no ambiguity between universal managed and native external state + +## Recommended First PR Sequence + +### PR 1 - JSON envelopes for existing lifecycle commands + +Ship: + +- `integrations {list|doctor|add|update|remove|repair} --format json` +- schema identifiers +- contract docs + +Must not do: + +- change lifecycle semantics +- invent `catalog` or `discover` early + +### PR 2 - managed lifecycle integration context + +Ship: + +- `integration_id` +- source refs +- policy scope +- workspace root +- target grouping under managed entries + +Must not do: + +- flatten per-target state into one integration status + +### PR 3 - explicit workspace root control + +Ship: + +- `--workspace-root` +- project-sensitive commands stop depending on implicit `cwd` + +Must not do: + +- silently rewrite missing workspace root into current cwd for project commands + +### PR 4 - universal catalog + +Ship: + +- normalized universal catalog JSON +- bundled snapshot story +- freshness metadata +- README/detail default rule + +Must not do: + +- block on category, icon, or popularity + +### PR 5 - native discovery + +Ship: + +- native external discovery JSON +- observed state +- activation hints +- manageability and match metadata + +Must not do: + +- auto-merge native and universal entries +- expose destructive actions without explicit backend manageability + +### PR 6 - claude_team read-only integration + +Ship: + +- bundled binary +- read-only mixed-entry rendering +- ranking +- degraded-state handling + +Must not do: + +- wire mutating actions before backend contracts are pinned + +## PR Exit Criteria + +These checks are intentionally strict. A PR is not “basically done” if these are still fuzzy. + +### Backend contract PRs + +Required: + +- schema id is documented +- schema version is documented +- arrays are never `null` +- one golden fixture exists +- one failure fixture exists +- one compatibility test exists + +### Backend discovery PRs + +Required: + +- at least one Claude discovery fixture +- at least one Codex discovery fixture +- explicit observed-state coverage +- explicit manageability coverage +- no destructive side effects in read-only commands + +### App read-only integration PRs + +Required: + +- mixed-entry rendering works with only `catalog` +- mixed-entry rendering works with only `discover` +- degraded-state copy is truthful +- unsupported-version handling is visible and safe + +### App lifecycle PRs + +Required: + +- explicit dry-run protection +- target-level partial success rendering +- stale-response protection +- safe retry behavior + +## Risks and Lowest-Confidence Areas + +### 1. Manifest model split between lifecycle and catalog + +🎯 8 🛡️ 8 🧠 6 + +Why this is hard: + +- the richer authored model and the narrower lifecycle model do not preserve the same metadata +- catalog wants richer storefront fields +- lifecycle wants compact normalized install truth + +Best resolution: + +- keep lifecycle and catalog as separate backend surfaces +- allow catalog generation to read the richer authored model +- normalize both into explicit JSON contracts + +### 2. Codex native external discovery fidelity + +🎯 7 🛡️ 8 🧠 7 + +Why this is hard: + +- current Codex inspect logic distinguishes installed, activation pending, disabled, and degraded by combining multiple native surfaces +- that is richer than installed yes/no, but still not a full external discovery surface + +Best resolution: + +- backend-owned discovery +- explicit `observed_state` +- explicit `activation_hint` +- never collapse prepared Codex state into fully active installed state + +### 3. Workspace-root and repo-root coupling + +🎯 8 🛡️ 9 🧠 6 + +Why this is hard: + +- current service construction still derives important paths from `os.Getwd()` +- workspace-lock paths are repo-root oriented +- packaged Electron app must not inherit the wrong working directory semantics + +Best resolution: + +- add explicit `--workspace-root` +- reject missing workspace root for project-sensitive commands +- keep `sync` and workspace-lock flows out of the first app rollout + +### 4. Source resolution cost for lifecycle vs catalog + +🎯 8 🛡️ 9 🧠 5 + +Why this is hard: + +- lifecycle source resolution may legitimately clone remote sources +- storefront catalog must stay fast and bounded + +Best resolution: + +- keep catalog generation batch-oriented +- keep lifecycle resolution source-oriented +- never build the app catalog by doing one source clone per entry at runtime + +### 5. Popularity parity + +🎯 6 🛡️ 9 🧠 5 + +Best resolution: + +- make popularity optional +- add it later only from a first-class universal metric + +### 6. Safe native convenience actions + +🎯 6 🛡️ 8 🧠 7 + +Best resolution: + +- delay to phase 3 +- require backend-declared manageability + +### 7. App-facing target subset confusion + +🎯 8 🛡️ 9 🧠 5 + +Why this is hard: + +- `plugin-kit-ai` knows more targets than the app should expose as first-class plugin lanes +- if the app shows every authored target equally, users will see support the app cannot actually manage yet + +Best resolution: + +- preserve full authored support in backend catalog +- use only `claude` and `codex-package` for primary actionability in `claude_team` +- keep broader support as secondary metadata only + +### 8. Target projection drift between authored, manageable, and app-primary targets + +🎯 8 🛡️ 9 🧠 6 + +Why this is hard: + +- these three target sets are related but not identical +- if they collapse into one field, the UI will eventually lie about support or actionability + +Best resolution: + +- preserve separate target fields in catalog +- test them with golden fixtures +- never let renderer heuristics rebuild one layer from another + +## E2E and Contract Testing + +### plugin-kit-ai + +Must add: + +- JSON contract tests for `catalog`, `discover`, `list`, `doctor` +- JSON contract tests for `add/update/remove/repair` +- temp-home tests +- temp-project tests +- target adapter tests for Claude and Codex +- golden fixture tests against at least one real universal plugin + +### claude_team + +Must add: + +- `PluginKitService` parsing tests +- mixed-entry normalization tests +- ranking tests +- badge tests +- manual-step rendering tests +- project-context tests + +### Failure checks + +- bundled binary missing +- bundled binary wrong architecture +- schema mismatch +- project install without workspace root +- unsupported scope for selected target +- partial target success +- stale catalog with failed refresh +- repeated idempotent retries + +## No-Go Conditions + +Do not enable by default if any of these remain true: + +- universal and native external entries still auto-merge by display name +- lifecycle still depends on parsing prose +- `local` scope is silently remapped +- partial target failures are flattened into one misleading status +- native external entries expose destructive actions without backend-declared manageability + +## Open Questions With Recommended Defaults + +### 1. Should the app call GitHub directly for universal plugins? + +Recommended default: **No**. + +Use backend `catalog`. + +### 2. Should native external entries merge into universal entries when matching looks obvious? + +Recommended default: **No**. + +Keep separate and use soft relations only. + +### 3. Should `local` scope be shown for universal installs in phase 1? + +Recommended default: **No**. + +Only show scopes explicitly supported by backend target metadata. + +### 4. Should popularity sorting block the migration? + +Recommended default: **No**. + +Hide or degrade it if there is no stable universal metric. + +### 5. Should `adopt` block phase 1? + +Recommended default: **No**. + +Phase 1 needs `discover`, not `adopt`. + +### 6. Should native uninstall for discovered entries ship immediately? + +Recommended default: **No**. + +Only where backend declares safe manageability. + +### 7. Should category or icon metadata block phase 1? + +Recommended default: **No**. + +Ship phase 1 with honest minimum storefront metadata, then add optional curation metadata later. + +### 8. Should `discover` be implemented by reusing current per-target `Inspect` directly? + +Recommended default: **No**. + +Use `Inspect` as one building block, but build a real native discovery scanner above it because external discovery and managed inspection are not the same problem. + +### 9. Should catalog be generated from the current `integrationctl` manifest loader only? + +Recommended default: **No**. + +Prefer the richer authored-model path such as `pluginmanifest.Inspect/publication/targetcontracts`, or enrich the lifecycle loader first. + +### 10. Should the `claude_team` plugin page expose every target known to `plugin-kit-ai` as a first-class action lane? + +Recommended default: **No**. + +Use the app-relevant target subset for primary actions: + +- `claude` +- `codex-package` + +Keep broader authored support as optional secondary detail, not as primary actionability. + +### 11. Should the catalog expose only one target field? + +Recommended default: **No**. + +Keep separate fields for: + +- authored target support +- backend-manageable lifecycle support +- app-primary action targets + +## Final Recommendation + +Build this integration in three layers: + +1. make `plugin-kit-ai` expose stable JSON contracts +2. make `plugin-kit-ai` the normalized backend for universal catalog, discovery, and lifecycle +3. make `claude_team` a frontend over those contracts + +This is the most reliable path because it: + +- keeps the current app UX +- reuses the existing lifecycle engine in `plugin-kit-ai` +- avoids false parity and false merging +- keeps rollout reversible diff --git a/src/renderer/components/sidebar/GlobalTaskList.tsx b/src/renderer/components/sidebar/GlobalTaskList.tsx index de860219..14dcc6c3 100644 --- a/src/renderer/components/sidebar/GlobalTaskList.tsx +++ b/src/renderer/components/sidebar/GlobalTaskList.tsx @@ -12,6 +12,7 @@ import { getNonEmptyTaskCategories, groupTasksByDate, groupTasksByProject, + NO_PROJECT_KEY, sortTasksByFreshness, } from '@renderer/utils/taskGrouping'; import { deriveTaskDisplayId } from '@shared/utils/taskIdentity'; @@ -430,8 +431,18 @@ export const GlobalTaskList = ({ ? categories.length > 0 : projectGroups.some((g) => g.tasks.length > 0)); + const noProjectGroupColor = useMemo( + () => ({ + border: 'var(--color-border)', + glow: 'transparent', + icon: 'var(--color-text-muted)', + text: 'var(--color-text-secondary)', + }), + [] + ); + return ( -
+
{!hideHeader && (
{/* Content */} -
+
{globalTasksLoading && !globalTasksInitialized && (
{[1, 2, 3].map((i) => ( @@ -644,7 +655,10 @@ export const GlobalTaskList = ({ projectGroups.map((group) => { if (group.tasks.length === 0) return null; const isGroupCollapsed = projectCollapsed.isCollapsed(group.projectKey); - const groupColor = projectColor(group.projectLabel); + const isNoProjectGroup = group.projectKey === NO_PROJECT_KEY; + const groupColor = isNoProjectGroup + ? noProjectGroupColor + : projectColor(group.projectLabel); const visibleCount = getProjectGroupVisibleCount( projectVisibleCountByKey[group.projectKey], group.tasks.length @@ -661,7 +675,9 @@ export const GlobalTaskList = ({ className="hover:bg-surface-raised/40 sticky top-0 z-10 flex w-full cursor-pointer items-center gap-1.5 p-2 transition-colors" style={{ backgroundColor: 'var(--color-surface-sidebar)', - backgroundImage: `linear-gradient(90deg, ${groupColor.glow} 0%, transparent 80%)`, + backgroundImage: isNoProjectGroup + ? undefined + : `linear-gradient(90deg, ${groupColor.glow} 0%, transparent 80%)`, boxShadow: `inset 2px 0 0 ${groupColor.border}, inset 0 -1px 0 var(--color-border)`, }} > @@ -676,7 +692,7 @@ export const GlobalTaskList = ({ aria-hidden="true" /> {group.projectLabel} diff --git a/src/renderer/components/team/members/MemberMessagesTab.tsx b/src/renderer/components/team/members/MemberMessagesTab.tsx index 3fc87f1c..9c646550 100644 --- a/src/renderer/components/team/members/MemberMessagesTab.tsx +++ b/src/renderer/components/team/members/MemberMessagesTab.tsx @@ -51,7 +51,8 @@ export const MemberMessagesTab = ({ const [pagedMessages, setPagedMessages] = useState([]); const [nextCursor, setNextCursor] = useState(null); const [hasMore, setHasMore] = useState(false); - const [loading, setLoading] = useState(false); + const [initialPageLoading, setInitialPageLoading] = useState(false); + const [loadingOlderMessages, setLoadingOlderMessages] = useState(false); const [activityFilter, setActivityFilter] = useState(initialFilter); const [expandedItem, setExpandedItem] = useState(null); const { readSet } = useTeamMessagesRead(teamName); @@ -74,7 +75,7 @@ export const MemberMessagesTab = ({ setPagedMessages([]); setNextCursor(null); setHasMore(false); - setLoading(true); + setInitialPageLoading(true); void (async () => { try { @@ -95,7 +96,9 @@ export const MemberMessagesTab = ({ setHasMore(false); } } finally { - if (!cancelled) setLoading(false); + if (!cancelled) { + setInitialPageLoading(false); + } } })(); @@ -105,8 +108,8 @@ export const MemberMessagesTab = ({ }, [teamName, memberName]); const loadOlderMessages = useCallback(async () => { - if (!nextCursor || loading) return; - setLoading(true); + if (!nextCursor || loadingOlderMessages) return; + setLoadingOlderMessages(true); try { const page = await api.teams.getMessagesPage(teamName, { beforeTimestamp: nextCursor, @@ -121,9 +124,9 @@ export const MemberMessagesTab = ({ } catch { // best-effort } finally { - setLoading(false); + setLoadingOlderMessages(false); } - }, [teamName, memberName, nextCursor, loading]); + }, [loadingOlderMessages, memberName, nextCursor, teamName]); const effectiveMessages = useMemo( () => mergeTeamMessages(messages, pagedMessages), @@ -198,7 +201,7 @@ export const MemberMessagesTab = ({ [onTaskClick, taskMap, tasks] ); - const emptyStateText = loading + const emptyStateText = initialPageLoading ? 'Loading activity...' : activityFilter === 'comments' ? 'No comments for this member' @@ -289,10 +292,11 @@ export const MemberMessagesTab = ({ variant="ghost" size="sm" className="text-xs" - disabled={loading} + aria-busy={loadingOlderMessages} + disabled={loadingOlderMessages} onClick={() => void loadOlderMessages()} > - {loading ? 'Loading...' : 'Load older messages'} + Load older messages
)} diff --git a/src/renderer/components/team/messages/MessagesPanel.tsx b/src/renderer/components/team/messages/MessagesPanel.tsx index 5638726c..2b61f147 100644 --- a/src/renderer/components/team/messages/MessagesPanel.tsx +++ b/src/renderer/components/team/messages/MessagesPanel.tsx @@ -152,13 +152,12 @@ export const MessagesPanel = memo(function MessagesPanel({ const [fetchedMessages, setFetchedMessages] = useState([]); const [nextCursor, setNextCursor] = useState(null); const [hasMore, setHasMore] = useState(false); - const [messagesLoading, setMessagesLoading] = useState(false); + const [loadingOlderMessages, setLoadingOlderMessages] = useState(false); const fetchIdRef = useRef(0); // Initial fetch on mount or team change useEffect(() => { const id = ++fetchIdRef.current; - setMessagesLoading(true); void (async () => { try { const page = await api.teams.getMessagesPage(teamName, { limit: PAGE_SIZE }); @@ -171,8 +170,6 @@ export const MessagesPanel = memo(function MessagesPanel({ if (fetchIdRef.current === id && messages.length > 0) { setFetchedMessages(messages); } - } finally { - if (fetchIdRef.current === id) setMessagesLoading(false); } })(); }, [teamName]); // eslint-disable-line react-hooks/exhaustive-deps -- intentionally only on teamName change @@ -193,8 +190,8 @@ export const MessagesPanel = memo(function MessagesPanel({ }, [teamName, isTeamAlive, leadActivity]); const loadOlderMessages = useCallback(async () => { - if (!nextCursor || messagesLoading) return; - setMessagesLoading(true); + if (!nextCursor || loadingOlderMessages) return; + setLoadingOlderMessages(true); try { const page = await api.teams.getMessagesPage(teamName, { beforeTimestamp: nextCursor, @@ -206,9 +203,9 @@ export const MessagesPanel = memo(function MessagesPanel({ } catch { // best-effort } finally { - setMessagesLoading(false); + setLoadingOlderMessages(false); } - }, [teamName, nextCursor, messagesLoading]); + }, [loadingOlderMessages, nextCursor, teamName]); // Use fetched messages, fall back to prop messages during initial load const effectiveMessages = useMemo(() => { @@ -307,7 +304,7 @@ export const MessagesPanel = memo(function MessagesPanel({ for (const [element, setHeight] of observedEntries) { if (!element) continue; - const updateHeight = () => { + const updateHeight = (): void => { const nextHeight = Math.ceil(element.getBoundingClientRect().height); if (nextHeight > 0) { setHeight(nextHeight); @@ -684,10 +681,11 @@ export const MessagesPanel = memo(function MessagesPanel({ variant="ghost" size="sm" className="text-xs text-text-muted" - disabled={messagesLoading} + aria-busy={loadingOlderMessages} + disabled={loadingOlderMessages} onClick={() => void loadOlderMessages()} > - {messagesLoading ? 'Loading...' : 'Load older messages'} + Load older messages
)} @@ -869,10 +867,11 @@ export const MessagesPanel = memo(function MessagesPanel({ variant="ghost" size="sm" className="text-xs text-text-muted" - disabled={messagesLoading} + aria-busy={loadingOlderMessages} + disabled={loadingOlderMessages} onClick={() => void loadOlderMessages()} > - {messagesLoading ? 'Loading...' : 'Load older messages'} + Load older messages
)} @@ -1155,10 +1154,11 @@ export const MessagesPanel = memo(function MessagesPanel({ variant="ghost" size="sm" className="text-xs text-text-muted" - disabled={messagesLoading} + aria-busy={loadingOlderMessages} + disabled={loadingOlderMessages} onClick={() => void loadOlderMessages()} > - {messagesLoading ? 'Loading...' : 'Load older messages'} + Load older messages
)} diff --git a/src/renderer/utils/taskGrouping.ts b/src/renderer/utils/taskGrouping.ts index 12f0b2dc..b007bca8 100644 --- a/src/renderer/utils/taskGrouping.ts +++ b/src/renderer/utils/taskGrouping.ts @@ -91,8 +91,8 @@ export function getNonEmptyTaskCategories(groups: DateGroupedTasks): DateCategor return DATE_CATEGORY_ORDER.filter((cat) => groups[cat].length > 0); } -const NO_PROJECT_KEY = '__no_project__'; -const NO_PROJECT_LABEL = 'Without project'; +export const NO_PROJECT_KEY = '__no_project__'; +export const NO_PROJECT_LABEL = 'No project'; function trimTrailingPathSep(p: string): string { let s = p;