Feature Specification: Skills-Only Installer Overhaul
Feature Branch: fix/skills-ai-generate-use-dynamic-agents (continuing on existing branch)
Created: 2026-05-04
Status: Draft (Phases 1-5 implemented; Phase 6 — multi-source crawl + path filtering — added 2026-05-04 as a follow-up on the same branch)
Input: Refactor the CAIPE skill installer (/api/skills/install.sh) to use a single SKILL.md layout for every supported coding agent, drop the legacy commands/ layout toggle and per-agent file-format machinery, install each skill to two universal locations per scope so one install satisfies all five supported agents, and migrate the two CAIPE helper slash commands to the new layout. Phase 6 extends the catalog ingestion side: bring GitLab to feature parity with GitHub in the per-skill ad-hoc importer, and add an include_paths filter to both the hub-level crawler and the per-skill importer so admins can pin ingestion to specific subdirectories of large monorepos.
Background
The CAIPE Skills Gateway today exposes /api/skills/install.sh — a one-line curl | bash installer that drops every catalog skill onto a user's machine in a per-agent file format and directory layout. It supports two layouts (commands and skills) and four file formats (markdown-frontmatter, markdown-plain, gemini-toml, continue-json-fragment), driven by the agent registry in ui/src/app/api/skills/live-skills/agents.ts.
Since shipping that installer, the four major coding agents that we target (Claude Code, Cursor, Codex CLI, Gemini CLI) plus a fifth (opencode) have all standardized on the open agentskills.io SKILL.md format under a skills/<name>/SKILL.md tree (see Claude Code, Cursor, Codex, Gemini CLI, opencode). All five also auto-discover from a vendor-neutral ~/.agents/skills/ mirror. Continue and Spec Kit, the two outliers we previously had to special-case, are no longer worth carrying.
This means the layout toggle, the four per-agent renderers, the format/extension/fragment plumbing, and the ~/.claude/settings.json allowlist patch are all dead weight. Every install can write the same SKILL.md to the same two universal paths, regardless of which agent the user picked in the UI.
User Scenarios & Testing
User Story 1 — One install, every agent (Priority: P1)
A developer runs the default curl | bash installer once. They later switch from Claude Code to Cursor (or add Codex CLI alongside). Their CAIPE skills are already discoverable in the new agent — no second install needed.
Why this priority: This is the core user-facing simplification of the overhaul. Today the agent picker isn't just cosmetic — picking the wrong one writes to the wrong directory and the user has to re-run install. After this work, the picker only changes the launch guide text shown in the UI; the bytes on disk are identical.
Independent Test:
- Run
curl -fsSL '<gateway>/api/skills/install.sh?agent=claude&scope=user' | bashagainst a clean$HOME. - Inspect
~/.claude/skills/<name>/SKILL.mdand~/.agents/skills/<name>/SKILL.md— both exist for every catalog skill. - Re-run with
?agent=cursoragainst the same$HOME— every file is byte-identical (idempotent), no churn in the manifest.
Acceptance Scenarios:
- Given a catalog with N non-flagged skills, When the user runs install with
agent=claude&scope=user, Then2*Nfiles are written (one per skill into~/.claude/skills/<name>/SKILL.mdand one into~/.agents/skills/<name>/SKILL.md), every file is a validSKILL.mdwithname:anddescription:frontmatter, and the manifest recordspaths: [<both paths>]per entry. - Given install has already been run with
agent=claude, When the user re-runs withagent=cursor(same scope), Then zero new files are written, the manifest is unchanged, and the launch-guide footer reflects Cursor's slash command syntax. - Given the user picks
scope=projectfrom the Advanced disclosure, When install runs, Then files land in.claude/skills/<name>/SKILL.mdand.agents/skills/<name>/SKILL.mdunder the current working directory, the manifest is./.caipe/installed.json, and the success card includes a.gitignorereminder for.caipe/and.claude/and.agents/.
User Story 2 — Helpers ship as real Skills (Priority: P1)
The two CAIPE-authored helpers (/skills for the live catalog browser, /update-skills for in-place upgrades) install as proper SKILL.md files alongside the catalog skills, with the right frontmatter so the agent does not nag the user for permission every time the helper shells out to the local Python catalog client.
Why this priority: The helpers are how users discover the live catalog and roll forward without re-running curl | bash. Today they install as commands/<name>.md files plus a special-case allowlist patch in ~/.claude/settings.json. Moving them into the same skills/<name>/SKILL.md tree as everything else removes the special case and makes the install behavior uniform.
Independent Test:
- Install with default flags. Confirm
~/.claude/skills/skills/SKILL.mdand~/.claude/skills/update-skills/SKILL.mdexist withdisable-model-invocation: trueand anallowed-tools:line that pre-approvesBash(uv run ~/.config/caipe/caipe-skills.py*)andBash(python3 ~/.config/caipe/caipe-skills.py*). - Inspect
~/.claude/settings.json— the SessionStart hook entry is present, but the twoBash(...caipe-skills.py*)allowlist entries are gone. - Run
/skillsinside Claude Code — no permission prompt fires for the Python invocation.
Acceptance Scenarios:
- Given a fresh install, When the helper templates are rendered, Then their frontmatter contains exactly
name,description,disable-model-invocation: true, and anallowed-toolsarray with both theuv runandpython3invocations of the catalog client (so users withoutuvstill get pre-approval). - Given an install that previously wrote helpers in
commands/layout, When the user re-runs install with--upgrade, Then the legacy~/.claude/commands/skills.md,~/.claude/commands/update-skills.md,~/.cursor/commands/...,~/.codex/prompts/...,~/.gemini/commands/..., and~/.config/opencode/command/...files are removed and replaced with the newSKILL.mdlayout. - Given a previous install added the two
Bash(...caipe-skills.py*)allowlist entries to~/.claude/settings.json, When the user re-runs install, Then those two specific entries are removed frompermissions.allowwhile the SessionStart hook entry underhooksis preserved untouched.
User Story 3 — Sane uninstall across both scopes (Priority: P2)
A developer who installed at both user and project scope runs the uninstall one-liner once and gets prompted to clean each manifest's entries individually with the existing y/N/a/q confirmation flow.
Why this priority: Today's uninstall walks one manifest per invocation (the scope passed in). Once an install lays down two paths per skill into universal locations, the manifest entry shape has to widen from path: <string> to paths: <string[]> and the uninstall has to know how to enumerate, prompt for, and remove every file in the array. Walking both manifests in one run also matches user expectations once the project scope is demoted to "advanced".
Independent Test:
- Install at
scope=user, thencdinto a project and install atscope=project. Verify both manifests exist and both referencepaths: [<2 entries>]per skill. - Run
curl -fsSL '<gateway>/api/skills/install.sh?mode=uninstall' | bash(noscope=param). - Confirm the script walks the user manifest first, then the project manifest, prompting per skill in each, and that an
a(apply-to-all) answer in the user-manifest pass does not auto-apply across the project-manifest pass (each manifest's prompt loop is independent — explicit consent per scope).
Acceptance Scenarios:
- Given both user and project manifests exist, When uninstall runs without
scope=, Then both manifests are walked in deterministic order (user, then project), each with its own y/N/a/q loop, and an empty parent skill directory (<root>/skills/<name>/) is removed once itsSKILL.mdis gone. - Given a manifest entry of the new shape
{ "name": "...", "paths": ["~/.claude/skills/foo/SKILL.md", "~/.agents/skills/foo/SKILL.md"] }, When the user confirms uninstall for that entry, Then both files are removed in a single confirmation, and the parentskills/foo/directory is rmdir'd in each tree if empty. - Given a legacy manifest entry of the old shape
{ "name": "...", "path": "~/.claude/commands/foo.md" }, When uninstall runs, Then the script handles the legacy shape transparently (treatspathas a one-elementpaths), removes the file, and the manifest is rewritten in the new shape. - Given the user passes
--purgeto a full-uninstall flow, When the script finishes removing skill files, Then~/.config/caipe/config.jsonand./.caipe/config.jsonare also deleted (existing behavior preserved).
User Story 4 — Multi-source crawl with path filtering (Priority: P2)
A platform admin needs to register a GitLab monorepo (e.g. mycorp/platform) as a skill hub the same way they register a GitHub repo today, and they need to point the crawler at one or more specific subdirectories (e.g. ["skills/", "agents/example/skills/"]) instead of having it walk the entire repo. A skill author also needs to import siblings of an arbitrary path from a GitLab project the same way import-github lets them today for GitHub.
Why this priority: The existing hub crawler already has a crawlGitLabRepo implementation, but the per-skill ad-hoc importer (POST /api/skills/import-github + the GithubImportPanel workspace UI) is GitHub-only — a GitLab user cannot bootstrap a skill from their company repo without copy-paste. Separately, both crawlers walk the entire repo tree on every refresh (one tree?recursive=1 call) and only filter at the SKILL.md discovery step. For a large monorepo where SKILL.md files live under a known prefix, this wastes API quota, slows refreshes, and pulls in unrelated SKILL.md files (e.g. third-party submodules vendored under vendor/). A simple include_paths: string[] filter on the hub doc — and a paths: string[] array on the per-skill import body — fixes both problems without changing any other surface.
Independent Test:
- Register a GitLab hub via
POST /api/skill-hubswith{ "type": "gitlab", "location": "mycorp/platform", "include_paths": ["skills/", "agents/observability/skills/"] }. Trigger a refresh. Inspecthub_skillscache: only SKILL.md files whose path starts with one of the two prefixes are present; everything else is ignored. - Re-register the same hub without
include_paths(or with[]) — every SKILL.md in the tree is crawled (current behavior preserved). - From the workspace editor, open the import panel and select GitLab as the source. Enter
mycorp/platformandskills/example. Confirm the sameRecord<string, string>of imported files lands in the editor as the GitHub case. - Use the import panel's "Add another path" affordance to specify two prefixes —
["skills/example", "skills/example-shared"]. Confirm both directories' files are flattened into the same imported map (with conflict detection: a duplicate filename across two paths surfaces a warning and keeps the first).
Acceptance Scenarios:
- Given a hub with
type: "gitlab"andinclude_paths: ["skills/", "agents/foo/skills/"], When the crawler runs, Then it issues the same singletree?recursive=truerequest, but theSKILL.mdcandidate list is filtered to entries whose path starts with one of the prefixes (with a trailing/enforced server-side soskillsdoes not matchskills-archive/). - Given a hub with
include_paths: []orinclude_pathsabsent, When the crawler runs, Then behavior matches today's "walk the whole repo" semantics — full backward compatibility. - Given a per-skill import request with
{ "source": "gitlab", "repo": "mycorp/platform", "paths": ["skills/example"], "credentials_ref": "GITLAB_TOKEN_FOO" }, When the API resolves it, Then the request hits${GITLAB_API_URL}/projects/mycorp%2Fplatform/repository/tree?recursive=true&per_page=100, the response is filtered topaths[0]'s prefix (excludingSKILL.mditself, matching today's GitHub behavior), and each blob is fetched viarepository/files/<encoded>/raw?ref=HEAD. - Given a per-skill import request with
paths: ["skills/a", "skills/b"], When the API resolves it, Then files from both prefixes are merged into onefiles: Record<string, string>map; if two prefixes contain the same relative filename, the response includesconflicts: [{ name, kept_from, dropped_from }]and the first prefix's content wins. - Given an admin uses the workspace editor's import panel, When they switch the source toggle from GitHub to GitLab, Then the placeholder text updates (
mycorp/platforminstead ofanthropics/skills), the credentials hint updates (GITLAB_TOKENinstead ofGITHUB_TOKEN), and the request body setssource: "gitlab". - Given a hub registration form, When the admin pastes a GitLab subgroup URL like
https://gitlab.com/mycorp/devops/platform, Then the location is normalized tomycorp/devops/platform(preserving subgroup nesting — unlike GitHub's flatowner/repo, GitLab supports arbitrary group nesting; the existing two-segment normalization is a bug for this case and MUST be widened to keep every path segment).
Edge Cases
- Mid-flight migration: A user has a previous CAIPE install that used the
commands/layout. They re-run install (without--upgrade). Behavior: the new install lays down theSKILL.mdfiles alongside the legacy.mdcommands. The--upgradepath is the only way to clean the legacy artifacts. Document this clearly in the success-card output. - GitLab subgroup nesting: GitLab projects can live arbitrarily deep (
group/subgroup/sub-subgroup/project). The existingPOST /api/skill-hubsURL normalizer truncates to two segments, which silently corrupts subgroup hubs. The fix MUST detect agitlab.comURL (or the configuredGITLAB_API_URLhost) and keep every path segment after the host, not just the first two. include_pathsmatches zeroSKILL.mdfiles: The crawl succeeds but returns an empty list. The hub'slast_success_atis set;last_failure_messagestays null; the admin sees0 skillsin the hub list with a tooltip ("0 SKILL.md files matched the configuredinclude_paths"). This is not an error — empty is a valid configured state.include_pathsprefix without trailing slash: The server normalizesskills→skills/on write soskillsdoes not accidentally matchskills-archive/SKILL.md. The original (un-normalized) input is shown back to the admin in the UI for clarity.- GitLab token absent: Public GitLab projects work without a token. The crawler MUST attempt unauthenticated access first and only error out with
authentication requiredif the response is 401 or 403 (the GitLab API returns404 Not Foundfor unauthenticated reads of private projects, which is a misleading error — surface a friendly message). - Per-skill import with conflicting paths: When the user provides multiple
paths[]and two of them contain the same relative filename, the API returns the merged map with first-wins semantics plus aconflicts: []array so the editor can surface a "skipped 2 duplicate files" toast. No data loss, no silent overwrite. - GitLab
include_pathsagainst a path that overlaps a nested SKILL.md owner: Same nested-skill protection as today (belongsToNestedSkillinhub-crawl.ts) — once a SKILL.md is found at a deeper level, files belonging to that deeper skill are not duplicated into the parent'sancillary_files. This invariant MUST hold across both filtered and unfiltered crawls. - Rate-limited or unauthorized catalog fetch: Existing 401 / 429 error handling in the install script and helpers must keep working unchanged — the overhaul touches layout, not transport.
- A manifest references a file that no longer exists on disk: Uninstall reports it as "already removed", proceeds, and rewrites the manifest without that entry (existing behavior; just needs to handle the new array shape).
- Project install with no
.gitignore: Success card still prints the.gitignorereminder and a sample snippet covering.caipe/,.claude/,.agents/— but the script does not modify.gitignoreitself. - Helper allowed-tools mismatch: If the user has Claude Code running in a mode that does not honor
allowed-toolsfrontmatter (e.g. headless CI), the helpers still function — they will just prompt for permission. This is not a regression from today. - Agent picker selects an agent the user does not actually have installed: Install still writes both universal paths; the user is unaffected. The launch guide in the UI may suggest a CLI command they cannot run, but the bytes on disk are the same as any other agent's install.
Requirements
Functional Requirements
-
FR-001: The
/api/skills/install.sh?agent=<id>endpoint MUST emit a script that writes every selected skill to two target paths per scope —~/.claude/skills/<name>/SKILL.mdand~/.agents/skills/<name>/SKILL.mdforscope=user, or.claude/skills/<name>/SKILL.mdand.agents/skills/<name>/SKILL.mdforscope=project. -
FR-002: The agent registry (
AGENTSinui/src/app/api/skills/live-skills/agents.ts) MUST contain exactly five entries:claude,cursor,codex,gemini,opencode. Continue and Spec Kit MUST be removed. -
FR-003: Every entry in the registry MUST share the same
installPaths(the two universal paths per scope, above). The agent'sidMUST only affect the launch guide and theargRefsubstitution token ($ARGUMENTSfor Claude/Cursor/opencode,$1for Codex/Gemini — see FR-009). -
FR-004: The
AgentSpecinterface MUST drop these fields:defaultLayout,format,ext,isFragment,skillsPaths. TheinstallPathsfield's value type MUST change fromPartial<Record<AgentScope, AgentLayout, string>>(a single path per layout/scope) toPartial<Record<AgentScope, readonly string[]>>(an array of universal paths per scope). -
FR-005:
renderForAgentMUST always emit a singleSKILL.mdbody whose frontmatter has exactlyname:anddescription:keys (plusdisable-model-invocationandallowed-toolsfor the two CAIPE helpers — see FR-008). All per-format branches (markdown-plain, gemini-toml, continue-json-fragment) MUST be removed. -
FR-006: The
RenderResultshape returned by/api/skills/live-skillsand/api/skills/update-skillsMUST drop these fields:file_extension,format,is_fragment,layout,layout_requested,layout_fallback,layouts_available. Theinstall_pathsfield MUST be aPartial<Record<AgentScope, readonly string[]>>. The convenienceinstall_pathfield MUST be the first entry in the resolved scope's array (for display purposes only). -
FR-007: The
/api/skills/install.shroute MUST drop thelayout=query parameter entirely. Any incominglayout=...MUST be silently ignored (not 400'd) so existing one-liners users have copy-pasted continue to work. -
FR-008: The two CAIPE helper templates (
charts/ai-platform-engineering/data/skills/live-skills.md,charts/ai-platform-engineering/data/skills/update-skills.md) MUST be updated so their frontmatter contains:name: skills/name: update-skillsdescription: <existing>disable-model-invocation: true(so the model never auto-invokes them; only an explicit/skillsor/update-skillsfrom the user does)allowed-tools: [Bash(uv run ~/.config/caipe/caipe-skills.py*), Bash(python3 ~/.config/caipe/caipe-skills.py*)]so neither invocation triggers a permission prompt.
-
FR-009: The install script MUST substitute
$ARGUMENTS→$1in every emittedSKILL.mdbody whenagent=codexoragent=gemini(since their slash-command runtime uses positional$1rather than$ARGUMENTS). All other agents (claude,cursor,opencode) keep$ARGUMENTSas-is. -
FR-010: The portion of
install.shthat mutates~/.claude/settings.jsonMUST stop adding the twoBash(uv run ~/.config/caipe/caipe-skills.py*)andBash(python3 ~/.config/caipe/caipe-skills.py*)entries topermissions.allow. The SessionStart hook patch (which writes the~/.config/caipe/caipe-catalog.shhook reference intohooks.SessionStart) MUST remain untouched. -
FR-011: The
--upgradelegacy-cleanup pass ininstall.shMUST be extended to remove leftover commands-layout artifacts for all five agents:- Claude:
~/.claude/commands/<name>.md - Cursor:
~/.cursor/commands/<name>.md - Codex:
~/.codex/prompts/<name>.md - Gemini:
~/.gemini/commands/<name>.toml - opencode:
~/.config/opencode/command/<name>.md
And the corresponding project-scope paths under
.claude/commands/,.cursor/commands/,.codex/prompts/,.gemini/commands/,.opencode/command/. The cleanup MUST also remove the two specific allowlist entries from~/.claude/settings.json(per FR-010) if they were added by a prior install. - Claude:
-
FR-012: The manifest entry shape (
~/.config/caipe/installed.jsonand./.caipe/installed.json) MUST change from{ "name": "...", "path": "..." }to{ "name": "...", "paths": ["...", "..."] }.buildUninstallScriptMUST handle both shapes during reads (treating a legacypathfield as a one-elementpathsarray) and only emit the new shape on writes. -
FR-013: When invoked without an explicit
scope=parameter, the uninstall mode (mode=uninstall) MUST walk both manifests in deterministic order — user manifest (~/.config/caipe/installed.json) first, then project manifest (./.caipe/installed.json) — each with its own independent y/N/a/q prompt loop. An "apply to all" answer in one manifest's loop MUST NOT carry across to the next. -
FR-014: After removing each skill's files, the script MUST
rmdirthe empty parent skill directory (<root>/<name>/) in each tree (i.e. both~/.claude/skills/<name>/and~/.agents/skills/<name>/). It MUST NOT touch the parentskills/root directory itself, nor anything outside the manifest's recorded paths. -
FR-015: The
TrySkillsGatewayUI panel MUST drop the "Skills layout" toggle entirely. The default (and only visible) install mode is "Bulk install (default)". Project-scope MUST be demoted to an "Advanced" disclosure (collapsed by default). The path preview block MUST display all target paths for the chosen scope (a vertical list of two paths), not just one. When project scope is selected, the preview MUST include a.gitignorereminder for.caipe/,.claude/,.agents/. -
FR-016: The per-skill ad-hoc importer MUST be reachable as
POST /api/skills/importand MUST accept{ source: "github" | "gitlab", repo: string, paths: string[], credentials_ref?: string }. The legacy routePOST /api/skills/import-githubMUST keep working (proxy to/api/skills/importwithsource: "github"injected) so any out-of-tree caller does not break; mark the legacy route as deprecated in its source docstring. The legacy single-path: stringbody MUST also be accepted as a one-elementpaths: [string]array. -
FR-017: When
source: "gitlab", the importer MUST resolve the project via${process.env.GITLAB_API_URL || "https://gitlab.com/api/v4"}/projects/<encoded-repo>/repository/tree?recursive=true&per_page=100, filter the tree to entries whosepathstarts with one of the request'spaths[i](with each prefix normalized to end in/), exclude any path ending in/SKILL.md(mirroring the GitHub branch's behavior), and fetch each blob viarepository/files/<encoded-path>/raw?ref=HEAD. Token resolution MUST go throughvalidateCredentialsRefagainst env varsGITLAB_TOKEN(default) or whatevercredentials_refresolves to; the token is sent asPRIVATE-TOKEN: <value>percrawlGitLabRepoprecedent. -
FR-018: When the importer's
paths[]has length > 1, the response MUST merge files into one map with first-wins conflict resolution and MUST include a top-levelconflicts: Array<{ name, kept_from, dropped_from }>field listing every dropped duplicate. An emptyconflicts: []MUST be returned when there are none (so callers can distinguish "no conflicts" from "field missing"). -
FR-019:
GithubImportPanel.tsxMUST be renamed toRepoImportPanel.tsx(or replaced by an equivalent component) that exposes a source toggle (GitHub / GitLab) above the existingrepo+pathinputs. The UI MUST allow adding additional path prefixes (a small "+ Add another path" affordance, capped at 5 prefixes per import). Placeholder text and the credentials-hint label MUST switch with the source toggle (GitHub:anthropics/skills,GITHUB_TOKEN; GitLab:mycorp/platform,GITLAB_TOKEN). The component MUST POST to/api/skills/importwith the new body shape. -
FR-020: The
SkillHubDocMongoDB schema (skill_hubscollection) MUST gain an optionalinclude_paths: string[]field.POST /api/skill-hubsandPATCH /api/skill-hubs/[id]MUST accept it; values MUST be normalized server-side (trim, drop empties, dedupe, append a trailing/to each entry, cap at 20 entries) before persistence. Absent or emptyinclude_pathsMUST mean "crawl the whole repo" (today's behavior). The validator MUST reject any prefix containing.., leading/, or characters outside[A-Za-z0-9._/\-]. -
FR-021:
crawlGitHubRepoandcrawlGitLabRepo(inui/src/lib/hub-crawl.ts) MUST accept an optionalincludePaths?: readonly string[]parameter. When non-empty, the SKILL.md candidate list (skillMdPaths) MUST be filtered to entries whose path starts with one of the prefixes (after the same trailing-slash normalization as FR-020). The ancillary-file collection step (tryAcceptAncillaryloop) is unchanged — siblings of an accepted SKILL.md are still gathered relative to its own directory, regardless ofincludePaths. Both crawler functions MUST forwardincludePathsfrom_crawlAndCacheso the value flows throughgetHubSkills. -
FR-022: The
POST /api/skill-hubsURL normalizer MUST be widened to preserve every path segment after the host forgitlab.comand the configuredGITLAB_API_URLhost. The existing two-segment truncation MUST remain only forgithub.com(which is flatowner/repo). A normalized GitLablocationMUST be the full nested path (e.g.mycorp/devops/platform) so subgroup hubs work end-to-end. The PATCH route's normalizer inui/src/app/api/skill-hubs/[id]/route.tsMUST be updated in lockstep.
Key Entities
- AgentSpec: A registry entry describing one supported coding agent. After overhaul:
id,label,installPaths: Partial<Record<AgentScope, readonly string[]>>,argRef: '$ARGUMENTS' | '$1',launchGuide: string, optionaldocsUrl: string. Five entries total. - RenderResult: The JSON returned by
/api/skills/live-skillsand/api/skills/update-skills. After overhaul:name,description,body,install_paths(theRecordabove),install_path(first entry in resolved scope, for display),agent_id,scope. All format/layout fields removed. - Manifest entry: One record in
~/.config/caipe/installed.json(or./.caipe/installed.json). After overhaul:{ "name": string, "paths": string[] }. Legacy{ "path": string }reads handled transparently. - SkillHubDoc (extended): The
skill_hubsMongoDB document. New optional fieldinclude_paths: string[](normalized to trailing-slash prefixes; empty/absent = crawl whole repo). All other fields unchanged. - ImportRequest (new): Body shape for
POST /api/skills/import:{ source: "github" | "gitlab", repo: string, paths: string[], credentials_ref?: string }. LegacyPOST /api/skills/import-githubproxies to this withsource: "github"injected and accepts the legacy single-pathshape. - ImportResponse (new):
{ files: Record<string, string>, count: number, conflicts: Array<{ name: string, kept_from: string, dropped_from: string }> }.
Success Criteria
Measurable Outcomes
- SC-001: A user who installs once at
scope=usercan launch any of the five supported agents and have CAIPE skills appear in their slash-command autocomplete with zero further configuration. Verified by inspecting that~/.claude/skills/,~/.cursor/skills/,~/.agents/skills/(read by Codex, Gemini, opencode), and the universal~/.agents/skills/mirror all contain the catalog after one run. - SC-002: The
agents.tssource file shrinks from 583 lines to under 350 lines, and the agent registry is reduced from 7 entries to 5. TheRenderResultshape exposed by/api/skills/live-skillshas 7 fields removed (file_extension,format,is_fragment,layout,layout_requested,layout_fallback,layouts_available). - SC-003: Re-running
install.shwith a differentagent=value writes zero new bytes (every emitted file is byte-identical to the prior run for the same skill set and scope). Verified by SHA-256 checksums in a CI smoke test. - SC-004: The
--upgradelegacy cleanup pass removes commands-layout artifacts for all five agents (10 paths per scope, 20 total per skill across both scopes). Verified by an integration test that seeds each legacy path, runs--upgrade, and asserts every legacy path is gone. - SC-005: After install, the user's
~/.claude/settings.jsondoes NOT containBash(uv run ~/.config/caipe/caipe-skills.py*)orBash(python3 ~/.config/caipe/caipe-skills.py*)underpermissions.allow. The SessionStart hook entry underhooksIS still present. - SC-006: Running
/skillsor/update-skillsinside Claude Code does NOT trigger a permission prompt for the Python catalog invocation, because the helpers'allowed-toolsfrontmatter pre-approves bothuv runandpython3forms. - SC-007: Uninstall without
scope=walks both manifests; the per-manifest y/N/a/q prompt loops are independent (proven by an integration test that answersain the first loop and verifies the second loop still prompts per item). - SC-008: The
TrySkillsGatewayUI ships zero "layout" controls, its path preview shows two paths for default (user) scope, and project scope is hidden behind an Advanced disclosure that includes the.gitignorereminder. - SC-009: The full UI Jest suite (
make caipe-ui-tests) passes after rewriting the four affected test files (agents.test.ts,route.test.ts,route.uninstall.test.ts,route.uninstall.smoke.test.ts, plusTrySkillsGateway.uninstall.test.tsx). - SC-010: A platform admin can register a GitLab subgroup hub (e.g.
mycorp/devops/platform) via either the JSON API or the admin UI and the location is persisted with every path segment intact (no two-segment truncation). Verified by a route-level test againstPOST /api/skill-hubsandPATCH /api/skill-hubs/[id]with subgroup URLs. - SC-011: A hub configured with
include_paths: ["skills/", "agents/foo/skills/"]against a 1000-file monorepo crawls in the same wall-clock time as before but caches only the SKILL.md files (and their ancillaries) under the configured prefixes. Verified by an integration test that seeds a fixture tree with SKILL.md files inside and outside the prefixes and assertshub_skillscontents after_crawlAndCache. - SC-012: The workspace import panel can pull files from both GitHub and GitLab using the same UI surface, supports up to 5 path prefixes per import, and surfaces filename conflicts non-destructively. Verified by a component test that toggles source, adds a second path, and asserts the request body shape and the
conflicts: []round-trip.