feat(plugin-system): GitHub connector, discovery, marketplaces, and access UX (#1525)

* feat(plugin-system): add GitHub connector, discovery, marketplaces, and access UX

End-to-end GitHub App connector flow and UI:

- GitHub App connect: install start/callback/complete endpoints, connector account upsert from installation, selection state, and a dedicated Den Web setup page.
- Repo discovery: GitHub tree + manifest inspection, Claude-compatible classification (marketplace/plugin-manifest), marketplace plugin metadata/component path parsing, discovery API + snapshot.
- Apply pipeline: materialize plugins, connector mappings, config objects (with frontmatter-aware skill/agent parsing), memberships, and source bindings; create marketplaces with name/description from marketplace.json.
- Auto-import on push: persist flag on connector instance, webhook-driven re-apply for new discoveries.
- Cleanup: cascading disconnect on connector account removal and remove on connector instance.
- Integrations UI: cleaner connected-account card, GitHub avatar, hover trash + confirm dialog, inline "Add new repo" action, per-account repo picker, manifest badges, configured/unconfigured sorting.
- Discovery UI: cleaner loader, plugin cards with component chips, inline apply action, auto-import toggle default on.
- Manage UI: instance configuration endpoint, auto-import toggle, remove repo danger zone with cascade confirmation.
- Plugins & Marketplaces pages: dashboard nav entries, list + detail screens, per-plugin component counts, marketplace resolved endpoint with source + plugins, marketplace access section (org-wide/team/member grants).
- Bitbucket card marked "Coming soon".
- PRDs, GitHub setup instructions, and learnings docs added.

* chore(docs): move GitHub-instructions.md into prds/new-plugin-arch/github-connection

* fix(den-web): wrap github integration page in Suspense for useSearchParams

* refactor(den-web): redirect GitHub post-install flow into the clean account selection phase

After completing the GitHub App install, previously we rendered a separate
GithubRepositorySelectionPhase with different styling. Now we call the install
completion endpoint, then router.replace to ?connectorAccountId=... so the
existing GithubConnectedAccountSelectionPhase renders the repo list. Removes
the duplicate selection phase and its unused helpers/imports.

* fix(den-web): drop Requires-scopes body and show GitHub description in integrations card

Removes the empty-state Requires scopes: <code>… block from both provider
cards and restores the provider description on the GitHub card so the empty
state is consistent with Bitbucket. Drops the header's bottom border when no
body follows.

* fix(den-web): only show integration provider description in empty state

Once a provider has connections, hide the description in the header so the
card focuses on the connected accounts + repos list.

---------

Co-authored-by: src-opn <src-opn@users.noreply.github.com>
This commit is contained in:
Source Open
2026-04-22 17:27:59 -07:00
committed by GitHub
parent 47db4e39e3
commit 6053ac937e
37 changed files with 9659 additions and 422 deletions

View File

@@ -0,0 +1,320 @@
# GitHub Instructions
This document lists exactly what you need to configure for the GitHub App connection flow and where each value should go.
## Goal
After this setup:
1. You open `Integrations` in Den Web.
2. You click `Connect` on GitHub.
3. GitHub shows the GitHub App install flow.
4. GitHub redirects back to OpenWork.
5. OpenWork shows the repositories visible to that installation.
6. You select one repo.
## Where to put the local server values
Fill these values in:
`ee/apps/den-api/.env.local`
That file is loaded by Den API in this order:
1. `ee/apps/den-api/.env.local`
2. `ee/apps/den-api/.env`
3. existing shell environment
## Values you need from GitHub
You need to create or update a GitHub App and collect these values:
- GitHub App ID
- GitHub App Client ID
- GitHub App Client Secret
- GitHub App Private Key
- GitHub App Webhook Secret
- GitHub Installation ID
- Test repository ID
- Test repository full name (`owner/repo`)
- Test branch
- Test ref (`refs/heads/<branch>`)
## Exactly where each value goes
Put these in `ee/apps/den-api/.env.local`:
```env
# Required Den API basics
PORT=8790
OPENWORK_DEV_MODE=1
CORS_ORIGINS=http://localhost:3000,http://localhost:3001,http://localhost:3005
BETTER_AUTH_URL=http://localhost:8790
BETTER_AUTH_SECRET=<generate-a-32-plus-char-secret>
DEN_DB_ENCRYPTION_KEY=<generate-a-32-plus-char-secret>
DATABASE_URL=mysql://root:password@127.0.0.1:3306/den
# Existing user auth GitHub values. These are separate from the connector app.
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
# GitHub connector app values
GITHUB_CONNECTOR_APP_ID=<github-app-id>
GITHUB_CONNECTOR_APP_CLIENT_ID=<github-app-client-id>
GITHUB_CONNECTOR_APP_CLIENT_SECRET=<github-app-client-secret>
GITHUB_CONNECTOR_APP_PRIVATE_KEY=<github-private-key-with-escaped-newlines>
GITHUB_CONNECTOR_APP_WEBHOOK_SECRET=<github-webhook-secret>
# Handy local test values
GITHUB_TEST_INSTALLATION_ID=<installation-id>
GITHUB_TEST_REPOSITORY_ID=<repository-id>
GITHUB_TEST_REPOSITORY_FULL_NAME=<owner/repo>
GITHUB_TEST_BRANCH=main
GITHUB_TEST_REF=refs/heads/main
```
## Important private key formatting
For `GITHUB_CONNECTOR_APP_PRIVATE_KEY`, paste the private key as one line with `\n` escapes.
Example:
```env
GITHUB_CONNECTOR_APP_PRIVATE_KEY=-----BEGIN PRIVATE KEY-----\nMIIEv...\n-----END PRIVATE KEY-----
```
Do not paste raw multi-line PEM text directly unless you know the env loader path is handling it the way you expect.
## GitHub App setup
Go to:
`GitHub -> Settings -> Developer settings -> GitHub Apps -> New GitHub App`
Use these settings.
### Basic info
- App name: choose any unique name, for example `OpenWork Den Local`
- Homepage URL: use your local/public Den Web URL
- local example: `http://localhost:3005`
- public example: your deployed Den Web URL
- Description: optional
### Webhooks
- Webhooks: enabled
- Webhook URL:
- for webhook deliveries themselves, use:
- `https://<your-public-den-web-host>/api/den/v1/webhooks/connectors/github`
- or the public Den API URL if you are not proxying through Den Web
- Webhook secret:
- set this to the same value you put in `GITHUB_CONNECTOR_APP_WEBHOOK_SECRET`
## Important: Setup URL vs Webhook URL
GitHub App has two different relevant URLs:
1. `Setup URL`
2. `Webhook URL`
### Setup URL
This is where GitHub sends the user's browser back after installation.
This should be an actual Den Web page, not a den-api callback route.
Set it to:
`https://<your-public-den-web-host>/dashboard/integrations/github`
GitHub will append values like:
- `installation_id`
- `setup_action`
- `state`
Den Web reads those query params and then calls Den API to validate the signed state and load the repositories for that installation.
Do not point the Setup URL at Den API for this flow.
### Webhook URL
This is where GitHub sends push/install webhook events.
Set it to:
`https://<your-public-den-web-host>/api/den/v1/webhooks/connectors/github`
If your public entrypoint is Den API directly, use:
`https://<your-public-den-api-host>/v1/webhooks/connectors/github`
## Repository permissions
Set these GitHub App repository permissions:
- `Metadata`: `Read-only`
- `Contents`: `Read-only`
That is the minimum needed for the current repo-listing and validation flow.
## Organization permissions
None are strictly required for the current slice.
## Subscribe to these webhook events
Enable these events:
- `Push`
- `Installation`
- `Installation target`
- `Repository`
## Install the app
After creating the app:
1. Generate a client secret.
2. Generate a private key.
3. Install the app on the user or org that owns the repo you want to test.
4. Grant access to the repo you want to test.
## How to collect the values after setup
### App ID
From the GitHub App settings page.
Put in:
`GITHUB_CONNECTOR_APP_ID`
### Client ID
From the GitHub App settings page.
Put in:
`GITHUB_CONNECTOR_APP_CLIENT_ID`
### Client Secret
Generate from the GitHub App settings page.
Put in:
`GITHUB_CONNECTOR_APP_CLIENT_SECRET`
### Private Key
Generate from the GitHub App settings page.
Put in:
`GITHUB_CONNECTOR_APP_PRIVATE_KEY`
### Webhook Secret
From the GitHub App webhook configuration.
Put in:
`GITHUB_CONNECTOR_APP_WEBHOOK_SECRET`
### Installation ID
You can get it from the GitHub install redirect/callback, or via `gh`:
```bash
gh api repos/<owner>/<repo>/installation --jq '.id'
```
Put in:
`GITHUB_TEST_INSTALLATION_ID`
### Repository ID
```bash
gh api repos/<owner>/<repo> --jq '.id'
```
Put in:
`GITHUB_TEST_REPOSITORY_ID`
### Repository full name
Format:
`owner/repo`
Put in:
`GITHUB_TEST_REPOSITORY_FULL_NAME`
### Branch and ref
Examples:
- branch: `main`
- ref: `refs/heads/main`
Put in:
- `GITHUB_TEST_BRANCH`
- `GITHUB_TEST_REF`
## Local run commands
From the repo root:
```bash
pnpm --filter @openwork-ee/den-api dev
pnpm --filter @openwork-ee/den-web dev
```
Den Web default local URL in this repo is:
`http://localhost:3005`
Den API default local URL in this repo is:
`http://localhost:8790`
## Public URL requirement
GitHub must be able to reach your callback and webhook endpoints.
That means for real testing you need a public URL, usually via a tunnel or deployed environment.
Examples:
- `ngrok`
- `cloudflared`
- deployed Den Web / Den API host
## What to do after env is filled
1. Start Den API.
2. Start Den Web.
3. Confirm the GitHub App `Setup URL` points to the Den Web GitHub setup page.
4. Confirm the GitHub App `Webhook URL` points to the webhook endpoint.
5. Go to Den Web `Integrations`.
6. Click `Connect` on GitHub.
7. Finish the GitHub App install flow.
8. GitHub should return to `/dashboard/integrations/github` in Den Web.
9. Den Web should show the repository selection screen.
## Current scope note
This phase currently gets you to:
- GitHub App install redirect
- return to OpenWork
- repository list
- selecting one repo to create a connector instance
It does not yet complete full content ingestion from the selected repository.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,123 @@
# GitHub Connection UX Plan
## Goal
Define the desired user experience for connecting GitHub to OpenWork in den-web (cloud), independent of the current implementation state.
This flow is for the den-api plugin connector system and GitHub App based connector onboarding.
## Desired user flow
1. User is in OpenWork den-web (cloud).
2. User sees an `Integrations` entry point.
3. User opens `Integrations`.
4. User sees a GitHub integration card.
5. User clicks `Connect` on GitHub.
6. OpenWork sends the user to the GitHub App install/authorize flow.
7. User completes the normal GitHub steps on GitHub.
8. GitHub returns the user to OpenWork.
9. OpenWork recognizes the completed GitHub App installation for the current org/user context.
10. OpenWork shows the user the list of repositories available through that installation.
11. User selects one repository.
12. OpenWork creates a new GitHub connector instance for that selected repository.
13. OpenWork configures webhook-driven sync for that repository.
14. Future pushes to the connected repository trigger OpenWork sync behavior through the connector pipeline.
## Product expectations
### Integrations surface
- den-web should expose a clear `Integrations` UI in cloud mode.
- GitHub should appear as a first-class integration option.
- The user should not need to manually paste GitHub installation ids or repository ids.
### Connect action
- Clicking `Connect` should start a GitHub App flow, not a legacy OAuth-only flow.
- The flow should preserve enough OpenWork context to return the user to the correct org and screen after GitHub finishes.
- The GitHub-side step should feel like a normal GitHub App installation flow.
### Return to OpenWork
- After GitHub redirects back, OpenWork should detect the installation that was just created or updated.
- If installation state is incomplete or ambiguous, OpenWork should guide the user instead of silently failing.
- The user should land back in the GitHub integration flow, not on a generic page with no next step.
### Repository selection
- OpenWork should list repositories available to the installation.
- The user should be able to pick one repository as the first connected source.
- Selecting a repository should create a connector instance for that repo in the current OpenWork org.
- The UX may later support branch choice and mapping choice, but repository selection is the minimum required step.
### Webhook + sync expectation
- Once connected, OpenWork should be ready to receive GitHub App webhooks for the selected repository.
- Pushes on the tracked branch should enter the connector sync pipeline.
- The system should present this as a connected integration, not as a hidden backend-only setup.
## User-facing behavior requirements
- The user should not need to know what an installation id is.
- The user should not need to call admin APIs manually.
- The user should not need to configure webhooks manually in normal product usage.
- The user should be able to understand whether GitHub is:
- not connected
- connected but no repository selected
- connected and repository syncing
- connected but needs attention
## Desired backend behavior
To support the UX above, the backend flow should conceptually do the following:
1. Generate or expose the GitHub App install URL.
2. Preserve OpenWork return context across the redirect.
3. Handle the GitHub return/callback.
4. Resolve the GitHub App installation id associated with the user action.
5. Create or update the corresponding `connector_account`.
6. List repositories accessible through that installation.
7. On repo selection, create:
- a `connector_instance`
- a `connector_target` for the repo/branch
- any initial mappings needed for ingestion
8. Ensure webhook events can resolve that connector target.
9. Queue sync work when relevant webhook events arrive.
## UX principles
- Prefer a short, guided flow over a configuration-heavy admin experience.
- Favor product language like `Connect GitHub` over backend nouns like `connector account`.
- Hide raw GitHub/App identifiers from the normal UX unless needed for support/debugging.
- Keep the first-run flow focused on success: install, return, pick repo, connected.
- Advanced settings can exist later, but should not block first connection.
## Success criteria
The experience is successful when:
1. A cloud user can start from den-web without using terminal commands.
2. The user can complete GitHub App installation from the app.
3. The user returns to OpenWork automatically.
4. OpenWork shows repositories from that installation.
5. The user selects a repo.
6. OpenWork creates a connector instance for that repo.
7. GitHub webhooks for that repo can be accepted and associated to the instance.
8. The connection state is visible in the product UI.
## Non-goals for this document
- Exact API shapes for every route.
- Full ingestion/reconciliation design details.
- Delivery/install runtime behavior for connected content.
- Final UI layout or visual design.
## Next planning step
Translate this desired UX into an implementation plan that maps:
- den-web screens and states
- den-api routes and callback behavior
- GitHub App configuration requirements
- connector-account / connector-instance creation behavior
- webhook readiness and initial sync behavior

View File

@@ -0,0 +1,773 @@
# GitHub Repo Discovery Plan
## Goal
Define the discovery phase that happens after a user connects a GitHub repo and returns to Den Web.
This phase should:
1. inspect the connected repository structure;
2. determine whether the repo is a Claude-compatible marketplace repo, a Claude-compatible single-plugin repo, or a looser folder-based repo;
3. present the discovered plugins to the user in a setup flow;
4. let the user choose which discovered plugins should map into OpenWork;
5. translate the selected discovery result into OpenWork connector records and future ingestion work.
This document covers:
- the discovery UX;
- the GitHub-side reads we need;
- how we detect supported repo shapes;
- how we infer plugins when no manifest exists;
- how the result maps into OpenWork internal structures.
Related:
- `prds/new-plugin-arch/github-connection/plan.md`
- `prds/new-plugin-arch/github-connection/connectors.md`
- `prds/new-plugin-arch/GitHub-connector.md`
## Why a discovery phase exists
The current post-connect flow stops at repository selection.
That is enough to create:
- a `connector_account`;
- a `connector_instance`;
- a `connector_target`;
- webhook-triggered `connector_sync_event` rows.
It is not enough to understand the shape of the repo and convert that shape into useful OpenWork mappings.
The discovery phase fills that gap.
Instead of immediately asking the user to author raw path mappings, OpenWork should first inspect the repo and propose a structured interpretation of what it found.
## Desired user flow
### Updated high-level flow
1. User connects GitHub.
2. User selects a repository.
3. OpenWork creates the connector instance and target.
4. OpenWork routes the user into a dedicated `Setup` / `Discovery` page for that connector instance.
5. OpenWork reads the repository tree and shows progress steps in the UI.
6. OpenWork classifies the repo shape.
7. OpenWork shows discovered plugins, preselected by default.
8. User confirms or deselects discovered plugins.
9. OpenWork creates the initial connector mappings and plugin records from that discovery result.
10. OpenWork is ready for initial ingestion/sync.
### User-facing setup steps
The setup page should feel like a guided scan.
Suggested steps:
1. `Reading repository structure`
2. `Checking for Claude marketplace manifest`
3. `Checking for plugin manifests`
4. `Looking for known component folders`
5. `Preparing discovered plugins`
The UI should show:
- which step is currently running;
- success/failure state per step;
- the discovered plugins list when ready;
- clear empty-state or unsupported-shape messaging when nothing useful is found.
## Reference conventions
### Official Claude plugin conventions
Based on the Claude plugin docs and reference repo:
- plugin manifest lives at `.claude-plugin/plugin.json`;
- marketplace manifest lives at `.claude-plugin/marketplace.json`;
- plugin components live at the plugin root, not inside `.claude-plugin/`;
- common plugin root folders include:
- `skills/`
- `commands/`
- `agents/`
- `hooks/`
- `.mcp.json`
- `.lsp.json`
- `monitors/`
- `settings.json`
- standalone Claude configuration can also live under `.claude/`, especially:
- `.claude/skills/`
- `.claude/agents/`
- `.claude/commands/`
### Reference repo
Use `https://github.com/anthropics/claude-plugins-official` as a reference shape for marketplace repos.
Important observations:
- the repo has a root `.claude-plugin/marketplace.json`;
- it contains multiple plugin entries;
- many entries point at local paths inside the repo such as `./plugins/...` or `./external_plugins/...`;
- some entries point at external git URLs or subdirs.
That means OpenWork discovery should treat marketplace repos as a first-class shape, but be explicit about what is in-scope for a connected single repo.
## Discovery output model
The discovery phase should produce an explicit, structured result.
Suggested conceptual result:
```ts
type RepoDiscoveryResult = {
connectorInstanceId: string
connectorTargetId: string
repositoryFullName: string
ref: string
treeSummary: {
scannedEntryCount: number
truncated: boolean
strategy: "git-tree-recursive" | "contents-bfs"
}
classification:
| "claude_marketplace_repo"
| "claude_multi_plugin_repo"
| "claude_single_plugin_repo"
| "folder_inferred_repo"
| "unsupported"
discoveredPlugins: DiscoveredPlugin[]
warnings: DiscoveryWarning[]
}
type DiscoveredPlugin = {
key: string
sourceKind:
| "marketplace_entry"
| "plugin_manifest"
| "standalone_claude"
| "folder_inference"
rootPath: string
displayName: string
description: string | null
selectedByDefault: boolean
manifestPath: string | null
componentKinds: Array<"skill" | "command" | "agent" | "hook" | "mcp_server" | "lsp_server" | "monitor" | "settings">
componentPaths: {
skills: string[]
commands: string[]
agents: string[]
hooks: string[]
mcpServers: string[]
lspServers: string[]
monitors: string[]
settings: string[]
}
metadata: Record<string, unknown>
}
```
This result is intentionally separate from final ingestion. Discovery should be cheap to recompute and safe to show in the UI.
## API surface
## Requirements
We need an API that can, given the selected connector instance/target, read GitHub and return a normalized view of the repository tree and discovery result.
The tree can be large, so the API must not assume that the full repo listing is always tiny.
### Recommended endpoints
#### 1. Start or refresh discovery
`POST /v1/connector-instances/:connectorInstanceId/discovery/refresh`
Purpose:
- read GitHub using the installation token;
- build or refresh the discovery snapshot;
- persist the result for the UI;
- return the current discovery state.
Recommended response:
- current step/state;
- summary counts;
- discovered plugins if already complete.
#### 2. Get discovery state
`GET /v1/connector-instances/:connectorInstanceId/discovery`
Purpose:
- return the last computed discovery result;
- support polling while the discovery scan runs;
- drive the setup page without recomputing every request.
#### 3. Page through the normalized repo tree
`GET /v1/connector-instances/:connectorInstanceId/discovery/tree?cursor=&limit=&prefix=`
Purpose:
- expose the discovered file list for debugging and future advanced UX;
- avoid forcing the UI to load every path at once;
- support drill-down into a directory prefix.
### Why a persisted snapshot is better than live-only reads
Discovery is more than a raw file listing.
It is a structured interpretation step.
Persisting the latest snapshot gives us:
- deterministic UI reload behavior;
- auditability of what the repo looked like when discovery ran;
- a clean handoff from discovery UI to mapping creation;
- a place to store warnings and unsupported cases.
## GitHub reading strategy
### Primary strategy
Use the GitHub Git Trees API against the selected branch head commit.
Preferred read path:
1. fetch the tracked branch head SHA;
2. fetch the recursive tree for that commit;
3. normalize to a path list with type metadata.
Advantages:
- one request gives the full tree in the common case;
- easy to search for known files;
- easy to infer folder groupings;
- deterministic against a known commit SHA.
### Fallback strategy for large repos
GitHub recursive tree responses can be truncated.
If the recursive tree response is truncated:
1. store that truncated flag;
2. fall back to directory-by-directory `contents` traversal using BFS;
3. page the normalized result by `prefix + cursor`;
4. cap the total scan budget for one discovery run.
### Suggested limits
For v1:
- default API page size: `200` normalized entries;
- default max discovery scan budget: `10,000` paths;
- stop scanning further when:
- we exceed budget;
- or we have enough evidence to classify the repo and build the discovered plugin list.
### Practical optimization
We do not need the full contents of every file during discovery.
We mostly need:
- the path list;
- whether certain files exist;
- the content of a small number of manifest files.
So discovery should:
- list tree entries first;
- only fetch file contents for:
- `.claude-plugin/marketplace.json`
- any `.claude-plugin/plugin.json`
- any root-level `plugin.json` used as a metadata hint
- `.mcp.json`
- `.lsp.json`
- `hooks/hooks.json`
- `monitors/monitors.json`
- `settings.json`
Do not eagerly fetch SKILL/agent/command content during the discovery phase.
## Classification algorithm
Discovery should classify the repo in this priority order.
### 1. Marketplace repo
Check for root:
- `.claude-plugin/marketplace.json`
If present:
- classify as `claude_marketplace_repo`;
- parse marketplace entries;
- attempt to resolve entries that point to local repo paths;
- present the listed plugins to the user, ticked by default.
### 2. Explicit plugin manifests
If no marketplace manifest exists, search for all instances of:
- `.claude-plugin/plugin.json`
If one or more are found:
- classify as:
- `claude_single_plugin_repo` if exactly one plugin manifest exists and it is at repo root;
- `claude_multi_plugin_repo` if more than one plugin manifest exists or plugin roots live in subdirectories.
- create one `DiscoveredPlugin` per manifest.
### 3. Standalone Claude folders
If no marketplace manifest and no plugin manifest is found, check for standalone Claude paths:
- `.claude/skills/**`
- `.claude/commands/**`
- `.claude/agents/**`
If present:
- classify as `standalone_claude` in the discovered plugin source kind;
- infer a single plugin rooted at repo root unless stronger folder grouping is present.
### 4. Folder inference
If none of the explicit Claude shapes exist, infer plugin candidates from known component folders.
Known folders:
- `skills/`
- `commands/`
- `agents/`
Rule:
- for each match, examine its parent folder;
- group sibling component folders by that parent;
- create one discovered plugin per parent folder.
Example:
```text
Sales/skills
Sales/commands
finance/agents
finance/commands
```
Discovery result:
- plugin `Sales`
- plugin `finance`
This becomes:
- one plugin candidate rooted at `Sales/`
- one plugin candidate rooted at `finance/`
If the repo itself has root-level `skills/`, `commands/`, or `agents/`, that should infer one root plugin using the repo name as the display name unless better metadata exists.
## Plugin metadata resolution
For each discovered plugin candidate, resolve metadata in this order.
### 1. Official Claude plugin manifest
Check:
- `<root>/.claude-plugin/plugin.json`
If present, use:
- `name`
- `description`
- `version`
- `author`
- other supported metadata as hints
### 2. Loose metadata hint
If no official manifest exists, optionally check:
- `<root>/plugin.json`
This is not an official Claude plugin location.
Treat it as a metadata hint only.
Use:
- `name`
- `description`
Do not treat it as proof that the repo is a Claude plugin.
### 3. Folder-name fallback
If no metadata file exists:
- use the folder name as `displayName`;
- derive a human-friendly label from that folder name.
For a root plugin with no folder name beyond the repo itself, use the repo name.
## Marketplace repo handling
Marketplace repos need special treatment.
### What we should support in v1
Support marketplace entries whose source resolves inside the currently connected repo.
Examples:
- `./plugins/example-plugin`
- `./external_plugins/something`
For these entries:
- resolve the local plugin root;
- inspect that root for components;
- create one `DiscoveredPlugin` for each entry.
### What we should not silently fake in v1
Marketplace entries that point to external URLs or other repos should not be treated as if they were fully present in the current repo.
Examples:
- `source.url = https://github.com/...`
- `source.source = git-subdir`
For those entries, discovery should either:
- mark them as `external source not yet supported in repo discovery`; or
- hide them unless we explicitly decide to support cross-repo expansion.
Recommended v1 behavior:
- show them in the discovery result but disable selection;
- explain that they require external source expansion, which is out of scope for the current single-repo connector flow.
This keeps the behavior honest and still lets users understand what OpenWork detected.
## Inferred plugin rules
### Known component directories
The discovery system should recognize these as plugin-like components:
- `skills/`
- `commands/`
- `agents/`
- `.claude/skills/`
- `.claude/commands/`
- `.claude/agents/`
Optional later additions:
- `hooks/`
- `.mcp.json`
- `.lsp.json`
- `monitors/`
- `settings.json`
### Grouping rules
Group by the nearest plugin root candidate.
Examples:
#### Case A: explicit manifest
```text
plugins/sales/.claude-plugin/plugin.json
plugins/sales/skills
plugins/sales/commands
```
Result:
- one discovered plugin rooted at `plugins/sales`
#### Case B: inferred sibling grouping
```text
Sales/skills
Sales/commands
Finance/agents
Finance/commands
```
Result:
- one discovered plugin rooted at `Sales`
- one discovered plugin rooted at `Finance`
#### Case C: root standalone repo
```text
.claude/skills
.claude/commands
```
Result:
- one discovered plugin rooted at repo root
## UI plan
## Setup page states
Suggested states:
1. `loading`
2. `discovery_running`
3. `discovery_ready`
4. `discovery_empty`
5. `discovery_error`
### discovery_running
Show:
- progress steps;
- current repo name/branch;
- a short explanation that OpenWork is figuring out how to map this repo.
### discovery_ready
Show:
- discovered plugins list;
- each item ticked by default if supported;
- description/metadata when available;
- badges for detected component kinds:
- skills
- commands
- agents
- hooks
- MCP
- warnings for unsupported marketplace entries or ambiguous structure.
Primary CTA:
- `Continue with selected plugins`
Secondary CTA:
- `Review file structure`
### discovery_empty
Show:
- no supported plugin structure found;
- what OpenWork looked for;
- option to create manual mappings.
### discovery_error
Show:
- discovery failed;
- which step failed;
- retry action.
## What the user selects
The user should select plugin groups, not raw files.
Each selected discovered plugin becomes a proposal for:
- one OpenWork `plugin` row;
- a set of `connector_mapping` rows covering that plugin's component folders.
This matches the product goal better than asking the user to map individual folders one by one on first run.
## Mapping discovered plugins to OpenWork internal data
## Internal objects we already have
- `connector_account`
- `connector_instance`
- `connector_target`
- `connector_mapping`
- `connector_sync_event`
- `connector_source_binding`
- `connector_source_tombstone`
- `plugin`
- plugin membership tables
- `config_object`
## Discovery-to-internal mapping
### Discovery phase output
Before the user confirms selection, discovery should exist as draft state.
Recommended persistence model:
- `connector_discovery_run`
- `connector_discovery_candidate`
Conceptually:
```text
connector_discovery_run
- id
- organization_id
- connector_instance_id
- connector_target_id
- source_revision_ref
- status
- classification
- tree_summary_json
- warnings_json
- created_at
- updated_at
connector_discovery_candidate
- id
- discovery_run_id
- key
- source_kind
- root_path
- display_name
- description
- manifest_path
- component_summary_json
- selection_state
- supported
- warnings_json
```
Why add dedicated discovery tables instead of jumping straight to `connector_mapping`?
- discovery is provisional;
- the user may deselect some plugin candidates;
- we want to store unsupported candidates and warnings;
- we want a clean boundary between `what we saw` and `what the user approved`.
### After user confirms selection
For each selected discovered plugin:
1. create or upsert an OpenWork `plugin` row;
2. create one `connector_mapping` per detected component kind/path;
3. set `auto_add_to_plugin = true` for those mappings;
4. link the mapping to the selected OpenWork plugin id;
5. enqueue an initial discovery-approved ingestion sync.
### Example mapping
Repo:
```text
Sales/skills
Sales/commands
finance/agents
finance/commands
```
Discovery result:
- plugin candidate `Sales`
- plugin candidate `finance`
Internal translation after user confirms:
- create OpenWork plugin `Sales`
- create OpenWork plugin `finance`
- create mappings:
- `Sales/skills/**` -> `skill` -> plugin `Sales`
- `Sales/commands/**` -> `command` -> plugin `Sales`
- `finance/agents/**` -> `agent` -> plugin `finance`
- `finance/commands/**` -> `command` -> plugin `finance`
### Marketplace mapping
For a local marketplace entry rooted at `plugins/feature-dev`:
- create one OpenWork plugin from the marketplace/plugin metadata;
- create mappings for each detected component path under that root;
- preserve the marketplace entry metadata as origin/discovery metadata.
## Discovery does not ingest content yet
Discovery should stop short of full content ingestion.
It should:
- inspect paths;
- read manifests and small metadata files;
- infer plugin groups;
- help the user approve a mapping shape.
It should not yet:
- parse every SKILL/agent/command file body;
- create `config_object` rows;
- create `connector_source_binding` rows;
- create tombstones.
Those belong to the subsequent ingestion/reconciliation phase.
## Relationship to initial sync
The initial sync should happen after discovery is approved.
Suggested flow:
1. repo selected
2. connector instance created
3. discovery run computes candidates
4. user confirms selections
5. OpenWork creates plugin rows + connector mappings
6. OpenWork enqueues initial full sync
7. sync executor reads repo contents and materializes config objects
This sequencing is important because ingestion needs the mapping decisions.
## v1 scope
### In scope
- dedicated setup/discovery page after repo selection;
- repo tree listing API with pagination/limits;
- root marketplace detection;
- `.claude-plugin/plugin.json` discovery anywhere in the repo;
- `.claude/skills`, `.claude/commands`, `.claude/agents` support;
- folder-based inference from known component paths;
- user selection UI for discovered plugins;
- translation from selected candidates into plugin rows + connector mappings.
### Explicitly out of scope for this phase
- full content ingestion;
- recursive external marketplace source expansion across other repos;
- hooks-to-OpenWork runtime semantics beyond discovery;
- automatic parsing of every skill/agent/command file body during discovery.
## Open questions
1. Should discovery run synchronously for small repos and asynchronously for larger repos, or always be modeled as a background run?
2. Do we want to persist discovery results in dedicated tables immediately, or temporarily store the first version inside connector metadata while the shape is still changing?
3. For marketplace repos with external URL entries, should we show unsupported entries disabled, or hide them entirely in v1?
4. Should root-level `plugin.json` remain a metadata hint only, or do we want to formalize it as an OpenWork-specific compatibility rule?
5. When multiple discovered plugin candidates have the same normalized name, what is the preferred display/slug collision strategy?
## Recommended next implementation order
1. Add a discovery result model and API endpoints.
2. Implement GitHub tree listing with truncation-aware fallback.
3. Implement classification + candidate extraction.
4. Update the GitHub setup page to become the discovery page.
5. Add the discovered plugin selection UI.
6. Convert approved candidates into `plugin` + `connector_mapping` rows.
7. Then implement initial ingestion against those mappings.