doc/plans/2026-05-26-skills-cli-catalog-contract.md
Status: Phase A engineering contract Date: 2026-05-26 Source plan: approved Paperclip skills CLI and catalog plan
This document freezes the first implementation contract for the paperclipai skills
command group and the app-shipped skills catalog. It is intentionally a build
contract, not a full product spec.
paperclipai skills manages Paperclip company skills. It does not manage
local adapter homes directly.company_skills record.skills/ remains reserved for Paperclip runtime and operational skills.packages/skills-catalog, not root
skills/.adapterConfig.paperclipSkillSync.desiredSkills for the first release. Do not
add a normalized agent_skills table unless later implementation evidence
requires it.company_skills, owned by one company.@paperclipai/skills-catalog.id, canonical key, or unique slug.id,
canonical key, or unique slug.AgentSkillSnapshot for desired,
installed, missing, stale, external, required, or unsupported skills.All skills commands use the existing client command stack:
--data-dir, --config, --context, --profile,
--api-base, --api-key, and --json.-C, --company-id <id> and otherwise use
PAPERCLIP_COMPANY_ID or the active context profile.--json prints pretty JSON and no decorative labels.0. Validation, API, or conflict errors exit 1.API error <status>: <message> formatting.--yes.These commands are Phase B and must work over existing APIs.
| Command | Behavior | JSON output |
|---|---|---|
skills list | Lists company skills from GET /api/companies/:companyId/skills. Human rows include id, key, slug, name, source, trust, compatibility, and attachedAgents. | CompanySkillListItem[] |
skills show <skill-ref> | Resolves id, key, or unique slug, then reads detail. Ambiguous slugs are conflicts. | CompanySkillDetail |
skills file <skill-ref> [--path <path>] | Resolves the skill, reads a file with default SKILL.md, and prints raw file content in human mode. This command must remain pipeable. | CompanySkillFileDetail |
skills import <source> | Calls existing import API. Source may be a local path, GitHub URL, skills.sh URL or command, owner/repo, owner/repo/skill, or URL-like source already accepted by the server. | CompanySkillImportResult |
| `skills create --name <name> [--slug <slug>] [--description <text>] [--body-file <path | ->]` | Creates a managed local company skill. If --body-file is omitted, the server default body is used. - reads markdown from stdin. |
skills scan-projects [--project-id <id>...] [--workspace-id <id>...] | Calls project scan. Repeated flags become arrays. With neither flag, scan all accessible project workspaces. | CompanySkillProjectScanResult |
skills check [skill-ref] | Reads update status for one skill, or for every listed company skill when no ref is provided. Unsupported statuses are shown, not hidden. | CompanySkillCheckRow[] |
skills update <skill-ref> | Installs the update for one skill through the existing install-update API. | CompanySkillUpdateRow |
skills update --all | Checks all skills, installs only those with hasUpdate=true, and reports skipped unsupported or current skills. | CompanySkillUpdateRow[] |
skills remove <skill-ref> [--yes] | Deletes one company skill after confirmation. | CompanySkill |
CompanySkillCheckRow is a CLI-side shape:
interface CompanySkillCheckRow {
skill: Pick<CompanySkillListItem, "id" | "key" | "slug" | "name">;
status: CompanySkillUpdateStatus;
}
CompanySkillUpdateRow is a CLI-side shape:
interface CompanySkillUpdateRow {
skillRef: string;
action: "updated" | "skipped" | "failed";
skill?: CompanySkill;
status?: CompanySkillUpdateStatus;
reason?: string;
}
These commands are Phase B and use existing agent skill APIs.
| Command | Behavior | JSON output |
|---|---|---|
skills agent list <agent-ref> | Resolves the agent using existing agent reference behavior, then prints the adapter AgentSkillSnapshot. Human rows include key, runtimeName, desired, managed, required, state, origin, and detail. | AgentSkillSnapshot |
skills agent sync <agent-ref> --skill <skill-ref>... | Replaces the agent's non-required desired skill set with the supplied refs and triggers adapter sync. Required Paperclip skills remain enforced by the server. | AgentSkillSnapshot |
skills agent clear <agent-ref> [--yes] | Clears non-required desired skills by sending an empty desired list, then returns the adapter snapshot. | AgentSkillSnapshot |
The word sync is deliberate: it is a desired-state replacement, not an append.
An additive command can be added later if operators need it.
These commands are Phase E and depend on the catalog APIs from Phase D.
| Command | Behavior | JSON output |
|---|---|---|
| `skills browse [--kind bundled | optional] [--category <slug>] [--query <text>]` | Lists app-shipped catalog skills. Human rows include id, key, kind, category, slug, name, trust, and recommendedForRoles. |
| `skills search <query> [--kind bundled | optional] [--category <slug>]` | Alias for catalog browse with query. |
skills inspect <catalog-ref> | Shows app-shipped catalog detail and file inventory. Does not mutate company state. | CatalogSkillDetail |
skills install <catalog-ref> [--as <slug>] [--force] | Installs a catalog skill into a company library. --as overrides the company skill slug. --force may replace a same-key catalog skill but must not bypass hard validation or dangerous security findings. | CompanySkillInstallCatalogResult |
Catalog commands are for the app-shipped Paperclip catalog only. External GitHub,
skills.sh, local path, and URL installs remain under skills import <source> in
the first release.
Add a workspace package:
packages/skills-catalog/
package.json
tsconfig.json
src/
index.ts
types.ts
catalog/
bundled/
<category>/
<slug>/
SKILL.md
references/
scripts/
assets/
optional/
<category>/
<slug>/
SKILL.md
references/
scripts/
assets/
generated/
catalog.json
scripts/
build-catalog-manifest.ts
validate-catalog.ts
Package name: @paperclipai/skills-catalog.
The package exports:
catalogManifestcatalogSkillsresolveCatalogSkillRef(ref)getCatalogSkill(id)Server and CLI code must import the generated manifest. They must not crawl arbitrary repository paths at request time.
The generated artifact is packages/skills-catalog/generated/catalog.json.
It is checked in and regenerated by the package build or validation script.
interface CatalogManifest {
schemaVersion: 1;
packageName: "@paperclipai/skills-catalog";
packageVersion: string;
generatedAt: string;
skills: CatalogSkill[];
}
interface CatalogSkill {
id: string;
key: string;
kind: "bundled" | "optional";
category: string;
slug: string;
name: string;
description: string;
path: string;
entrypoint: "SKILL.md";
trustLevel: "markdown_only" | "assets" | "scripts_executables";
compatibility: "compatible" | "unknown" | "invalid";
defaultInstall: boolean;
recommendedForRoles: string[];
requires: string[];
tags: string[];
files: CatalogSkillFile[];
contentHash: string;
}
interface CatalogSkillFile {
path: string;
kind: "skill" | "markdown" | "reference" | "script" | "asset" | "other";
sizeBytes: number;
sha256: string;
}
id is path-safe:
paperclipai:<kind>:<category>:<slug>
key is the canonical company skill key installed into company_skills:
paperclipai/<kind>/<category>/<slug>
Example:
{
"id": "paperclipai:bundled:software-development:github-pr-workflow",
"key": "paperclipai/bundled/software-development/github-pr-workflow",
"kind": "bundled",
"category": "software-development",
"slug": "github-pr-workflow",
"name": "github-pr-workflow",
"description": "Prepare pull requests, review responses, and verification notes.",
"path": "catalog/bundled/software-development/github-pr-workflow",
"entrypoint": "SKILL.md",
"trustLevel": "markdown_only",
"compatibility": "compatible",
"defaultInstall": false,
"recommendedForRoles": ["engineer"],
"requires": [],
"tags": ["github", "pull-requests"],
"files": [
{
"path": "SKILL.md",
"kind": "skill",
"sizeBytes": 1200,
"sha256": "..."
}
],
"contentHash": "sha256:..."
}
Each catalog SKILL.md must include:
---
name: github-pr-workflow
description: Prepare pull requests, review responses, and verification notes.
key: paperclipai/bundled/software-development/github-pr-workflow
recommendedForRoles:
- engineer
tags:
- github
- pull-requests
---
Optional frontmatter:
slugdefaultInstallrequiresmetadataThe manifest generator owns kind, category, path, files,
trustLevel, compatibility, and contentHash.
Validation must fail when:
catalog/bundled/<category>/<slug> or
catalog/optional/<category>/<slug>.SKILL.md is missing.category or slug is not a lowercase URL slug.name or description frontmatter is missing or empty.key, when present, does not equal the generated key.id, key, or slug... segments, broken symlinks, or
files outside the skill directory.compatible cannot be parsed as Agent Skills markdown.generated/catalog.json.Trust level is derived from inventory:
scripts_executables when any file is classified as script.assets when any file is classified as asset or other and no script is
present.markdown_only when all files are markdown, references, or SKILL.md.Validation must report all discovered catalog errors when practical, not just the first one.
Phase D adds read APIs and one company install API.
GET /api/skills/catalog
GET /api/skills/catalog/:catalogId
GET /api/skills/catalog/:catalogId/files?path=SKILL.md
POST /api/companies/:companyId/skills/install-catalog
GET /api/skills/catalog accepts:
kind=bundled|optionalcategory=<slug>q=<text>catalogId is the path-safe manifest id. The server should also support
resolution by key or unique slug where the ref is carried in a query or body,
but route parameters use id to avoid slash handling ambiguity.
Install request:
interface CompanySkillInstallCatalogRequest {
catalogSkillId: string;
slug?: string | null;
force?: boolean;
}
Install result:
interface CompanySkillInstallCatalogResult {
action: "created" | "updated" | "unchanged";
skill: CompanySkill;
catalogSkill: CatalogSkill;
warnings: string[];
}
Install behavior:
sourceType="catalog".key as the company skill canonical key.slug unless slug is provided.catalogIdcatalogKeycatalogKindcatalogCategorycatalogPathpackageNamepackageVersionoriginHashoriginVersionuserModifiedAtupdateHoldReason409 for duplicate slug/key conflicts that cannot be resolved safely.422 for invalid, incompatible, or hard-blocked catalog entries.force may replace a same-key catalog-managed skill. It must not bypass
company boundaries, permission checks, hard validation, or hard security
findings.Use existing HTTP semantics:
400: invalid CLI arguments, invalid query/body shape, or malformed refs.401: missing or invalid auth.403: authenticated principal lacks access or mutation permission.404: skill, catalog entry, agent, file, company, or source not found.409: ambiguous slug, duplicate key/slug, update conflict, or unsafe overwrite.422: semantic violation such as invalid skill content or unsupported source.500: unexpected server failure.CLI messages should name the next useful correction, for example:
Skill slug "review" is ambiguous. Use an id or key.Company ID is required. Pass --company-id, set PAPERCLIP_COMPANY_ID, or set a context profile.Catalog skill contains executable scripts and cannot be force-installed until security review semantics allow it.Phase A is complete when this contract is available in the repo and the issue thread links it.
Phase B, CLI MVP:
paperclipai skills --help exposes the Phase B command group.doc/CLI.md documents company install vs agent desired sync vs runtime sync.Phase C, catalog package:
packages/skills-catalog is a workspace package.generated/catalog.json.skills/ is not expanded with the app-shipped catalog.Phase D, catalog APIs:
Phase E, catalog CLI:
skills import.Phase F, update/reset/audit:
Phase G, adapter truth model:
unsupported, persistent, or
ephemeral.Phase H, UI:
Phase I, initial skill content:
Phase J, QA and docs:
doc/CLI.md, doc/DEVELOPING.md, and skill workflow docs match shipped
behavior.agent_skills table in the first release.