Skip to content

Install skills and plugins from any git host - #279

Open
Minitour wants to merge 1 commit into
developfrom
feat/generic-git-source
Open

Minitour wants to merge 1 commit into
developfrom
feat/generic-git-source

Conversation

@Minitour

Copy link
Copy Markdown
Member

Summary

capa add only understood github.com and gitlab.com. A clone URL on any other host (Gitea, Forgejo, self-hosted servers, internal registries) fell through to the raw-URL skill type, so capa downloaded the host's HTML page, saved it as SKILL.md and reported success. This adds a git source type so any git host works for skills and plugins.

What changed

  • New type: git for skills (def: { url, path?, version?, ref? }) and plugins (def: { url, subpath?, version?, ref? }), in the schema, types and lockfile (source: git plus url).
  • It reuses the existing mirror cache: CachePlatform gains git, keyed by <host>/<path> (file:// URLs by name plus a hash to stay under Windows path limits), with the clone URL threaded through getRepoSnapshot. Pinning by tag or commit, "newest vX.Y.Z tag" and lock reuse behave exactly as for GitHub.
  • capa add recognizes https://host/path/repo.git[::path][:version|#sha] for skills and --plugin. GitHub and GitLab URLs still parse as before.
  • A git skill installs the SKILL.md at the repo root by default. When it's missing, the error lists the skills found in the repo.
  • Credentials come from the user's git credential helper (capa already disables prompts). Clone errors for git hosts say how to sign in instead of pointing at the GitHub/GitLab integrations page.
  • Fixed while wiring plugins: plugin resolution skipped any entry without def.repo, and the duplicate check in capa add --plugin compared def.repo only.
  • Rules and hooks reject .git URLs with a clear message until they support them. A remote skill whose response is an HTML page now fails instead of installing it.
  • Docs: capabilities-manager command and schema references.

Not included (follow-ups): auth integration for generic hosts (capa auth, env tokens), rules/hooks/instructions from git hosts, registry adapters, web UI source badges.

Screenshots / logs

$ capa add https://git.example.com/acme/code-review.git
✓ Added skill "code-review" to capabilities.yaml
  Type: git
  URL: https://git.example.com/acme/code-review.git

$ capa install -p claude-code --yes
✓ Done · 15 added

# capabilities.lock
- id: code-review
  source: git
  repo: git.example.com/acme/code-review
  url: https://git.example.com/acme/code-review.git
  resolvedRef: 7fa4825b…
  resolvedVersion: v0.1.0

Missing or private repo:

Skill "does-not-exist" failed:
  Authentication failed for https://git.example.com/acme/does-not-exist.git (repository not accessible)
  If the repository is private, sign in once with `git clone …` so your git credential helper remembers the credentials, then re-run `capa install`.

Test plan

  • Unit tests for URL parsing, cache keys, capa add skill/plugin parsing and plugin validation (src/shared/__tests__/git-url.test.ts)
  • Real install from a bare repo over file://: root skill at the newest tag with the URL in the lock, nested def.path, and the "skills found" error (install-one-skill-git.test.ts)
  • Compiled binary on Windows: capa add + capa install of a skill from a live non-GitHub HTTPS git host (pinned to its v0.1.0 tag), a plugin from a bare repo (its skill installed), and the error for a missing repo
  • Full suite: no new failures compared to main locally
  • bunx tsc --noEmit, bunx tsc --noEmit -p web-ui/tsconfig.json, biome lint on changed files

Checklist

  • Tests added or updated
  • bunx tsc --noEmit passes
  • Docs updated (if user-facing)

🤖 Generated with Claude Code

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Install skills and plugins from any Git host

✨ Enhancement 🐞 Bug fix 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Add Git clone URL sources so skills and plugins can install from hosts beyond GitHub and GitLab.
• Reuse repository snapshots and lockfiles for pinning, while making unsupported URLs and clone
 failures clearer.
• Document the new source type and test parsing and installation from a local Git repository.
Diagram

graph TD
  ADD["capa add"] --> PARSE["Git URL parser"] --> CONFIG["Capabilities config"] --> INSTALL["Install pipeline"] --> CACHE[("Mirror cache")] --> HOST["Git host"]
  INSTALL --> LOCK[("Lockfile")]
  INSTALL --> FILES["Provider files"]
Loading
High-Level Assessment

Reusing the mirror and snapshot pipeline is the best fit: it preserves existing pinning and installation behavior without duplicating Git operations. A generic entry in the Git-provider registry was considered, but that registry assumes host-specific URL parsing and OAuth integration that arbitrary hosts cannot share.

Files changed (26) +535 / -43

Enhancement (20) +331 / -38
add-parse-plugin.tsParse Git clone URLs as plugin sources +15/-2

Parse Git clone URLs as plugin sources

• Maps a clone URL and optional subpath or pin to a Git plugin definition. Adds the new format to CLI usage guidance.

src/cli/commands/add-parse-plugin.ts

add-parse-skill.tsParse Git clone URLs as skill sources +19/-1

Parse Git clone URLs as skill sources

• Recognizes clone URLs before the raw remote-URL fallback and creates Git skill definitions with optional paths and pins.

src/cli/commands/add-parse-skill.ts

context.tsCarry clone URLs through snapshot requests +1/-1

Carry clone URLs through snapshot requests

• Extends the install task's snapshot function type with an optional repository URL.

src/cli/commands/install-tasks/context.ts

git.tsProvide host-appropriate Git clone errors +51/-3

Provide host-appropriate Git clone errors

• Adds error messages for generic hosts, including credential-helper sign-in guidance, without directing users to GitHub or GitLab integrations.

src/cli/commands/install-tasks/helpers/git.ts

install-one-skill.tsInstall Git skills from cached repository snapshots +74/-3

Install Git skills from cached repository snapshots

• Resolves Git skill clone URLs, installs root or selected SKILL.md directories, and records source URLs and revisions in the lockfile. Reports discoverable skills when the selected file is absent and adds a check intended to reject HTML remote responses.

src/cli/commands/install-tasks/helpers/install-one-skill.ts

repo-snapshot.tsPass clone URLs into snapshot resolution +7/-3

Pass clone URLs into snapshot resolution

• Forwards optional clone URLs to the cache and Git error handling while preserving generated URLs for existing hosts.

src/cli/commands/install-tasks/helpers/repo-snapshot.ts

install-skills.tsCheck for Git when installing Git skills +1/-1

Check for Git when installing Git skills

• Includes the new source type in the pre-install Git availability check.

src/cli/commands/install-tasks/install-skills.ts

write-lockfile.tsRetain Git skills in lockfile processing +1/-1

Retain Git skills in lockfile processing

• Includes Git skills alongside GitHub and GitLab skills when collecting lockfile entries.

src/cli/commands/install-tasks/write-lockfile.ts

plugin-install.tsResolve and lock plugins from arbitrary Git hosts +17/-9

Resolve and lock plugins from arbitrary Git hosts

• Allows URL-backed plugins through resolution, cloning, manifest discovery, and lockfile creation. Uses the clone URL for plugin source links and relevant errors.

src/cli/commands/plugin-install.ts

resolve-effective-capabilities.tsInclude plugin URLs in effective-capabilities cache keys +1/-1

Include plugin URLs in effective-capabilities cache keys

• Uses a Git plugin's clone URL when no repo string exists, so URL-backed sources participate in cache-key generation.

src/server/resolve-effective-capabilities.ts

mirror.tsRequire explicit clone URLs for generic Git mirrors +3/-0

Require explicit clone URLs for generic Git mirrors

• Prevents the cache from constructing a GitHub-style URL for the generic Git platform.

src/shared/cache/mirror.ts

paths.tsAdd Git to cached repository platforms +2/-1

Add Git to cached repository platforms

• Extends cache platform typing so generic Git repositories can use existing mirror and snapshot paths.

src/shared/cache/paths.ts

capabilities.tsAccept Git sources in capabilities schemas +5/-2

Accept Git sources in capabilities schemas

• Adds Git skill and plugin types and allows plugin definitions to carry a clone URL instead of a repo string.

src/shared/capabilities.ts

executable-surface.tsRepresent URL-backed plugins in executable surfaces +4/-2

Represent URL-backed plugins in executable surfaces

• Falls back to a Git plugin's clone URL for its source identifier and displayed repository value.

src/shared/executable-surface.ts

git-url.tsAdd shared Git clone URL parsing and cache keys +66/-0

Add shared Git clone URL parsing and cache keys

• Parses clone URLs with optional paths and revision selectors. Derives host/path cache keys and bounded, hashed keys for file:// repositories.

src/shared/git-url.ts

lockfile.tsValidate Git skill and plugin lock entries +3/-1

Validate Git skill and plugin lock entries

• Recognizes Git as a lock source and requires a clone URL on its skill and plugin entries.

src/shared/lockfile.ts

plugin-source.tsValidate URL-backed Git plugin definitions +44/-0

Validate URL-backed Git plugin definitions

• Validates Git plugin clone URLs and subpaths, produces the repository cache key, and provides a common repo-or-URL accessor.

src/shared/plugin-source.ts

capabilities.tsType Git skill definitions +3/-1

Type Git skill definitions

• Adds Git to skill source types and documents the clone URL and in-repository path fields.

src/types/capabilities.ts

lockfile.tsType Git lock sources and clone URLs +9/-4

Type Git lock sources and clone URLs

• Adds Git to lock source types and optional clone URLs to skill and plugin lock entries.

src/types/lockfile.ts

plugin.tsType URL-backed Git plugins +5/-2

Type URL-backed Git plugins

• Adds the Git plugin type and makes room for clone URLs in plugin definitions while retaining repo strings for existing hosts.

src/types/plugin.ts

Bug fix (2) +17 / -3
add-builders.tsReject Git clone URLs for rules and hooks +13/-0

Reject Git clone URLs for rules and hooks

• Produces explicit unsupported-source errors instead of treating clone URLs as raw rule or hook files.

src/cli/commands/add-builders.ts

add.tsCompare and display plugin URLs correctly +4/-3

Compare and display plugin URLs correctly

• Uses either a repo string or clone URL for plugin duplicate checks and add-command output.

src/cli/commands/add.ts

Tests (2) +174 / -0
install-one-skill-git.test.tsTest Git skill installation against a bare repository +100/-0

Test Git skill installation against a bare repository

• Exercises cloning and installation over file://, newest-tag selection, nested skill paths, lockfile URLs, and missing-SKILL.md guidance.

src/cli/commands/install-tasks/helpers/tests/install-one-skill-git.test.ts

git-url.test.tsTest Git URL parsing and source validation +74/-0

Test Git URL parsing and source validation

• Covers host and port handling, paths and pins, cache keys, skill and plugin parsing, and preservation of GitHub URL behavior.

src/shared/tests/git-url.test.ts

Documentation (2) +13 / -2
capabilities-schema.mdDocument Git skill and plugin definitions +11/-2

Document Git skill and plugin definitions

• Adds the Git skill source and a plugin example using a clone URL, subpath, and version. Clarifies how Git definitions differ from GitHub and GitLab repo strings.

skills/capabilities-manager/references/capabilities-schema.md

commands.mdDocument Git host add commands +2/-0

Document Git host add commands

• Shows how to add Git-hosted skills and plugins, select subpaths, pin revisions, and use a Git credential helper for private repositories.

skills/capabilities-manager/references/commands.md

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (6) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Git installs can use the wrong repository 🐞 Bug ≡ Correctness
Description
gitRepoKey() omits the URL scheme and normalizes distinct URL components into the same cache key.
When two clone URLs share that key, ensureMirrorClone() keeps the first URL's existing mirror, so
a later skill or plugin install can receive content from the first repository.
Code

src/shared/git-url.ts[R62-65]

+	return [u.host, ...path.split("/")]
+		.filter((s) => s && s !== "." && s !== "..")
+		.map((s) => s.replace(/[<>:"|?*\\]/g, "_"))
+		.join("/");
Evidence
HTTP and HTTPS URLs with the same host and path produce the same key. Cache directories use that
key, and the existing-mirror branch does not compare its origin with the newly requested URL.

src/shared/git-url.ts[54-65]
src/shared/cache/paths.ts[14-29]
src/shared/cache/mirror.ts[137-160]
src/shared/cache/snapshot.ts[115-145]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Distinct clone URLs can share a generic git cache key, causing installs to use an existing mirror for another URL.
## Fix Focus Areas
- src/shared/git-url.ts[54-65]
- src/shared/cache/mirror.ts[137-160]
## Recommended Fix
Include all source-distinguishing URL components in a safe cache identity and verify that an existing mirror's origin matches the requested clone URL before reusing it.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Changed plugin URLs keep old content 🐞 Bug ≡ Correctness
Description
resolvePlugins() passes a prior plugin lock's SHA to snapshot resolution without checking the lock
entry's url against the current repoUrl. If a git plugin's clone URL changes but retains the
same normalized cache key, the pinned-snapshot fast path can install the old content and then record
the new URL in the lock.
Code

src/cli/commands/plugin-install.ts[424]

+          repoUrl,
Evidence
Both plugin lock lookup paths match source and normalized repo but not URL. The matched SHA is
passed as a pin, and snapshot resolution can return an existing snapshot without consulting the
requested URL.

src/cli/commands/plugin-install.ts[393-425]
src/shared/lockfile.ts[382-415]
src/shared/cache/snapshot.ts[115-133]
src/shared/git-url.ts[54-65]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A git plugin can reuse a lock pin created for a different clone URL with the same cache key.
## Fix Focus Areas
- src/cli/commands/plugin-install.ts[393-424]
- src/shared/lockfile.ts[382-415]
- src/shared/lockfile.ts[423-449]
## Recommended Fix
Require the previous git lock entry's URL to match the current clone URL in both plugin lock lookup paths before passing its resolved SHA to snapshot resolution.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Remote skills that return a web page still install 🐞 Bug ≡ Correctness
Description
installOneSkill() uses /^s*]/i, which matches literal s characters instead of whitespace.
Standard ` and ` responses, including ones with leading whitespace, bypass the check and continue
through installation as SKILL.md while the install reports success.
Code

src/cli/commands/install-tasks/helpers/install-one-skill.ts[315]

+      if (/^s*<(!doctype|html)[s>]/i.test(skillMarkdown)) {
Evidence
The expression requires literal s characters before the opening tag and after doctype or html,
so ordinary HTML document beginnings do not match. When the check does not throw, the fetched
response remains skillMarkdown and proceeds through installation; the existing server check in
skill-content.ts handles leading whitespace with trimStart() and checks the tag with
startsWith().

src/cli/commands/install-tasks/helpers/install-one-skill.ts[313-320]
src/server/skill-content.ts[372-377]
src/cli/commands/install-tasks/helpers/install-one-skill.ts[310-325]
src/cli/commands/install-tasks/helpers/install-one-skill.ts[399-426]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The remote-skill HTML check uses literal `s` characters where whitespace matching is needed, allowing standard HTML responses to proceed through skill installation.
## Fix Focus Areas
- src/cli/commands/install-tasks/helpers/install-one-skill.ts[312-320]
## Recommended Fix
Change the expression to `/^\s*<(!doctype|html)[\s>]/i`, or use `skillMarkdown.trimStart().slice(0,200).toLowerCase()` with `startsWith('<!doctype') || startsWith('<html')`, as in `src/server/skill-content.ts`. Add tests for ordinary HTML responses with and without leading whitespace, including `<!DOCTYPE html>\n<html lang="en">`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View high (1)
4. Web UI plugin list crashes on git plugins ⊘ Outdated 🐞 Bug ☼ Reliability
Description
The schema and types now allow a plugin with no def.repo, and project-routes sends def to the
browser unchanged. PluginsEditor calls plugin.def.repo.split('@') without a guard, so any git
plugin whose resolved name differs from its id throws a TypeError and the plugins section fails to
render; the preview dialog also shows capa add undefined.
Code

src/types/plugin.ts[R62-64]

+  repo?: string;
+  /** Clone URL for `git` plugins (any host). The plugin location goes in `subpath`. */
+  url?: string;
Evidence
The PR makes repo optional and adds the git plugin type. The server passes def through
unchanged, and the UI calls .split on def.repo without a guard.

src/shared/capabilities.ts[217-234]
src/server/project-routes.ts[290-294]
web-ui/src/features/projects/components/PluginsEditor.tsx[82-94]
web-ui/src/types/api.ts[190-199]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Git plugins have `def.url` and no `def.repo`, but the web UI dereferences `def.repo` unconditionally.
## Fix Focus Areas
- web-ui/src/types/api.ts[190-199]
- web-ui/src/features/projects/components/PluginsEditor.tsx[46-128]
- web-ui/src/features/projects/components/PluginPreviewDialog.tsx[26-131]
## Recommended Fix
Make `repo` optional and add `url?: string` to `AuthoredPlugin.def`. Compute `const source = plugin.def.repo ?? plugin.def.url ?? ''` and use it for the display name, the key, `.split`, and the preview text.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

5. Plugins with no repository disappear 🐞 Bug ☼ Reliability
Description
pluginDefSchema now makes both def.repo and def.url optional, allowing a plugin with neither
source through capabilities-file validation. When that entry reaches resolvePlugins(),
pluginSourceOf() returns no source and the loop silently continues before validatePluginDef()
can report the missing field.
Code

src/shared/capabilities.ts[R220-222]

+		// github/gitlab use `repo`; git uses `url` (validatePluginDef checks which).
+		repo: z.string().optional(),
+		url: z.string().optional(),
Evidence
The schema accepts an empty plugin definition, while the resolution loop skips an empty source
before calling the existing validator that would identify the missing field.

src/shared/capabilities.ts[217-248]
src/cli/commands/plugin-install.ts[360-387]
src/shared/plugin-source.ts[89-105]
src/shared/plugin-source.ts[180-185]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Plugin definitions with no repository or clone URL now validate but are silently skipped during installation.
## Fix Focus Areas
- src/shared/capabilities.ts[217-248]
- src/cli/commands/plugin-install.ts[360-387]
## Recommended Fix
Validate the required source field according to plugin type in the schema, and let source-less entries reach the plugin validator so an actionable warning is emitted.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Cache cleanup leaves git clones behind ⊘ Outdated 🐞 Bug ≡ Correctness
Description
Adding git as a cache platform creates cache directories that getCacheStats() explicitly
excludes. When only generic-git repositories are cached, capa cache reports an empty cache and
capa cache clean returns before deleting those repositories.
Code

src/shared/cache/paths.ts[R5-6]

+/** `git` is any other host: its repoPath is `gitRepoKey(url)` and callers pass the clone URL. */
+export type CachePlatform = "github" | "gitlab" | "git";
Evidence
Generic repositories are stored below the new platform directory, but the stats scanner accepts only
github and gitlab. The cleanup command treats an empty stats list as proof there is nothing to
delete.

src/shared/cache/paths.ts[5-25]
src/shared/cache/stats.ts[51-70]
src/cli/commands/cache.ts[33-43]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Generic-git cache entries are omitted from cache statistics, preventing cleanup when they are the only entries.
## Fix Focus Areas
- src/shared/cache/stats.ts[51-100]
- src/shared/cache/paths.ts[14-29]
- src/cli/commands/cache.ts[33-43]
## Recommended Fix
Scan the git platform and recursively find repository cache directories, since host and repository paths can have more than two segments; include them in the totals used by cache cleanup.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Uninstalled git skills return no content ⊘ Outdated 🐞 Bug ≡ Correctness
Description
The new git skill type has an install branch, but fetchUninstalledSkillContent() has branches
only for remote, GitHub and GitLab skills. Before a git skill is installed locally,
resolveSkillContentById() therefore returns null and the skill-content route responds with a 404.
Code

src/cli/commands/install-tasks/helpers/install-one-skill.ts[234]

+  } else if (skill.type === 'git' && skill.def.url) {
Evidence
The route calls the content resolver and returns 404 on null. Its pre-install fallback never handles
the newly added skill type, despite that type being installable.

src/server/mcp-meta-routes.ts[239-255]
src/server/skill-content.ts[342-355]
src/server/skill-content.ts[364-420]
src/cli/commands/install-tasks/helpers/install-one-skill.ts[234-290]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The skill-content route cannot retrieve a generic-git skill before its first local installation.
## Fix Focus Areas
- src/server/skill-content.ts[342-420]
- src/cli/commands/install-tasks/helpers/install-one-skill.ts[234-290]
## Recommended Fix
Add a generic-git branch to the uninstalled-content fallback, using the clone URL and safe skill path with the shared snapshot cache, then parse its SKILL.md.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View medium (2)
8. Failed git skills retain lock pins 🐞 Bug ☼ Reliability
Description
The git branch calls lockBuilder.upsertSkill() before checking for SKILL.md or copying the
skill. If either later step fails, the install task records the failure but lockfile writing retains
the new git skill's ID and saves its pin, which a subsequent install can reuse.
Code

src/cli/commands/install-tasks/helpers/install-one-skill.ts[R265-269]

+    lockBuilder.upsertSkill({
+      id: skill.id,
+      source: 'git',
+      repo: repoPath,
+      url,
Evidence
The new git branch inserts its pin before its missing-file error and later copy operations. Failure
handling continues to lockfile writing, whose pruning keeps every declared git skill ID.

src/cli/commands/install-tasks/helpers/install-one-skill.ts[265-290]
src/cli/commands/install-tasks/install-skills.ts[38-65]
src/cli/commands/install-tasks/write-lockfile.ts[15-45]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A failed generic-git skill install can persist a lock pin for content that was never installed.
## Fix Focus Areas
- src/cli/commands/install-tasks/helpers/install-one-skill.ts[265-290]
- src/cli/commands/install-tasks/install-skills.ts[38-65]
- src/cli/commands/install-tasks/write-lockfile.ts[15-40]
## Recommended Fix
Stage the git skill lock entry until its path validation and installation succeed, or remove that entry when installation fails; avoid pruning based solely on declared IDs.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


9. A missing tag is reported as a missing repo 🐞 Bug ◔ Observability
Description
explainGenericGitError treats any message that contains 'not found' as a missing repository. The
cache's own ref-resolution errors (Tag/branch "v2" not found, Commit … not found) contain that
phrase, so a bad def.version or def.ref becomes 'Repository not found … sign in with git', and
the real cause is dropped.
Code

src/cli/commands/install-tasks/helpers/git.ts[R60-62]

+  if (errorMessage.includes('not found') || errorMessage.includes('does not appear to be a git repository')) {
+    return new Error(`Repository not found: ${repoUrl}\nCheck the URL, or sign in with git if it is private.`);
+  }
Evidence
resolveRef throws plain Errors whose messages contain 'not found', and getRepoSnapshot passes every
error through explainGitError.

src/shared/cache/mirror.ts[290-308]
src/cli/commands/install-tasks/helpers/repo-snapshot.ts[18-31]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The generic git error mapper rewrites 'Tag/branch … not found' and 'Commit … not found' as 'Repository not found'.
## Fix Focus Areas
- src/cli/commands/install-tasks/helpers/git.ts[39-70]
## Recommended Fix
Before the 'not found' check, return the original error when the message starts with 'Tag/branch', 'Commit ' or 'Pinned commit'. Otherwise match only git transport text such as `repository '` + `' not found` or 'does not appear to be a git repository'.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can turn on the rule miner and Qodo learns your standards from review history

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/shared/git-url.ts
Comment on lines +62 to +65
return [u.host, ...path.split("/")]
.filter((s) => s && s !== "." && s !== "..")
.map((s) => s.replace(/[<>:"|?*\\]/g, "_"))
.join("/");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Git installs can use the wrong repository 🐞 Bug ≡ Correctness

gitRepoKey() omits the URL scheme and normalizes distinct URL components into the same cache key.
When two clone URLs share that key, ensureMirrorClone() keeps the first URL's existing mirror, so
a later skill or plugin install can receive content from the first repository.
Agent Prompt
## Issue description
Distinct clone URLs can share a generic git cache key, causing installs to use an existing mirror for another URL.
## Fix Focus Areas
- src/shared/git-url.ts[54-65]
- src/shared/cache/mirror.ts[137-160]
## Recommended Fix
Include all source-distinguishing URL components in a safe cache identity and verify that an existing mirror's origin matches the requested clone URL before reusing it.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

ref,
pinnedSha,
noCache,
repoUrl,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Changed plugin urls keep old content 🐞 Bug ≡ Correctness

resolvePlugins() passes a prior plugin lock's SHA to snapshot resolution without checking the lock
entry's url against the current repoUrl. If a git plugin's clone URL changes but retains the
same normalized cache key, the pinned-snapshot fast path can install the old content and then record
the new URL in the lock.
Agent Prompt
## Issue description
A git plugin can reuse a lock pin created for a different clone URL with the same cache key.
## Fix Focus Areas
- src/cli/commands/plugin-install.ts[393-424]
- src/shared/lockfile.ts[382-415]
- src/shared/lockfile.ts[423-449]
## Recommended Fix
Require the previous git lock entry's URL to match the current clone URL in both plugin lock lookup paths before passing its resolved SHA to snapshot resolution.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +220 to +222
// github/gitlab use `repo`; git uses `url` (validatePluginDef checks which).
repo: z.string().optional(),
url: z.string().optional(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

5. Plugins with no repository disappear 🐞 Bug ☼ Reliability

pluginDefSchema now makes both def.repo and def.url optional, allowing a plugin with neither
source through capabilities-file validation. When that entry reaches resolvePlugins(),
pluginSourceOf() returns no source and the loop silently continues before validatePluginDef()
can report the missing field.
Agent Prompt
## Issue description
Plugin definitions with no repository or clone URL now validate but are silently skipped during installation.
## Fix Focus Areas
- src/shared/capabilities.ts[217-248]
- src/cli/commands/plugin-install.ts[360-387]
## Recommended Fix
Validate the required source field according to plugin type in the schema, and let source-less entries reach the plugin validator so an actionable warning is emitted.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread src/shared/cache/paths.ts
Comment thread src/cli/commands/install-tasks/helpers/install-one-skill.ts
Comment on lines +265 to +269
lockBuilder.upsertSkill({
id: skill.id,
source: 'git',
repo: repoPath,
url,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

8. Failed git skills retain lock pins 🐞 Bug ☼ Reliability

The git branch calls lockBuilder.upsertSkill() before checking for SKILL.md or copying the
skill. If either later step fails, the install task records the failure but lockfile writing retains
the new git skill's ID and saves its pin, which a subsequent install can reuse.
Agent Prompt
## Issue description
A failed generic-git skill install can persist a lock pin for content that was never installed.
## Fix Focus Areas
- src/cli/commands/install-tasks/helpers/install-one-skill.ts[265-290]
- src/cli/commands/install-tasks/install-skills.ts[38-65]
- src/cli/commands/install-tasks/write-lockfile.ts[15-40]
## Recommended Fix
Stage the git skill lock entry until its path validation and installation succeed, or remove that entry when installation fails; avoid pruning based solely on declared IDs.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

skillMarkdown = await response.text();
// A web page is never a SKILL.md: usually a repository page or a clone URL
// missing its .git suffix. Installing it would hand the agent HTML.
if (/^s*<(!doctype|html)[s>]/i.test(skillMarkdown)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

3. Remote skills that return a web page still install 🐞 Bug ≡ Correctness

installOneSkill() uses /^s*<(!doctype|html)[s>]/i, which matches literal s characters instead
of whitespace. Standard <!DOCTYPE html> and <html lang="en"> responses, including ones with
leading whitespace, bypass the check and continue through installation as SKILL.md while the install
reports success.
Agent Prompt
## Issue description
The remote-skill HTML check uses literal `s` characters where whitespace matching is needed, allowing standard HTML responses to proceed through skill installation.

## Fix Focus Areas
- src/cli/commands/install-tasks/helpers/install-one-skill.ts[312-320]

## Recommended Fix
Change the expression to `/^\s*<(!doctype|html)[\s>]/i`, or use `skillMarkdown.trimStart().slice(0,200).toLowerCase()` with `startsWith('<!doctype') || startsWith('<html')`, as in `src/server/skill-content.ts`. Add tests for ordinary HTML responses with and without leading whitespace, including `<!DOCTYPE html>\n<html lang="en">`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread src/types/plugin.ts
Comment on lines +60 to +62
if (errorMessage.includes('not found') || errorMessage.includes('does not appear to be a git repository')) {
return new Error(`Repository not found: ${repoUrl}\nCheck the URL, or sign in with git if it is private.`);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

9. A missing tag is reported as a missing repo 🐞 Bug ◔ Observability

explainGenericGitError treats any message that contains 'not found' as a missing repository. The
cache's own ref-resolution errors (Tag/branch "v2" not found, Commit … not found) contain that
phrase, so a bad def.version or def.ref becomes 'Repository not found … sign in with git', and
the real cause is dropped.
Agent Prompt
## Issue description
The generic git error mapper rewrites 'Tag/branch … not found' and 'Commit … not found' as 'Repository not found'.

## Fix Focus Areas
- src/cli/commands/install-tasks/helpers/git.ts[39-70]

## Recommended Fix
Before the 'not found' check, return the original error when the message starts with 'Tag/branch', 'Commit ' or 'Pinned commit'. Otherwise match only git transport text such as `repository '` + `' not found` or 'does not appear to be a git repository'.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

A clone URL ending in .git on a host other than github.com or gitlab.com
used to fall through to the raw-URL skill type, which saved the host's
HTML page as SKILL.md and reported success.

Skills and plugins get a `git` type with `def.url` (the clone URL). It
reuses the existing mirror cache, keyed by host and path, so pinning by
tag or commit, "latest version tag" and the lockfile work as for GitHub.
Credentials come from the user's git credential helper. `capa add`
recognizes `https://host/path/repo.git[::path][:version|#sha]`, rules and
hooks refuse such URLs until they support them, and a remote skill that
returns a web page now fails instead of installing HTML.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Minitour
Minitour force-pushed the feat/generic-git-source branch from 2dbead7 to c727cd0 Compare September 28, 2026 16:06
@Minitour
Minitour changed the base branch from main to develop September 28, 2026 16:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant