From 8be20ae20bd3873030bb935c777db991429a21e0 Mon Sep 17 00:00:00 2001 From: Daniele Martinoli Date: Fri, 26 Jun 2026 12:08:41 +0200 Subject: [PATCH 01/21] updated skill pack repos to use agentic-collections-skils Signed-off-by: Daniele Martinoli --- marketplace/rh-agentic-collection.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/marketplace/rh-agentic-collection.yml b/marketplace/rh-agentic-collection.yml index 50863e8..4f85211 100644 --- a/marketplace/rh-agentic-collection.yml +++ b/marketplace/rh-agentic-collection.yml @@ -1,13 +1,13 @@ name: "redhat-agentic-collections" description: "Validated Agentic Collections from Red Hat Ecosystem Engineering" -url: "https://github.com/RHEcosystemAppEng/agentic-collections" +url: "https://github.com/RHEcosystemAppEng/agentic-collections-skills" version: "0.1.0" modules: - name: "ocp-admin" description: "Automation capabilities for OpenShift Container Platform cluster management, workload orchestration, security policies, and operational tasks." version: "0.1.0" - repository: "https://github.com/RHEcosystemAppEng/agentic-collections" + repository: "https://github.com/RHEcosystemAppEng/agentic-collections-skills" path: "ocp-admin" tags: - "red-hat" @@ -25,7 +25,7 @@ modules: - name: "rh-developer" description: "Plugins for building and deploying applications on Red Hat platforms." version: "0.1.0" - repository: "https://github.com/RHEcosystemAppEng/agentic-collections" + repository: "https://github.com/RHEcosystemAppEng/agentic-collections-skills" path: "rh-developer" tags: - "developer" @@ -40,7 +40,7 @@ modules: - name: "rh-sre" description: "Site reliability engineering tools and automation for managing Red Hat platforms and infrastructure." version: "0.1.0" - repository: "https://github.com/RHEcosystemAppEng/agentic-collections" + repository: "https://github.com/RHEcosystemAppEng/agentic-collections-skills" path: "rh-sre" tags: - "sre" @@ -52,7 +52,7 @@ modules: - name: "rh-ai-engineer" description: "Red Hat OpenShift AI Engineering Agentic Collection" version: "0.1.0" - repository: "https://github.com/RHEcosystemAppEng/agentic-collections" + repository: "https://github.com/RHEcosystemAppEng/agentic-collections-skills" path: "rh-ai-engineer" tags: - "ai-engineer" @@ -68,7 +68,7 @@ modules: - name: "rh-virt" description: "Virtual machine management and automation for OpenShift Virtualization and KubeVirt workloads." version: "0.1.0" - repository: "https://github.com/RHEcosystemAppEng/agentic-collections" + repository: "https://github.com/RHEcosystemAppEng/agentic-collections-skills" path: "rh-virt" tags: - "red-hat" @@ -80,7 +80,7 @@ modules: - name: "rh-basic" description: "Essential Red Hat skills for IT professionals: CVE explanation, diagnostics, product lifecycle, RHEL patch planning, and support severity guidance." version: "0.1.0" - repository: "https://github.com/RHEcosystemAppEng/agentic-collections" + repository: "https://github.com/RHEcosystemAppEng/agentic-collections-skills" path: "rh-basic" tags: - "red-hat" @@ -94,7 +94,7 @@ modules: - name: "rh-automation" description: "Ansible Automation Platform governance, execution safety, and forensic troubleshooting tools for Red Hat automation engineers." version: "0.1.0" - repository: "https://github.com/RHEcosystemAppEng/agentic-collections" + repository: "https://github.com/RHEcosystemAppEng/agentic-collections-skills" path: "rh-automation" tags: - "ansible" From 0ff6a41647b470db3351a1075163239bb985c2db Mon Sep 17 00:00:00 2001 From: Daniele Martinoli Date: Fri, 26 Jun 2026 13:44:45 +0200 Subject: [PATCH 02/21] removed catalog schema Signed-off-by: Daniele Martinoli --- catalog/schema.yaml | 224 -------------------------------------------- 1 file changed, 224 deletions(-) delete mode 100644 catalog/schema.yaml diff --git a/catalog/schema.yaml b/catalog/schema.yaml deleted file mode 100644 index f46fa57..0000000 --- a/catalog/schema.yaml +++ /dev/null @@ -1,224 +0,0 @@ -# Pack-local collection catalog — JSON Schema (Draft 2020-12), YAML encoding. -# Field semantics and authoring: COLLECTION_SPEC.md. Instance files: /.catalog/collection.yaml -# Fragments: sibling *.md of collection.yaml; reference as #name.md (quote in YAML). Inline monitored fields -# >500 code points must use a fragment; see COLLECTION_SPEC.md. Resolved by scripts/collection_validate_lib.py. - -$schema: https://json-schema.org/draft/2020-12/schema -$id: https://github.com/RHEcosystemAppEng/agentic-collections/catalog/schema.yaml -title: Agentic collection catalog -type: object -additionalProperties: true -required: -- id -- name -- provider -- version -- categories -- personas -- marketplaces -- maturity -- description -- summary -- contents -- deploy_and_use -- mcp_section -- sample_workflows -- resources -properties: - id: - type: string - minLength: 1 - name: - type: string - minLength: 1 - provider: - type: string - minLength: 1 - version: - type: string - pattern: ^[0-9]+\.[0-9]+\.[0-9]+ - categories: - type: array - minItems: 1 - items: - type: string - minLength: 1 - personas: - type: array - minItems: 1 - items: - type: string - minLength: 1 - marketplaces: - type: array - minItems: 1 - items: - type: string - minLength: 1 - support_level: - type: string - maturity: - type: string - description: > - Publication readiness for the GitHub Pages catalog and bundled site data. GREEN collections are listed; - ORANGE are maintained in-repo but excluded from the public catalog until promoted. - enum: - - GREEN - - ORANGE - description: - type: string - minLength: 1 - summary: - type: string - minLength: 1 - contents: - type: object - additionalProperties: true - required: - - description - - skills - - orchestration_skills - - skills_decision_guide - properties: - description: - type: string - minLength: 1 - skills: - type: array - items: - $ref: '#/$defs/catalogSkill' - orchestration_skills: - type: array - items: - $ref: '#/$defs/catalogSkill' - skills_decision_guide: - type: array - items: - $ref: '#/$defs/decisionGuideRow' - deploy_and_use: - type: string - minLength: 1 - description: > - Inline markdown (≤500 code points unless externalized) or a one-line fragment ref - '#filename.md' sibling under .catalog/. - documentation_section: - type: string - minLength: 1 - description: > - How documentation is organized; inline markdown or one-line '#fragment.md' under .catalog/ - (same convention as deploy_and_use). - mcp_section: - type: string - minLength: 1 - description: > - MCP usage summary; inline or '#fragment.md' under .catalog/. - security_model: - type: string - minLength: 1 - description: > - Security posture narrative; inline or '#fragment.md' under .catalog/. - sample_workflows: - type: array - minItems: 1 - items: - $ref: '#/$defs/sampleWorkflow' - resources: - type: array - minItems: 1 - items: - $ref: '#/$defs/resource' - legal_resources: - type: object - author: - type: object - homepage: - type: string - repository: - type: string - license: - type: string - keywords: - type: array - items: - type: string - documentation_section: - description: > - Inline markdown, or a one-line sibling fragment ref `#name.md` under .catalog/ (same rules as deploy_and_use). - type: string - minLength: 1 - mcp_section: - description: > - Inline markdown, or a one-line sibling fragment ref `#name.md` under .catalog/ (same rules as deploy_and_use). - type: string - minLength: 1 - security_model: - description: > - Inline markdown, or a one-line sibling fragment ref `#name.md` under .catalog/ (same rules as deploy_and_use). - type: string - minLength: 1 -$defs: - catalogSkill: - type: object - required: - - name - - description - - summary_markdown - additionalProperties: true - properties: - name: - type: string - minLength: 1 - description: - type: string - minLength: 1 - summary_markdown: - type: string - minLength: 1 - decisionGuideRow: - type: object - required: - - user_request - - skill_to_use - - reason - additionalProperties: false - properties: - user_request: - type: string - minLength: 1 - skill_to_use: - type: string - minLength: 1 - reason: - type: string - minLength: 1 - sampleWorkflow: - type: object - required: - - name - - workflow - additionalProperties: false - properties: - name: - type: string - minLength: 1 - workflow: - type: string - minLength: 10 - resource: - type: object - required: - - title - - description - - url - additionalProperties: true - properties: - title: - type: string - minLength: 1 - description: - type: string - url: - type: string - minLength: 1 - embedded_doc: - type: string From 57d1795f35e764a590d8c53f8c41b12acb367128 Mon Sep 17 00:00:00 2001 From: Daniele Martinoli Date: Fri, 26 Jun 2026 13:45:05 +0200 Subject: [PATCH 03/21] python project files Signed-off-by: Daniele Martinoli --- pyproject.toml | 9 ++++++ uv.lock | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 pyproject.toml create mode 100644 uv.lock diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..91ae0e4 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,9 @@ +[project] +name = "agentic-collections-catalog" +version = "0.1.0" +description = "Unified catalog and marketplace for Red Hat agentic collections" +requires-python = ">=3.11" +dependencies = [ + "PyYAML>=6.0", + "markdown>=3.6", +] diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..36afa3d --- /dev/null +++ b/uv.lock @@ -0,0 +1,82 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" + +[[package]] +name = "agentic-collections-catalog" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] + +[package.metadata] +requires-dist = [ + { name = "markdown", specifier = ">=3.6" }, + { name = "pyyaml", specifier = ">=6.0" }, +] + +[[package]] +name = "markdown" +version = "3.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] From c7da1c734db91c2904592c892193a7df7be78b60 Mon Sep 17 00:00:00 2001 From: Daniele Martinoli Date: Fri, 26 Jun 2026 13:47:07 +0200 Subject: [PATCH 04/21] CI workflows Signed-off-by: Daniele Martinoli --- .github/workflows/deploy-pages.yml | 54 ++++++++++++++++++++++++++++++ .github/workflows/validate.yml | 26 ++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 .github/workflows/deploy-pages.yml create mode 100644 .github/workflows/validate.yml diff --git a/.github/workflows/deploy-pages.yml b/.github/workflows/deploy-pages.yml new file mode 100644 index 0000000..4857a6b --- /dev/null +++ b/.github/workflows/deploy-pages.yml @@ -0,0 +1,54 @@ +name: Deploy GitHub Pages + +on: + workflow_dispatch: + push: + branches: [main] + paths: + - 'marketplace/**' + - 'catalog/**' + - 'scripts/**' + - 'docs/**' + - '.github/workflows/deploy-pages.yml' + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install uv + run: curl -LsSf https://astral.sh/uv/install.sh | sh + + - name: Install dependencies and generate documentation + run: | + source $HOME/.cargo/env + make install + make generate + + - name: Setup Pages + uses: actions/configure-pages@v4 + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: 'docs' + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 0000000..fba49a4 --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,26 @@ +name: Validate Catalog + +on: + pull_request: + push: + branches: [main] + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install uv + run: curl -LsSf https://astral.sh/uv/install.sh | sh + + - name: Install dependencies and verify site generation + run: | + source $HOME/.cargo/env + make install + make test From 5d73c491391e4b7e77195c82fd54a151e7a8a27f Mon Sep 17 00:00:00 2001 From: Daniele Martinoli Date: Fri, 26 Jun 2026 13:48:30 +0200 Subject: [PATCH 05/21] Add .serena/ to .gitignore for Serena AI assistant workspace Signed-off-by: Daniele Martinoli --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 13842d5..ebf8e77 100644 --- a/.gitignore +++ b/.gitignore @@ -115,6 +115,9 @@ config.local.* # Lola package manager cache .lola/ +# Serena AI assistant workspace +.serena/ + # Generated Documentation docs/data.json reports/ From f6ad1e9c392a4f7da93bebb2e01efe8b7f403716 Mon Sep 17 00:00:00 2001 From: Daniele Martinoli Date: Fri, 26 Jun 2026 13:48:59 +0200 Subject: [PATCH 06/21] Refactor rh-agentic-collection.yml to remove commented-out federated module examples. Signed-off-by: Daniele Martinoli --- marketplace/rh-agentic-collection.yml | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/marketplace/rh-agentic-collection.yml b/marketplace/rh-agentic-collection.yml index 4f85211..c444106 100644 --- a/marketplace/rh-agentic-collection.yml +++ b/marketplace/rh-agentic-collection.yml @@ -103,26 +103,4 @@ modules: - "red-hat" - "governance" - "troubleshooting" - - "execution" - - # Federated modules — complete Lola packs hosted in external repositories. - # Add them as regular module entries with a different repository URL. - # The build identifies federated packs by comparing the repository field - # against this repo's URL. Extra field (ref) is a project extension: required - # here for CI/catalog pinning (40-character commit SHA). Lola ignores ref at - # install time until https://github.com/LobsterTrap/lola/issues/180 — installs - # use the repository default branch. Lola installs the full pack at path. - # To request federation, open a PR adding the module entry with the "federation" label. - # See FEDERATION_REQUEST_GUIDE.md. - # - # Example: - # - name: "partner-network-tools" - # description: "Network troubleshooting skills for SDN and OVN diagnostics" - # version: "1.2.0" - # repository: "https://github.com/partner-org/network-skills" - # ref: "a1b2c3d4e5f6789012345678901234567890abcd" # required: 40-character commit SHA - # path: "." # Lola pack root; all skills/ here are installed - # tags: - # - "networking" - # - "sdn" - # - "troubleshooting" + - "execution" \ No newline at end of file From a905f436faacdffdbaf14d2ceaaf7bb53dd1e47f Mon Sep 17 00:00:00 2001 From: Daniele Martinoli Date: Fri, 26 Jun 2026 14:17:49 +0200 Subject: [PATCH 07/21] initial review of document generation to support external repos Signed-off-by: Daniele Martinoli --- docs/app.js | 13 +----- scripts/build_website.py | 16 ++++--- scripts/generate_collection_pages.py | 69 +++++++++++++++++++--------- scripts/generate_mcp_data.py | 64 ++++++++++---------------- scripts/generate_pack_data.py | 43 ++++++++++------- scripts/pack_registry.py | 9 ++-- 6 files changed, 115 insertions(+), 99 deletions(-) diff --git a/docs/app.js b/docs/app.js index 3db5e71..5b28bee 100644 --- a/docs/app.js +++ b/docs/app.js @@ -306,12 +306,6 @@ function createPackCard(pack) { } badges.appendChild(badge); } - if (pack.source === 'federated') { - const fedBadge = document.createElement('span'); - fedBadge.className = 'pack-eval-badge is-federated'; - fedBadge.textContent = 'FEDERATED'; - badges.appendChild(fedBadge); - } if (badges.childNodes.length > 0) { headerRow.appendChild(badges); } @@ -319,11 +313,8 @@ function createPackCard(pack) { const meta = document.createElement('p'); meta.className = 'pack-meta'; - if (pack.source === 'federated') { - meta.textContent = `External · v${pack.plugin.version || '0.0.0'} · ${pack.ref || ''}`; - } else { - meta.textContent = `By Red Hat · v${pack.plugin.version || '0.0.0'}`; - } + const author = pack.plugin.author?.name || 'Red Hat'; + meta.textContent = `By ${author} · v${pack.plugin.version || '0.0.0'}`; div.appendChild(meta); // Description (prefer collection catalog metadata over plugin fallback text) diff --git a/scripts/build_website.py b/scripts/build_website.py index b8c8e95..17f98cb 100644 --- a/scripts/build_website.py +++ b/scripts/build_website.py @@ -60,12 +60,14 @@ def build_website(): for pack in pack_data: pack_name = pack['name'] pack['icon'] = icons['packs'].get(pack_name, '') - catalog_dir = pack.get('catalog_dir', pack_name) - cat_bundle, cat_warns = bundle_catalog_for_site(catalog_dir, root) - for w in cat_warns: - print(f"⚠️ {w}") - if cat_bundle is not None: - pack['collection'] = cat_bundle + # Skip local catalog lookup when the catalog was already loaded during cloning + if 'collection' not in pack: + catalog_dir = pack.get('catalog_dir', pack_name) + cat_bundle, cat_warns = bundle_catalog_for_site(catalog_dir, root) + for w in cat_warns: + print(f"⚠️ {w}") + if cat_bundle is not None: + pack['collection'] = cat_bundle # Keep pack cards deterministic and alphabetically ordered. pack_data = sorted(pack_data, key=lambda p: p['name']) @@ -76,7 +78,7 @@ def build_website(): # Generate MCP server data print("🔌 Parsing MCP servers...") - mcp_data = generate_mcp_data() + mcp_data = generate_mcp_data(pack_data) # Merge MCP server icons for server in mcp_data: diff --git a/scripts/generate_collection_pages.py b/scripts/generate_collection_pages.py index 10cf0a0..25f6968 100644 --- a/scripts/generate_collection_pages.py +++ b/scripts/generate_collection_pages.py @@ -15,8 +15,34 @@ from typing import Any, Dict, List import markdown +import yaml REPO_ROOT = Path(__file__).resolve().parent.parent +_MARKETPLACE_PATH = REPO_ROOT / "marketplace" / "rh-agentic-collection.yml" +def _load_marketplace_modules() -> Dict[str, Any]: + if not _MARKETPLACE_PATH.exists(): + return {} + with open(_MARKETPLACE_PATH, encoding="utf-8") as f: + data = yaml.safe_load(f) or {} + return {mod["name"]: mod for mod in (data.get("modules") or []) if "name" in mod} + + +_MARKETPLACE_MODULES: Dict[str, Any] = _load_marketplace_modules() + + +def _pack_blob_base(pack: Dict[str, Any]) -> str: + """Return the GitHub blob URL base (repo/blob/ref/path) for a pack.""" + name = pack.get("name", "") + mod = _MARKETPLACE_MODULES.get(name, {}) + repo = (pack.get("repository") or mod.get("repository") or "").rstrip("/") + if not repo: + raise ValueError(f"Pack '{name}' has no repository in pack data or marketplace") + ref = pack.get("ref") or "main" + path = (mod.get("path") if mod else None) or name + path = path.strip("/") + if path and path != ".": + return f"{repo}/blob/{ref}/{path}" + return f"{repo}/blob/{ref}" def md_to_html(text: Any) -> str: @@ -384,7 +410,7 @@ def _metric(val: Any, digits: int = 2) -> str: return "".join(parts) -def _render_skills_list(skills: List[Dict[str, Any]], pack_name: str, list_tag: str | None = None) -> str: +def _render_skills_list(skills: List[Dict[str, Any]], blob_base: str, list_tag: str | None = None) -> str: items: List[str] = [] for skill in skills: if not isinstance(skill, dict): @@ -392,7 +418,7 @@ def _render_skills_list(skills: List[Dict[str, Any]], pack_name: str, list_tag: name = str(skill.get("name", "")) desc = html.escape(str(skill.get("description", ""))) summary = md_to_html(skill.get("summary_markdown", "")) - skill_path = f"https://github.com/RHEcosystemAppEng/agentic-collections/blob/main/{pack_name}/skills/{name}/SKILL.md" + skill_path = f"{blob_base}/skills/{name}/SKILL.md" heading = f"
/{html.escape(name)} - {desc}" if list_tag: heading += f' {html.escape(list_tag)}' @@ -442,7 +468,7 @@ def _render_decision_guide(rows: List[Dict[str, Any]]) -> str: ) -def _render_resources(resources: List[Dict[str, Any]], pack_name: str) -> str: +def _render_resources(resources: List[Dict[str, Any]], blob_base: str) -> str: if not resources: return "

No resources.

" @@ -467,10 +493,7 @@ def _render_resources(resources: List[Dict[str, Any]], pack_name: str) -> str: if description: line.append(f" - {description}") if embedded: - embed_url = ( - "https://github.com/RHEcosystemAppEng/agentic-collections/blob/main/" - f"{pack_name}/{embedded.lstrip('/')}" - ) + embed_url = f"{blob_base}/{embedded.lstrip('/')}" line.append( f' [embedded doc]' @@ -534,14 +557,10 @@ def render_collection_page(pack: Dict[str, Any], mcp_data: List[Dict[str, Any]]) page_title = collection.get("name") or pack.get("plugin", {}).get("title") or pack.get("name") description = str(collection.get("description") or "").strip() subtitle = html.escape(description[:120].strip() + ("..." if len(description) > 120 else "")) - yaml_link = ( - "https://github.com/RHEcosystemAppEng/agentic-collections/blob/main/" - f"{pack['name']}/.catalog/collection.yaml" - ) - readme_link = ( - "https://github.com/RHEcosystemAppEng/agentic-collections/blob/main/" - f"{pack['name']}/README.md" - ) + blob_base = _pack_blob_base(pack) + has_catalog = bool(collection) + yaml_link = f"{blob_base}/.catalog/collection.yaml" + readme_link = f"{blob_base}/README.md" categories = collection.get("categories") or [] personas = collection.get("personas") or [] version = pack.get("plugin", {}).get("version") @@ -591,6 +610,14 @@ def render_collection_page(pack: Dict[str, Any], mcp_data: List[Dict[str, Any]]) overview_parts.append("

Security Model

") overview_parts.append(f"
{md_to_html(collection.get('security_model'))}
") + # README fallback: no catalog data but a README is available + if not overview_parts: + readme_content = pack.get("readme_content") or "" + if readme_content.strip(): + overview_parts.append(f"
{md_to_html(readme_content)}
") + else: + overview_parts.append("

No overview available.

") + # Skills tab contents = collection.get("contents") or {} skills_parts = ["

Skills

"] @@ -603,10 +630,10 @@ def render_collection_page(pack: Dict[str, Any], mcp_data: List[Dict[str, Any]]) skills = contents.get("skills") or [] if orchestration: skills_parts.append(f"

{'Orchestration Skill' if len(orchestration) == 1 else 'Orchestration Skills'}

") - skills_parts.append(_render_skills_list(orchestration, pack["name"], "Orchestration skill")) + skills_parts.append(_render_skills_list(orchestration, blob_base, "Orchestration skill")) if skills: skills_parts.append(f"

{'Basic Skills' if orchestration else 'Skills'}

") - skills_parts.append(_render_skills_list(skills, pack["name"])) + skills_parts.append(_render_skills_list(skills, blob_base)) guide = contents.get("skills_decision_guide") or [] if guide: skills_parts.append("

Skills Decision Guide

") @@ -615,7 +642,7 @@ def render_collection_page(pack: Dict[str, Any], mcp_data: List[Dict[str, Any]]) skills_parts.append("

No skills content.

") # Resources tab - resources_html = _render_resources(collection.get("resources") or [], pack["name"]) + resources_html = _render_resources(collection.get("resources") or [], blob_base) # Agents tab pack_mcp = [s for s in mcp_data if s.get("pack") == pack.get("name")] @@ -635,7 +662,7 @@ def render_collection_page(pack: Dict[str, Any], mcp_data: List[Dict[str, Any]]) legal = collection.get("legal_resources") or {} license_nav_href = str(legal.get("license_agreement_url") or "").strip() if not license_nav_href: - license_nav_href = "https://github.com/RHEcosystemAppEng/agentic-collections/blob/main/LICENSE" + license_nav_href = f"{blob_base}/LICENSE" license_nav_href_esc = html.escape(license_nav_href, quote=True) return f""" @@ -659,7 +686,7 @@ def render_collection_page(pack: Dict[str, Any], mcp_data: List[Dict[str, Any]]) ← Back to Catalog
- catalog YAML → + {f'catalog YAML →' if has_catalog else ''}
README → @@ -708,7 +735,7 @@ def generate_collection_pages(pack_data: List[Dict[str, Any]], mcp_data: List[Di generated_files = set() count = 0 for pack in pack_data: - if not pack.get("collection"): + if not pack.get("collection") and not pack.get("readme_content") and not pack.get("skills"): continue collection = pack.get("collection") or {} collection_id = ( diff --git a/scripts/generate_mcp_data.py b/scripts/generate_mcp_data.py index 45c67e6..332e126 100644 --- a/scripts/generate_mcp_data.py +++ b/scripts/generate_mcp_data.py @@ -4,13 +4,10 @@ """ import json -import sys import re from pathlib import Path from typing import Dict, List, Any -from generate_pack_data import DOCS_PACK_DIRS - MCP_FILENAME = "mcps.json" MCP_DEPRECATED = ".mcp.json" @@ -80,8 +77,7 @@ def parse_mcp_file(pack_dir: str) -> List[Dict[str, Any]]: mcp_file = pack_path / MCP_FILENAME if deprecated_path.exists(): - print(f"Error: {pack_dir}/{MCP_DEPRECATED} is deprecated; rename to {MCP_FILENAME}", file=sys.stderr) - sys.exit(1) + print(f"Warning: {pack_dir}/{MCP_DEPRECATED} is deprecated and will be ignored; rename to {MCP_FILENAME}") if not mcp_file.exists(): return [] @@ -160,59 +156,49 @@ def load_custom_mcp_data() -> Dict[str, Any]: return {} -def generate_mcp_data() -> List[Dict[str, Any]]: +def _merge_custom_data(server: Dict[str, Any], custom_data: Dict[str, Any]) -> None: + """Merge docs/mcp.json metadata into a server dict in place.""" + server_name = server['name'] + custom = custom_data.get(server_name, {}) + server['repository'] = custom.get('repository', '') + server['tools'] = custom.get('tools', []) + server['title'] = custom.get('title', server_name) + server['tier'] = custom.get('tier', 'Official') + server['owner'] = custom.get('owner', 'Red Hat') + + +def generate_mcp_data(pack_data: List[Dict[str, Any]] | None = None) -> List[Dict[str, Any]]: """ - Generate MCP server data for all agentic packs. - Merges data from mcps.json files with custom data from docs/mcp.json. + Generate MCP server data from pack_data (marketplace packs with mcp_servers_raw) + merged with custom metadata from docs/mcp.json. + + Args: + pack_data: List of pack dicts from generate_pack_data(); each may carry + ``mcp_servers_raw`` parsed during the clone phase. Returns: List of MCP server dictionaries """ mcp_servers = [] - - # Load custom data (repository URLs and tool descriptions) custom_data = load_custom_mcp_data() - for pack_dir in DOCS_PACK_DIRS: - pack_path = Path(pack_dir) - - if not pack_path.exists(): - continue - - servers = parse_mcp_file(pack_dir) - - # Merge custom data for each server + for pack in (pack_data or []): + servers = list(pack.get("mcp_servers_raw") or []) for server in servers: - server_name = server['name'] - if server_name in custom_data: - # Add custom metadata from docs/mcp.json - server['repository'] = custom_data[server_name].get('repository', '') - server['tools'] = custom_data[server_name].get('tools', []) - server['title'] = custom_data[server_name].get('title', server_name) - server['tier'] = custom_data[server_name].get('tier', 'Official') - server['owner'] = custom_data[server_name].get('owner', 'Red Hat') - else: - # No custom data available - use defaults - server['repository'] = '' - server['tools'] = [] - server['title'] = server_name - server['tier'] = 'Official' - server['owner'] = 'Red Hat' - + _merge_custom_data(server, custom_data) mcp_servers.extend(servers) - if servers: - print(f"✓ Parsed {pack_dir}: {len(servers)} MCP server(s)") + print(f"✓ {pack.get('name', '?')}: {len(servers)} MCP server(s)") return mcp_servers if __name__ == '__main__': - # Test the script + from generate_pack_data import generate_pack_data print("Parsing MCP server configurations...") print() - servers = generate_mcp_data() + servers = generate_mcp_data(generate_pack_data()) print() print(f"Found {len(servers)} MCP servers total") diff --git a/scripts/generate_pack_data.py b/scripts/generate_pack_data.py index 663e8f6..ea5e49f 100644 --- a/scripts/generate_pack_data.py +++ b/scripts/generate_pack_data.py @@ -11,6 +11,8 @@ import yaml import pack_registry +from catalog_site_bundle import bundle_catalog_for_site +from generate_mcp_data import parse_mcp_file # Union registry (marketplace ∪ plugins.json); docs site uses subset helper PACK_DIRS = pack_registry.get_union_pack_dirs() @@ -319,8 +321,7 @@ def load_federated_packs(plugin_titles: Dict[str, str] | None = None) -> List[Di """ Fetch federated modules and return them as standalone pack entries. - Each federated pack gets ``source: "federated"`` so the docs site can - badge it differently from in-tree packs. + Fetch modules listed in the marketplace and return them as standalone pack entries. """ import shutil import subprocess @@ -338,16 +339,6 @@ def load_federated_packs(plugin_titles: Dict[str, str] | None = None) -> List[Di try: for mod in modules: name = mod.get("name", "unknown") - fed_catalog_dir = f"federation/modules/{name}" - maturity = pack_registry.load_pack_maturity(fed_catalog_dir, repo_root) - if maturity != pack_registry.DOCS_MATURITY_PUBLISH: - label = maturity or "missing catalog" - print( - f" Skipping federated '{name}' for docs site: maturity {label} " - f"(only GREEN is published)" - ) - continue - repository = mod.get("repository", "") ref = mod.get("ref", "") description = mod.get("description", "") @@ -382,11 +373,23 @@ def load_federated_packs(plugin_titles: Dict[str, str] | None = None) -> List[Di license_id = detect_repo_license(clone_dest, pack_path) skills = parse_skills(str(pack_dir)) + # Read catalog from the cloned source repo (not from a local federation/ mirror) + cat_bundle, cat_warns = bundle_catalog_for_site(pack_path, clone_dest) + for w in cat_warns: + print(f" ⚠️ {w}") + + # README fallback: read content so the UI can show it when catalog is absent + readme_path = pack_dir / "README.md" + readme_content = readme_path.read_text(encoding="utf-8") if readme_path.is_file() else None + + # MCP servers: parse from cloned content; fix pack field to use module name + mcp_servers = parse_mcp_file(str(pack_dir)) + for s in mcp_servers: + s["pack"] = name + pack = { "name": name, "path": repository, - "catalog_dir": fed_catalog_dir, - "source": "federated", "repository": repository, "ref": pack_registry.normalize_federation_ref(ref)[:12], "plugin": { @@ -401,10 +404,16 @@ def load_federated_packs(plugin_titles: Dict[str, str] | None = None) -> List[Di "skills": sorted(skills, key=lambda s: s["name"]), "agents": [], "docs": [], - "has_readme": (pack_dir / "README.md").exists(), + "has_readme": readme_content is not None, + "readme_content": readme_content, + "mcp_servers_raw": mcp_servers, } + if cat_bundle is not None: + pack["collection"] = cat_bundle packs.append(pack) - print(f" ✓ Federated '{name}': {len(skills)} skill(s) from {repository}") + catalog_status = "with catalog" if cat_bundle else "README only" + mcp_status = f", {len(mcp_servers)} MCP server(s)" if mcp_servers else "" + print(f" ✓ '{name}': {len(skills)} skill(s) from {repository} ({catalog_status}{mcp_status})") finally: shutil.rmtree(tmp, ignore_errors=True) @@ -454,7 +463,7 @@ def generate_pack_data() -> List[Dict[str, Any]]: federated = load_federated_packs(plugin_titles) if federated: packs.extend(federated) - print(f"✓ Added {len(federated)} federated pack(s)") + print(f"✓ Added {len(federated)} marketplace pack(s)") return packs diff --git a/scripts/pack_registry.py b/scripts/pack_registry.py index 2157c8c..63565ce 100644 --- a/scripts/pack_registry.py +++ b/scripts/pack_registry.py @@ -92,9 +92,9 @@ def load_marketplace_module_by_path( def federation_ref_error(ref: Any) -> Optional[str]: - """Return an error message when *ref* is missing or not a 40-character commit SHA.""" + """Return an error message when *ref* is set but not a 40-character commit SHA.""" if ref is None or not str(ref).strip(): - return "ref is required (40-character commit SHA; not a branch or tag name)" + return None # absent ref → defaults to main branch value = str(ref).strip() if not FEDERATION_REF_SHA_RE.fullmatch(value): return ( @@ -104,11 +104,12 @@ def federation_ref_error(ref: Any) -> Optional[str]: def normalize_federation_ref(ref: Any) -> str: - """Return a lowercase 40-character commit SHA.""" + """Return a lowercase 40-character commit SHA, or 'main' when ref is absent.""" err = federation_ref_error(ref) if err: raise ValueError(err) - return str(ref).strip().lower() + value = str(ref).strip() if ref is not None else "" + return value.lower() if value else "main" def validate_federated_module_entry(module: Dict[str, Any]) -> List[str]: From f5c20b95152186cbbaf315c0307be39cde3e313d Mon Sep 17 00:00:00 2001 From: Daniele Martinoli Date: Fri, 26 Jun 2026 14:38:55 +0200 Subject: [PATCH 08/21] removed icons.json and replaced with new, unmanaged field in marketplace file Signed-off-by: Daniele Martinoli --- docs/app.js | 40 ++++++++++++------------- docs/icons.json | 20 ------------- docs/styles.css | 8 +++++ marketplace/rh-agentic-collection.yml | 7 +++++ scripts/build_website.py | 42 +++------------------------ scripts/generate_pack_data.py | 1 + 6 files changed, 38 insertions(+), 80 deletions(-) delete mode 100644 docs/icons.json diff --git a/docs/app.js b/docs/app.js index 5b28bee..b52f981 100644 --- a/docs/app.js +++ b/docs/app.js @@ -175,6 +175,19 @@ function formatMetric(value, digits = 2) { return typeof value === 'number' && Number.isFinite(value) ? value.toFixed(digits) : 'N/A'; } +function createIconElement(icon, className) { + const isUrl = icon.startsWith('http'); + const el = document.createElement(isUrl ? 'img' : 'span'); + if (className) el.className = className; + if (isUrl) { + el.src = icon; + el.alt = ''; + } else { + el.textContent = icon; + } + return el; +} + function createPackCard(pack) { const NS_SVG = 'http://www.w3.org/2000/svg'; const makeSvg = () => { @@ -274,10 +287,7 @@ function createPackCard(pack) { // Custom icon (if available) if (pack.icon) { - const customIcon = document.createElement('span'); - customIcon.className = 'card-icon'; - customIcon.textContent = pack.icon; - h3.appendChild(customIcon); + h3.appendChild(createIconElement(pack.icon, 'card-icon')); } const titleText = document.createElement('span'); @@ -552,11 +562,7 @@ function createMCPCard(server) { // Custom icon (if available) if (server.icon) { - const customIcon = document.createElement('span'); - customIcon.className = 'card-icon'; - customIcon.textContent = server.icon; - customIcon.style.fontSize = '1.2rem'; - h3.appendChild(customIcon); + h3.appendChild(createIconElement(server.icon, 'card-icon')); } const titleText = document.createElement('span'); @@ -1135,11 +1141,7 @@ lola install -f ${moduleName}`; // Custom icon (if available) if (server.icon) { - const customIcon = document.createElement('span'); - customIcon.className = 'item-icon'; - customIcon.textContent = server.icon; - customIcon.style.fontSize = '0.9rem'; - nameGroup.appendChild(customIcon); + nameGroup.appendChild(createIconElement(server.icon, 'item-icon')); } const nameCode = document.createElement('code'); @@ -1245,11 +1247,7 @@ function showMCPDetails(serverName, packName) { // Custom icon (if available) if (server.icon) { - const customIcon = document.createElement('span'); - customIcon.className = 'card-icon'; - customIcon.textContent = server.icon; - customIcon.style.fontSize = '1.8rem'; - h2.appendChild(customIcon); + h2.appendChild(createIconElement(server.icon, 'card-icon')); } const titleText = document.createElement('span'); @@ -2606,9 +2604,7 @@ function buildCollectionAgentsPanel(panel, pack, c) { const titleRow = document.createElement('div'); titleRow.className = 'collection-mcp-card-title'; if (server.icon) { - const ic = document.createElement('span'); - ic.textContent = server.icon; - titleRow.appendChild(ic); + titleRow.appendChild(createIconElement(server.icon, '')); } const t = document.createElement('span'); t.textContent = server.title || server.name; diff --git a/docs/icons.json b/docs/icons.json deleted file mode 100644 index 81a9f25..0000000 --- a/docs/icons.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "packs": { - "rh-sre": "🔧", - "rh-developer": "💻", - "ocp-admin": "☸️", - "rh-virt": "🖥️", - "rh-ai-engineer": "🤖", - "rh-automation": "⚙️" - }, - "mcp_servers": { - "lightspeed-mcp": "💡", - "aap-mcp-job-management": "⚙️", - "aap-mcp-inventory-management": "📋", - "openshift": "☸️", - "openshift-virtualization": "🖥️", - "podman": "🐳", - "github": "🐙", - "ai-observability": "🔬" - } -} diff --git a/docs/styles.css b/docs/styles.css index ed7be59..81b9f46 100644 --- a/docs/styles.css +++ b/docs/styles.css @@ -1312,6 +1312,14 @@ footer a:hover { font-size: 1rem; } +img.card-icon, +img.item-icon { + height: 1em; + width: auto; + vertical-align: middle; + object-fit: contain; +} + /* Per-collection catalog page (#collection/) */ .collection-page { max-width: 920px; diff --git a/marketplace/rh-agentic-collection.yml b/marketplace/rh-agentic-collection.yml index c444106..8d5a479 100644 --- a/marketplace/rh-agentic-collection.yml +++ b/marketplace/rh-agentic-collection.yml @@ -9,6 +9,7 @@ modules: version: "0.1.0" repository: "https://github.com/RHEcosystemAppEng/agentic-collections-skills" path: "ocp-admin" + icon: "☸️" tags: - "red-hat" - "openshift" @@ -27,6 +28,7 @@ modules: version: "0.1.0" repository: "https://github.com/RHEcosystemAppEng/agentic-collections-skills" path: "rh-developer" + icon: "💻" tags: - "developer" - "openshift" @@ -42,6 +44,7 @@ modules: version: "0.1.0" repository: "https://github.com/RHEcosystemAppEng/agentic-collections-skills" path: "rh-sre" + icon: "🔧" tags: - "sre" - "red-hat" @@ -54,6 +57,7 @@ modules: version: "0.1.0" repository: "https://github.com/RHEcosystemAppEng/agentic-collections-skills" path: "rh-ai-engineer" + icon: "🤖" tags: - "ai-engineer" - "openshift-ai" @@ -70,6 +74,7 @@ modules: version: "0.1.0" repository: "https://github.com/RHEcosystemAppEng/agentic-collections-skills" path: "rh-virt" + icon: "🖥️" tags: - "red-hat" - "kubevirt" @@ -82,6 +87,7 @@ modules: version: "0.1.0" repository: "https://github.com/RHEcosystemAppEng/agentic-collections-skills" path: "rh-basic" + icon: "https://static.redhat.com/libs/redhat/brand-assets/latest/corp/logo--hat-only.svg" tags: - "red-hat" - "cve" @@ -96,6 +102,7 @@ modules: version: "0.1.0" repository: "https://github.com/RHEcosystemAppEng/agentic-collections-skills" path: "rh-automation" + icon: "⚙️" tags: - "ansible" - "aap" diff --git a/scripts/build_website.py b/scripts/build_website.py index 17f98cb..03106d3 100644 --- a/scripts/build_website.py +++ b/scripts/build_website.py @@ -7,7 +7,7 @@ import sys from datetime import datetime, timezone from pathlib import Path -from typing import Dict, Any + # Import our data generators from catalog_site_bundle import bundle_catalog_for_site @@ -17,27 +17,6 @@ from generate_mcp_data import generate_mcp_data -def load_icons() -> Dict[str, Dict[str, str]]: - """ - Load icon mappings from docs/icons.json. - - Returns: - Dictionary with 'packs' and 'mcp_servers' icon mappings - """ - icons_file = Path('docs/icons.json') - - if not icons_file.exists(): - print("⚠️ Warning: docs/icons.json not found, icons will not be loaded") - return {'packs': {}, 'mcp_servers': {}} - - try: - with open(icons_file, 'r', encoding='utf-8') as f: - return json.load(f) - except Exception as e: - print(f"⚠️ Warning: Failed to load docs/icons.json: {e}") - return {'packs': {}, 'mcp_servers': {}} - - def build_website(): """ Generate the complete website data file. @@ -45,23 +24,16 @@ def build_website(): print("🔨 Building documentation website...") print() - # Load icons - print("🎨 Loading icons...") - icons = load_icons() - print() - # Generate pack data print("📦 Parsing agentic collections...") pack_data = generate_pack_data() - + root = Path(__file__).resolve().parent.parent - # Merge pack icons and optional resolved collection catalog (for Pages UI) + # Resolve collection catalog for packs that don't have it from the clone phase for pack in pack_data: - pack_name = pack['name'] - pack['icon'] = icons['packs'].get(pack_name, '') - # Skip local catalog lookup when the catalog was already loaded during cloning if 'collection' not in pack: + pack_name = pack['name'] catalog_dir = pack.get('catalog_dir', pack_name) cat_bundle, cat_warns = bundle_catalog_for_site(catalog_dir, root) for w in cat_warns: @@ -79,12 +51,6 @@ def build_website(): # Generate MCP server data print("🔌 Parsing MCP servers...") mcp_data = generate_mcp_data(pack_data) - - # Merge MCP server icons - for server in mcp_data: - server_name = server['name'] - server['icon'] = icons['mcp_servers'].get(server_name, '') - print() # Generate static collection pages (fork-compatible UX) diff --git a/scripts/generate_pack_data.py b/scripts/generate_pack_data.py index ea5e49f..2fe69ee 100644 --- a/scripts/generate_pack_data.py +++ b/scripts/generate_pack_data.py @@ -392,6 +392,7 @@ def load_federated_packs(plugin_titles: Dict[str, str] | None = None) -> List[Di "path": repository, "repository": repository, "ref": pack_registry.normalize_federation_ref(ref)[:12], + "icon": mod.get("icon", ""), "plugin": { "name": name, "title": titles.get(name, name.replace("-", " ").title()), From 0ff5f697f7badc404ca67b692a0341190bcc80be Mon Sep 17 00:00:00 2001 From: Daniele Martinoli Date: Fri, 26 Jun 2026 14:42:43 +0200 Subject: [PATCH 09/21] Update README.md to reflect removal of style-tokens.json and adjust validation instructions for docs metadata Signed-off-by: Daniele Martinoli --- docs/README.md | 6 +++--- docs/style-tokens.json | 21 --------------------- 2 files changed, 3 insertions(+), 24 deletions(-) delete mode 100644 docs/style-tokens.json diff --git a/docs/README.md b/docs/README.md index 57292f7..1612996 100644 --- a/docs/README.md +++ b/docs/README.md @@ -47,10 +47,10 @@ To manually update the site: ## Validation for Docs Metadata -When changing docs presentation metadata (`styles.css`, `app.js`, `icons.json`, `plugins.json`, `mcp.json`): +When changing docs presentation metadata (`styles.css`, `app.js`, `plugins.json`, `mcp.json`): -1. Prefer canonical tokens from `styles.css` and `style-tokens.json` over hardcoded values. -2. Ensure every in-scope pack has icon/title mappings in `icons.json` and `plugins.json`. +1. Prefer canonical tokens from `styles.css` over hardcoded values. +2. Ensure every in-scope pack has icon/title mappings in `plugins.json`. 3. Run validation checks: ```bash diff --git a/docs/style-tokens.json b/docs/style-tokens.json deleted file mode 100644 index 524517b..0000000 --- a/docs/style-tokens.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "colors": { - "primary": "var(--primary)", - "primaryDark": "var(--primary-dark)", - "primaryLight": "var(--primary-light)", - "textPrimary": "var(--text-primary)", - "textSecondary": "var(--text-secondary)", - "textMuted": "var(--text-muted)" - }, - "sizes": { - "cardIcon": "1.2rem", - "itemIcon": "1rem" - }, - "usage": { - "stylesCss": "docs/styles.css", - "appJs": "docs/app.js", - "iconsJson": "docs/icons.json", - "pluginsJson": "docs/plugins.json", - "mcpJson": "docs/mcp.json" - } -} From ee48f9a06a2f124f42ea5b8c7171de2aee62c1b1 Mon Sep 17 00:00:00 2001 From: Daniele Martinoli Date: Fri, 26 Jun 2026 14:42:55 +0200 Subject: [PATCH 10/21] First draft of open items doc Signed-off-by: Daniele Martinoli --- OPEN_ITEMS.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 OPEN_ITEMS.md diff --git a/OPEN_ITEMS.md b/OPEN_ITEMS.md new file mode 100644 index 0000000..4c64137 --- /dev/null +++ b/OPEN_ITEMS.md @@ -0,0 +1,2 @@ +## Open Items +* How to collect `icon` field from Compass registration \ No newline at end of file From 8e6b48225fab6a26ec2af434e25f179016881311 Mon Sep 17 00:00:00 2001 From: Daniele Martinoli Date: Fri, 26 Jun 2026 15:04:30 +0200 Subject: [PATCH 11/21] Remove COLLECTION_SPEC.md and update references in README.md and CLAUDE.md to reflect this change. Signed-off-by: Daniele Martinoli --- CLAUDE.md | 5 ++- COLLECTION_SPEC.md | 80 ---------------------------------------------- README.md | 3 +- docs/README.md | 8 ++--- 4 files changed, 6 insertions(+), 90 deletions(-) delete mode 100644 COLLECTION_SPEC.md diff --git a/CLAUDE.md b/CLAUDE.md index 6a7c3de..92d0386 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -25,7 +25,7 @@ agentic-collections-catalog/ │ ├── mcp.json # MCP server metadata │ └── collections/ # Generated per-pack HTML pages ├── scripts/ # Catalog build and maintenance scripts -├── COLLECTION_SPEC.md # Specification for .catalog/ structure + ├── README.md └── LICENSE ``` @@ -72,7 +72,6 @@ The `docs/` directory contains the static site served at agentskills.io. Key fil ### Collection Schema - `catalog/schema.yaml` — JSON Schema that defines the structure of `.catalog/collection.yaml` files in skills repos -- `COLLECTION_SPEC.md` — Human-readable specification with rules for catalog metadata (inline vs fragment, path conventions) ## Scripts @@ -107,4 +106,4 @@ Files like `docs/data.json`, `docs/collections/*.html`, and parts of `plugins.js ### Schema Changes -If the structure of `.catalog/collection.yaml` needs to change, update `catalog/schema.yaml` and `COLLECTION_SPEC.md` here, then coordinate with skills repos to update their `.catalog/` directories accordingly. +If the structure of `.catalog/collection.yaml` needs to change, update `catalog/schema.yaml` here, then coordinate with skills repos to update their `.catalog/` directories accordingly. diff --git a/COLLECTION_SPEC.md b/COLLECTION_SPEC.md deleted file mode 100644 index 0597879..0000000 --- a/COLLECTION_SPEC.md +++ /dev/null @@ -1,80 +0,0 @@ -# Collection catalog specification - -This repository uses a **pack-local collection catalog**: curated metadata and summaries live under **`/.catalog/`** (YAML as the source of record, JSON as a deterministic mirror for consumers that prefer it). **Golden sources** are pack `SKILL.md` files, `README.md`, `AGENTS.md`, and [`marketplace/rh-agentic-collection.yml`](marketplace/rh-agentic-collection.yml). Catalog files **describe** the collection for tooling and documentation; they are **authored** primarily via the [**create-collection**](.claude/skills/create-collection/SKILL.md) skill (assistant + maintainer + PR review) and must not overwrite READMEs or marketplace YAML. - -**Machine validation:** [`catalog/schema.yaml`](catalog/schema.yaml) (JSON Schema expressed in YAML) and [`scripts/validate_collection_compliance.py`](scripts/validate_collection_compliance.py). These checks are primarily structural and cannot fully evaluate semantic parity with golden-source docs. **Required review gate:** run an AI semantic alignment review (via [`.claude/skills/collection-compliance/SKILL.md`](.claude/skills/collection-compliance/SKILL.md)) to confirm catalog metadata/fragments remain aligned with `SKILL.md`, `README.md`, `AGENTS.md`, and marketplace module metadata. **Pack list (registry):** union of Lola marketplace `modules[].path` and keys of [`docs/plugins.json`](docs/plugins.json), limited to directories that exist on disk — see [`scripts/pack_registry.py`](scripts/pack_registry.py). **GitHub Pages / bundled catalog listing:** only packs whose **`maturity`** is **`GREEN`** are included in site generation (`docs/data.json`, collection pages, MCP bundle for docs); **`ORANGE`** means the pack remains installable from source/Lola but is not promoted on the public catalog until reviewers raise maturity. - -### `maturity` (required) - -| Value | Meaning | -|-------|---------| -| **`GREEN`** | Listed on the GitHub Pages agentic collections catalog and included in generated `docs/data.json` / static collection pages. | -| **`ORANGE`** | Collection metadata is maintained under `.catalog/` for validation and future promotion; excluded from the public catalog surface until explicitly moved to **`GREEN`**. | - -Set in **`collection.yaml`** as a plain string (`maturity: GREEN` or `maturity: ORANGE`). Enforcement: [`scripts/pack_registry.py`](scripts/pack_registry.py) (`get_docs_pack_dirs()` filters on **`GREEN`**). - -## Layout - -| Path | Purpose | -|------|---------| -| `/.catalog/collection.yaml` | Canonical catalog document (YAML) | -| `/.catalog/collection.json` | Deterministic JSON mirror of YAML (regenerate with `make catalog-mirror-json`; CI fails on drift) | -| `/.catalog/*.md` | Optional prose fragments, **siblings of** `collection.yaml`. Reference them with **`#.md`** (quoted in YAML) **on the same field** as inline text would use—for example `documentation_section: '#docs.md'` or `deploy_and_use: '#install.md'`. If inline text in `collection.yaml` for a monitored field exceeds **`CATALOG_INLINE_CHAR_LIMIT`** (500 Unicode code points; see `scripts/collection_validate_lib.py`), move prose here and set **that same field** to the ref. | - -**Multiline YAML in `collection.yaml`:** For `contents.description`, each **`summary_markdown`**, and each **`sample_workflows[].workflow`**, authors should use YAML **literal block scalars** (`field: |`) with indented lines—see step 4 of the [**create-collection**](.claude/skills/create-collection/SKILL.md) skill (reference **`rh-sre`** / **`rh-developer`** `.catalog/collection.yaml`). CI does not assert this style; it is a maintainability convention. - -### External references (`#…md` on the same field) - -- **Path rule:** refs are **siblings of** `collection.yaml` inside **`/.catalog/`**. Write **`#install.md`**, not `#.catalog/install.md` (omit the `.catalog/` segment in the string). -- **One field, two shapes:** for **`deploy_and_use`**, **`documentation_section`**, **`mcp_section`**, and **`security_model`**, the value is either **inline** markdown or a **one-line** fragment ref **`#.md`**. Do **not** use separate `documentation_section_file` / `mcp_section_file` / `security_model_file` keys. -- **Monitored inline length:** for **`summary`**, **`documentation_section`**, **`mcp_section`**, and **`security_model`**, if the value is **inline** (not a `#…md` ref) and longer than **500 Unicode code points**, move the prose to a fragment file and set **that same key** to **`#.md`** (one line only). For **`deploy_and_use`**, the same limit applies when it is **inline** markdown (not a one-line ref). -- **`deploy_and_use` (two flavors):** (1) **Inline** — markdown in YAML, ≤ **500** code points unless you externalize. (2) **File ref** — one line only: **`#.md`** next to `collection.yaml`. CI resolves the file under **`/.catalog/`**. -- **Fragment ref values:** must start with **`#`** (e.g. `#documentation_section.md`). Legacy `#.catalog/…` is accepted and normalized to a sibling path. - -### `deploy_and_use` content (install + env + MCP) - -Consumers read **`deploy_and_use`** (resolved from inline YAML or from **`#deploy_and_use.md`**) for marketplace install steps and operator setup. **CI does not yet cross-check `mcps.json` or README install parity;** reviewers enforce this section with the [**create-collection**](.claude/skills/create-collection/SKILL.md) and [**collection-compliance**](.claude/skills/collection-compliance/SKILL.md) workflows. - -**When `/mcps.json` defines one or more MCP servers**, the prose behind **`deploy_and_use`** (inline or fragment) **must** cover, in substance: - -1. **Prerequisites** — Cluster/product access, CLIs, operators, or accounts needed before install, aligned with **`README.md`** / **`AGENTS.md`**. -2. **Environment setup** — Example **`export VAR=...`** (or host env equivalent) for **every** environment variable **name** referenced in **`mcps.json`** via **`${VAR}`** in `command`, `args`, or `env`. Variable **names** must match **`mcps.json`** exactly. State that secrets are never committed and must not be echoed in assistant output. -3. **MCP configuration** — Server definitions live in **`mcps.json`** at the pack root; use **`${...}`** placeholders only; briefly note isolation/network/credentials posture if it affects the user (see existing pack fragments). -4. **Installation** — At minimum **Lola** (`lola install -f `) and the module **`path:`** in **`marketplace/rh-agentic-collection.yml`**. If **`marketplaces`** in the catalog includes **Claude Code** or **Cursor**, include install notes for those hosts (copy the pattern from **`rh-sre/.catalog/deploy_and_use.md`**). - -**Layout recommendation:** Prefer **`deploy_and_use: '#deploy_and_use.md'`** and a sibling **`deploy_and_use.md`** so `collection.yaml` stays short. Use the **same section order** as **`rh-sre/.catalog/deploy_and_use.md`**: Prerequisites → Environment setup → Installation (Lola, then Claude Code / Cursor as applicable) → MCP configuration. For a kubeconfig-only MCP story, see **`rh-virt/.catalog/deploy_and_use.md`**. - -**Packs with no MCP servers** (or a trivial single-token env story): a compact **`deploy_and_use: |`** under **500** Unicode code points is acceptable if it still states Lola/repo install and defers detail to **`README.md`** where needed. - -**Plugin / install IDs:** default `id` equals the pack directory name. Overrides: **`rh-virt`** → `openshift-virtualization`; **`ocp-admin`** → `openshift-administration`. - -## Source precedence (pack-local) - -When multiple sources could supply the same logical field: - -1. **`skills/*/SKILL.md`** (per-skill `name`, `description`, body for `summary_markdown` hints) -2. **`/README.md`** -3. **`/AGENTS.md`** (intent routing for decision-guide hints, personas) - -**Lola marketplace:** the module whose `path` equals `` supplies `version`, module `name`, module `description`, and `tags` for listing-oriented fields. **Never** write back to marketplace YAML from automation or the create-collection workflow. - -## Provenance banners - -| Artifact | Banner | -|----------|--------| -| `collection.yaml` | Leading `#` comment block: must mention **create-collection** workflow and **golden sources** (SKILL, README, AGENTS, marketplace). | -| `.catalog/*.md` fragments | Leading HTML `` with the same intent. | -| `collection.json` | **No** embedded banner; **CI** regenerates from YAML and fails if the committed file differs. | - -## Orchestration vs regular skills - -**Primary:** maintainer / assistant judgment while following **create-collection**. **Optional:** `metadata.collection.role: orchestration` in `SKILL.md` frontmatter for clearer compliance checks — not required on every skill. - -## Completeness and CI - -All **required** schema fields must be present on merge to `main` (no empty placeholders, no `TODO:` / `TBD` in `sample_workflows.workflow`). Author field values so they stay aligned with **`AGENTS.md`**, **`mcps.json`**, and **`README.md`** where those sources define behavior; fragment refs follow [External references](#external-references-md-on-the-same-field). CI runs **`make validate`** (includes structure + **collection compliance**). - -## Related - -- [`SKILL_DESIGN_PRINCIPLES.md`](SKILL_DESIGN_PRINCIPLES.md) — skill content (Tier 2) -- [`.claude/skills/collection-compliance/SKILL.md`](.claude/skills/collection-compliance/SKILL.md) — validation workflow diff --git a/README.md b/README.md index fd0ace4..39880b7 100644 --- a/README.md +++ b/README.md @@ -24,8 +24,7 @@ agentic-collections-catalog/ │ ├── plugins.json # Plugin registry │ ├── mcp.json # MCP server metadata │ └── collections/ # Generated per-pack HTML pages -├── scripts/ # Catalog build scripts -├── COLLECTION_SPEC.md # Specification for .catalog/ structure +├── scripts/ # Catalog build and validation scripts └── Makefile ``` diff --git a/docs/README.md b/docs/README.md index 1612996..1219804 100644 --- a/docs/README.md +++ b/docs/README.md @@ -39,11 +39,9 @@ The **`name`** of each skill in **`collection.yaml`** must match the **`skills/< ## Manual Updates To manually update the site: -1. Modify pack configurations (`mcps.json`, skills, agents; optional `.claude-plugin/plugin.json`) and per-pack **`.catalog/collection.yaml`** when catalog metadata changes (see [COLLECTION_SPEC.md](../COLLECTION_SPEC.md) and [catalog/schema.yaml](../catalog/schema.yaml); use the **create-collection** / **collection-compliance** repo skills). -2. If you edit `collection.yaml`, refresh mirrors: `make catalog-mirror-json` from the repository root. -3. Run `make generate` from the root directory (runs `make validate` first when using `make test`). -4. Commit the updated `docs/data.json` and any `.catalog/` files. -5. Push to trigger GitHub Pages deployment. +1. Run `make generate` from the root directory (runs `make validate` first when using `make test`). +2. Commit the updated `docs/data.json`. +3. Push to trigger GitHub Pages deployment. ## Validation for Docs Metadata From daa5de5e06c31ccecffb24155f4f69a8f7e811d2 Mon Sep 17 00:00:00 2001 From: Daniele Martinoli Date: Fri, 26 Jun 2026 15:38:41 +0200 Subject: [PATCH 12/21] Remove plugins.json and update related documentation to reflect the single source of truth for pack discovery in the marketplace file. Adjust scripts to eliminate dependencies on the removed file and ensure proper handling of plugin metadata. Signed-off-by: Daniele Martinoli --- CLAUDE.md | 8 +-- docs/README.md | 14 +---- docs/plugins.json | 23 -------- marketplace/rh-agentic-collection.yml | 7 +++ scripts/generate_pack_data.py | 83 +++++---------------------- scripts/pack_registry.py | 49 +++------------- 6 files changed, 35 insertions(+), 149 deletions(-) delete mode 100644 docs/plugins.json diff --git a/CLAUDE.md b/CLAUDE.md index 92d0386..c679fd1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -21,7 +21,6 @@ agentic-collections-catalog/ │ ├── app.js # Application logic │ ├── styles.css # Styling │ ├── data.json # Generated catalog data (skills, packs, MCPs) -│ ├── plugins.json # Plugin registry │ ├── mcp.json # MCP server metadata │ └── collections/ # Generated per-pack HTML pages ├── scripts/ # Catalog build and maintenance scripts @@ -40,7 +39,7 @@ Skills repos (multiple) Internal process This repo │ agentic-collections-│ │ Fetch skills repos │ │ marketplace/ │ │ skills/ │────>│ Evaluate & score │────>│ docs/ (website) │ │ rh-sre/ │ │ Build catalog data │ │ data.json │ -│ ocp-admin/ │ │ Generate website │ │ plugins.json │ +│ ocp-admin/ │ │ Generate website │ │ mcp.json │ │ ... │ └──────────────────────┘ └─────────────────────┘ ├─────────────────────┤ │ other-skills-repo/ │────> (same process) @@ -65,7 +64,6 @@ This file defines which packs are available, their versions, and where to fetch The `docs/` directory contains the static site served at agentskills.io. Key files: - `data.json` — Generated catalog data aggregating all packs, skills, and MCP servers -- `plugins.json` — Plugin registry for marketplace discovery - `mcp.json` — MCP server metadata and tool descriptions - `collections/*.html` — Per-pack detail pages @@ -88,7 +86,7 @@ Scripts in `scripts/` support catalog build and maintenance: | `test_local.sh` | Automated site verification (data.json, HTML, XSS, credentials) | | `validate_mcp_types.py` | Validate MCP server type parsing (command vs HTTP) | | `eval_site_enrichment.py` | Enrich catalog with evaluation data | -| `pack_registry.py` | Discover packs from marketplace and plugins.json | +| `pack_registry.py` | Discover packs from the marketplace (single source of truth) | ## Key Considerations @@ -102,7 +100,7 @@ If you need to create or modify skills, work in the appropriate skills source re ### Catalog Data is Generated -Files like `docs/data.json`, `docs/collections/*.html`, and parts of `plugins.json` are generated artifacts. They are rebuilt by the internal process and should not be edited manually. +Files like `docs/data.json` and `docs/collections/*.html` are generated artifacts. They are rebuilt by the internal process and should not be edited manually. ### Schema Changes diff --git a/docs/README.md b/docs/README.md index 1219804..016f8f3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -36,26 +36,18 @@ When present under the repository root, **`eval///report. The **`name`** of each skill in **`collection.yaml`** must match the **`skills//`** directory and the **`eval///`** folder segment so lookups resolve correctly. -## Manual Updates - -To manually update the site: -1. Run `make generate` from the root directory (runs `make validate` first when using `make test`). -2. Commit the updated `docs/data.json`. -3. Push to trigger GitHub Pages deployment. - ## Validation for Docs Metadata -When changing docs presentation metadata (`styles.css`, `app.js`, `plugins.json`, `mcp.json`): +When changing docs presentation metadata (`styles.css`, `app.js`, `mcp.json`): 1. Prefer canonical tokens from `styles.css` over hardcoded values. -2. Ensure every in-scope pack has icon/title mappings in `plugins.json`. -3. Run validation checks: +2. Run validation checks: ```bash make validate ``` -4. Review validation output before merge. +3. Review validation output before merge. ## Security diff --git a/docs/plugins.json b/docs/plugins.json deleted file mode 100644 index c503ffa..0000000 --- a/docs/plugins.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "rh-sre": { - "title": "Agentic skill pack for Security Reliability Engineers" - }, - "rh-virt": { - "title": "Agentic skill pack for Red Hat OpenShift Virtualization" - }, - "rh-developer": { - "title": "Agentic skill pack for Software Developers" - }, - "ocp-admin": { - "title": "Agentic skill pack for Red Hat OpenShift" - }, - "rh-ai-engineer": { - "title": "Agentic skill pack for OpenShift AI Engineers" - }, - "rh-automation": { - "title": "Agentic skill pack for Automation Engineers" - }, - "rh-basic": { - "title": "Agentic skill pack for Red Hat customers" - } -} \ No newline at end of file diff --git a/marketplace/rh-agentic-collection.yml b/marketplace/rh-agentic-collection.yml index 8d5a479..b8daa02 100644 --- a/marketplace/rh-agentic-collection.yml +++ b/marketplace/rh-agentic-collection.yml @@ -5,6 +5,7 @@ version: "0.1.0" modules: - name: "ocp-admin" + title: "Agentic skill pack for Red Hat OpenShift" description: "Automation capabilities for OpenShift Container Platform cluster management, workload orchestration, security policies, and operational tasks." version: "0.1.0" repository: "https://github.com/RHEcosystemAppEng/agentic-collections-skills" @@ -24,6 +25,7 @@ modules: - "vulnerability-management" - name: "rh-developer" + title: "Agentic skill pack for Software Developers" description: "Plugins for building and deploying applications on Red Hat platforms." version: "0.1.0" repository: "https://github.com/RHEcosystemAppEng/agentic-collections-skills" @@ -40,6 +42,7 @@ modules: - "podman" - name: "rh-sre" + title: "Agentic skill pack for Security Reliability Engineers" description: "Site reliability engineering tools and automation for managing Red Hat platforms and infrastructure." version: "0.1.0" repository: "https://github.com/RHEcosystemAppEng/agentic-collections-skills" @@ -53,6 +56,7 @@ modules: - "automation" - name: "rh-ai-engineer" + title: "Agentic skill pack for OpenShift AI Engineers" description: "Red Hat OpenShift AI Engineering Agentic Collection" version: "0.1.0" repository: "https://github.com/RHEcosystemAppEng/agentic-collections-skills" @@ -70,6 +74,7 @@ modules: - "gpu" - name: "rh-virt" + title: "Agentic skill pack for Red Hat OpenShift Virtualization" description: "Virtual machine management and automation for OpenShift Virtualization and KubeVirt workloads." version: "0.1.0" repository: "https://github.com/RHEcosystemAppEng/agentic-collections-skills" @@ -83,6 +88,7 @@ modules: - "virt" - name: "rh-basic" + title: "Agentic skill pack for Red Hat customers" description: "Essential Red Hat skills for IT professionals: CVE explanation, diagnostics, product lifecycle, RHEL patch planning, and support severity guidance." version: "0.1.0" repository: "https://github.com/RHEcosystemAppEng/agentic-collections-skills" @@ -98,6 +104,7 @@ modules: - "patching" - name: "rh-automation" + title: "Agentic skill pack for Automation Engineers" description: "Ansible Automation Platform governance, execution safety, and forensic troubleshooting tools for Red Hat automation engineers." version: "0.1.0" repository: "https://github.com/RHEcosystemAppEng/agentic-collections-skills" diff --git a/scripts/generate_pack_data.py b/scripts/generate_pack_data.py index 2fe69ee..a5afb13 100644 --- a/scripts/generate_pack_data.py +++ b/scripts/generate_pack_data.py @@ -4,7 +4,6 @@ """ import json -import os import re from pathlib import Path from typing import Dict, List, Any @@ -14,7 +13,7 @@ from catalog_site_bundle import bundle_catalog_for_site from generate_mcp_data import parse_mcp_file -# Union registry (marketplace ∪ plugins.json); docs site uses subset helper +# Pack dirs from marketplace (single source of truth); docs site uses maturity-gated subset PACK_DIRS = pack_registry.get_union_pack_dirs() DOCS_PACK_DIRS = pack_registry.get_docs_pack_dirs() @@ -46,81 +45,37 @@ def parse_yaml_frontmatter(file_path: Path) -> Dict[str, Any]: return {} -def load_plugin_titles() -> Dict[str, str]: +def parse_plugin_json(pack_dir: str) -> Dict[str, Any]: """ - Load plugin title mappings from docs/plugins.json. - - Returns: - Dictionary mapping plugin names to display titles - """ - plugins_file = Path('docs/plugins.json') - - if not plugins_file.exists(): - print("Warning: docs/plugins.json not found, using default titles") - return {} - - try: - with open(plugins_file, 'r', encoding='utf-8') as f: - data = json.load(f) - - # Extract just the titles into a simple mapping - return {name: info['title'] for name, info in data.items()} - - except Exception as e: - print(f"Warning: Failed to load docs/plugins.json: {e}") - return {} - - -def parse_plugin_json(pack_dir: str, plugin_titles: Dict[str, str]) -> Dict[str, Any]: - """ - Parse optional plugin.json from a pack directory and merge with title from docs/plugins.json. - - Args: - pack_dir: Name of the pack directory - plugin_titles: Dictionary mapping plugin names to display titles - - Returns: - Dictionary with plugin metadata, or defaults if file doesn't exist + Parse optional plugin.json from a pack directory. Title falls back to + the marketplace module's ``title`` field, then a humanized pack name. """ plugin_path = Path(pack_dir) / '.claude-plugin' / 'plugin.json' + mod = pack_registry.load_marketplace_module_by_path(pack_dir) or {} + default_title = mod.get("title") or pack_dir.replace("-", " ").title() - # Default values if plugin.json doesn't exist defaults = { 'name': pack_dir, 'version': '0.0.0', 'description': f'{pack_dir} agentic collection', 'author': {'name': 'Red Hat'}, 'license': 'Apache-2.0', - 'keywords': [] + 'keywords': [], + 'title': default_title, } if not plugin_path.exists(): - # Use title from plugins.json if available - if pack_dir in plugin_titles: - defaults['title'] = plugin_titles[pack_dir] return defaults try: with open(plugin_path, 'r', encoding='utf-8') as f: data = json.load(f) - - # Merge with defaults (in case some fields are missing) result = {**defaults, **data} - - # Override with title from docs/plugins.json if available - if pack_dir in plugin_titles: - result['title'] = plugin_titles[pack_dir] - elif 'title' not in result: - # Fallback: use name as title if not set - result['title'] = result['name'] - + if 'title' not in result: + result['title'] = default_title return result - except Exception as e: print(f"Warning: Failed to parse {plugin_path}: {e}") - # Use title from plugins.json if available even on error - if pack_dir in plugin_titles: - defaults['title'] = plugin_titles[pack_dir] return defaults @@ -317,17 +272,12 @@ def detect_repo_license(repo_root: Path, pack_path: str = ".") -> str: return "Unknown" -def load_federated_packs(plugin_titles: Dict[str, str] | None = None) -> List[Dict[str, Any]]: - """ - Fetch federated modules and return them as standalone pack entries. - - Fetch modules listed in the marketplace and return them as standalone pack entries. - """ +def load_federated_packs() -> List[Dict[str, Any]]: + """Clone each marketplace module and return it as a standalone pack entry.""" import shutil import subprocess import tempfile - titles = plugin_titles if plugin_titles is not None else load_plugin_titles() modules = pack_registry.load_federated_modules() if not modules: return [] @@ -395,7 +345,7 @@ def load_federated_packs(plugin_titles: Dict[str, str] | None = None) -> List[Di "icon": mod.get("icon", ""), "plugin": { "name": name, - "title": titles.get(name, name.replace("-", " ").title()), + "title": mod.get("title") or name.replace("-", " ").title(), "version": version, "description": description, "author": {"name": "External"}, @@ -430,9 +380,6 @@ def generate_pack_data() -> List[Dict[str, Any]]: """ packs = [] - # Load plugin title mappings from docs/plugins.json - plugin_titles = load_plugin_titles() - for pack_dir in DOCS_PACK_DIRS: pack_path = Path(pack_dir) @@ -442,7 +389,7 @@ def generate_pack_data() -> List[Dict[str, Any]]: docs = parse_docs(pack_dir) - plugin = parse_plugin_json(pack_dir, plugin_titles) + plugin = parse_plugin_json(pack_dir) overlay_plugin_version_from_marketplace(pack_dir, plugin) pack = { @@ -461,7 +408,7 @@ def generate_pack_data() -> List[Dict[str, Any]]: plugin_title = pack['plugin'].get('title', pack_dir) print(f"✓ Parsed {plugin_title}: {len(pack['skills'])} skills, {len(pack['agents'])} agents, {len(docs)} docs") - federated = load_federated_packs(plugin_titles) + federated = load_federated_packs() if federated: packs.extend(federated) print(f"✓ Added {len(federated)} marketplace pack(s)") diff --git a/scripts/pack_registry.py b/scripts/pack_registry.py index 63565ce..1958c4e 100644 --- a/scripts/pack_registry.py +++ b/scripts/pack_registry.py @@ -1,13 +1,12 @@ """ -Resolve the set of agentic pack directories from Lola marketplace + docs/plugins.json. +Resolve the set of agentic pack directories from the Lola marketplace file. -Policy (union): include every ``modules[].path`` from ``marketplace/rh-agentic-collection.yml`` -and every key from ``docs/plugins.json``, keeping only directory names that exist on disk. +The marketplace file (``marketplace/rh-agentic-collection.yml``) is the single +source of truth for pack discovery. """ from __future__ import annotations -import json import re from pathlib import Path from typing import Any, Dict, List, Optional, Set @@ -15,7 +14,6 @@ import yaml DEFAULT_MARKETPLACE = Path("marketplace/rh-agentic-collection.yml") -DEFAULT_PLUGINS_JSON = Path("docs/plugins.json") def _repo_root() -> Path: @@ -37,35 +35,17 @@ def load_marketplace_module_paths(marketplace_path: Optional[Path] = None) -> Li return out -def load_plugins_json_keys(plugins_path: Optional[Path] = None) -> List[str]: - path = plugins_path or (_repo_root() / DEFAULT_PLUGINS_JSON) - if not path.exists(): - return [] - with open(path, "r", encoding="utf-8") as f: - data = json.load(f) - if not isinstance(data, dict): - return [] - return sorted(data.keys()) - - def get_union_pack_dirs( repo_root: Optional[Path] = None, marketplace_path: Optional[Path] = None, - plugins_path: Optional[Path] = None, ) -> List[str]: """ - Sorted unique pack directory names that exist under repo root and appear in - marketplace and/or docs/plugins.json union. + Sorted pack directory names from the marketplace that exist on disk under repo root. + The marketplace file is the single source of truth for pack discovery. """ root = repo_root or _repo_root() - names: Set[str] = set() - names.update(load_marketplace_module_paths(marketplace_path)) - names.update(load_plugins_json_keys(plugins_path)) - existing: List[str] = [] - for name in sorted(names): - if (root / name).is_dir(): - existing.append(name) - return existing + names: Set[str] = set(load_marketplace_module_paths(marketplace_path)) + return [name for name in sorted(names) if (root / name).is_dir()] def load_marketplace_module_by_path( @@ -158,21 +138,6 @@ def is_federation_module(pack_dir: str) -> bool: return pack_dir.startswith("federation/modules/") -def load_plugin_title(pack_dir: str, repo_root: Optional[Path] = None) -> Optional[str]: - root = repo_root or _repo_root() - p = root / DEFAULT_PLUGINS_JSON - if not p.exists(): - return None - with open(p, "r", encoding="utf-8") as f: - data = json.load(f) - entry = data.get(pack_dir) - if isinstance(entry, dict): - t = entry.get("title") - if isinstance(t, str): - return t - return None - - # Catalog `maturity` value that is included in GitHub Pages / docs/data.json generation. DOCS_MATURITY_PUBLISH: str = "GREEN" From 7110ba7ac7f1e000d696afe1e53310cb508120a7 Mon Sep 17 00:00:00 2001 From: Daniele Martinoli Date: Fri, 26 Jun 2026 15:39:19 +0200 Subject: [PATCH 13/21] Update OPEN_ITEMS.md to include new entries for collecting `title` and `icon` fields from Compass registration under Marketplace configuration. Signed-off-by: Daniele Martinoli --- OPEN_ITEMS.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/OPEN_ITEMS.md b/OPEN_ITEMS.md index 4c64137..78fad91 100644 --- a/OPEN_ITEMS.md +++ b/OPEN_ITEMS.md @@ -1,2 +1,5 @@ ## Open Items -* How to collect `icon` field from Compass registration \ No newline at end of file +### Marketplace configuration +* How to collect `icon` field from Compass registration +* How to collect `title` field from Compass registration +* \ No newline at end of file From b9a7a685515c876045359e254ad22b59338a098c Mon Sep 17 00:00:00 2001 From: Daniele Martinoli Date: Fri, 26 Jun 2026 15:51:09 +0200 Subject: [PATCH 14/21] Update OPEN_ITEMS.md to include a reference to the new mcp.json file for marketplace configuration. Signed-off-by: Daniele Martinoli --- OPEN_ITEMS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OPEN_ITEMS.md b/OPEN_ITEMS.md index 78fad91..c2fb60e 100644 --- a/OPEN_ITEMS.md +++ b/OPEN_ITEMS.md @@ -2,4 +2,4 @@ ### Marketplace configuration * How to collect `icon` field from Compass registration * How to collect `title` field from Compass registration -* \ No newline at end of file +* Replace [mcp.json](./docs/mcp.json) \ No newline at end of file From 5b3e0cc8020db4add2ed7e9e9dabfbb13c9fb8fb Mon Sep 17 00:00:00 2001 From: Daniele Martinoli Date: Fri, 26 Jun 2026 16:08:04 +0200 Subject: [PATCH 15/21] Filter federated packs by maturity from catalog bundle Read the maturity field from the already-loaded .catalog/collection.yaml bundle after cloning. Absent catalog => assume GREEN (include pack). Present catalog with non-GREEN maturity => skip with warning. Co-Authored-By: Claude Sonnet 4.6 --- scripts/generate_pack_data.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scripts/generate_pack_data.py b/scripts/generate_pack_data.py index a5afb13..fe6dfb3 100644 --- a/scripts/generate_pack_data.py +++ b/scripts/generate_pack_data.py @@ -328,6 +328,11 @@ def load_federated_packs() -> List[Dict[str, Any]]: for w in cat_warns: print(f" ⚠️ {w}") + maturity = (cat_bundle.get("maturity") or "").strip().upper() if cat_bundle else "" + if maturity and maturity != pack_registry.DOCS_MATURITY_PUBLISH: + print(f" ⚠️ Skipping '{name}': maturity is {maturity!r} (not GREEN)") + continue + # README fallback: read content so the UI can show it when catalog is absent readme_path = pack_dir / "README.md" readme_content = readme_path.read_text(encoding="utf-8") if readme_path.is_file() else None From 78ef2e4c3a42f18a06292af6bd4d2bb7aac2994f Mon Sep 17 00:00:00 2001 From: Daniele Martinoli Date: Fri, 26 Jun 2026 16:12:52 +0200 Subject: [PATCH 16/21] Remove dead local-pack scanning code; marketplace is sole source of truth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All packs are now cloned from external repos at build time — no local pack directories exist. Remove functions that scanned local paths: pack_registry.py: - load_pack_maturity, get_docs_pack_dirs, get_federation_module_dirs, get_docs_federation_module_dirs, is_federation_module generate_pack_data.py: - DOCS_PACK_DIRS, parse_plugin_json, overlay_plugin_version_from_marketplace, parse_agents, parse_docs, sanitize_for_json, dead local-pack loop Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 1 - scripts/generate_pack_data.py | 213 +--------------------------------- scripts/pack_registry.py | 59 +--------- 3 files changed, 2 insertions(+), 271 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index c679fd1..c61484a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -24,7 +24,6 @@ agentic-collections-catalog/ │ ├── mcp.json # MCP server metadata │ └── collections/ # Generated per-pack HTML pages ├── scripts/ # Catalog build and maintenance scripts - ├── README.md └── LICENSE ``` diff --git a/scripts/generate_pack_data.py b/scripts/generate_pack_data.py index fe6dfb3..53a335a 100644 --- a/scripts/generate_pack_data.py +++ b/scripts/generate_pack_data.py @@ -3,7 +3,6 @@ Parse agentic packs and extract plugin metadata, skills, and agents. """ -import json import re from pathlib import Path from typing import Dict, List, Any @@ -13,89 +12,19 @@ from catalog_site_bundle import bundle_catalog_for_site from generate_mcp_data import parse_mcp_file -# Pack dirs from marketplace (single source of truth); docs site uses maturity-gated subset -PACK_DIRS = pack_registry.get_union_pack_dirs() -DOCS_PACK_DIRS = pack_registry.get_docs_pack_dirs() - - def parse_yaml_frontmatter(file_path: Path) -> Dict[str, Any]: - """ - Extract YAML frontmatter from a markdown file. - - Args: - file_path: Path to the markdown file - - Returns: - Dictionary containing the frontmatter data - """ try: with open(file_path, 'r', encoding='utf-8') as f: content = f.read() - - # Match YAML frontmatter (---\n...\n---) match = re.match(r'^---\s*\n(.*?)\n---\s*\n', content, re.DOTALL) if not match: return {} - - frontmatter_text = match.group(1) - return yaml.safe_load(frontmatter_text) or {} - + return yaml.safe_load(match.group(1)) or {} except Exception as e: print(f"Warning: Failed to parse frontmatter from {file_path}: {e}") return {} -def parse_plugin_json(pack_dir: str) -> Dict[str, Any]: - """ - Parse optional plugin.json from a pack directory. Title falls back to - the marketplace module's ``title`` field, then a humanized pack name. - """ - plugin_path = Path(pack_dir) / '.claude-plugin' / 'plugin.json' - mod = pack_registry.load_marketplace_module_by_path(pack_dir) or {} - default_title = mod.get("title") or pack_dir.replace("-", " ").title() - - defaults = { - 'name': pack_dir, - 'version': '0.0.0', - 'description': f'{pack_dir} agentic collection', - 'author': {'name': 'Red Hat'}, - 'license': 'Apache-2.0', - 'keywords': [], - 'title': default_title, - } - - if not plugin_path.exists(): - return defaults - - try: - with open(plugin_path, 'r', encoding='utf-8') as f: - data = json.load(f) - result = {**defaults, **data} - if 'title' not in result: - result['title'] = default_title - return result - except Exception as e: - print(f"Warning: Failed to parse {plugin_path}: {e}") - return defaults - - -def overlay_plugin_version_from_marketplace(pack_dir: str, plugin: Dict[str, Any]) -> None: - """ - Set plugin['version'] from marketplace/rh-agentic-collection.yml modules[].version - when present. Overrides defaults and .claude-plugin/plugin.json so the docs site - matches the Lola marketplace module version. - """ - mod = pack_registry.load_marketplace_module_by_path(pack_dir) - if not mod: - return - raw = mod.get("version") - if raw is None: - return - ver = str(raw).strip() - if ver: - plugin["version"] = ver - - def parse_skills(pack_dir: str) -> List[Dict[str, Any]]: """ Parse skills from skills/*/SKILL.md files. @@ -133,118 +62,6 @@ def parse_skills(pack_dir: str) -> List[Dict[str, Any]]: return sorted(skills, key=lambda s: s['name']) -def parse_agents(pack_dir: str) -> List[Dict[str, Any]]: - """ - Parse agents from agents/*.md files. - - Args: - pack_dir: Name of the pack directory - - Returns: - List of agent dictionaries with name, description, model, tools, file_path - """ - agents = [] - agents_dir = Path(pack_dir) / 'agents' - - if not agents_dir.exists(): - return agents - - # Find all .md files in agents directory - for agent_file in agents_dir.glob('*.md'): - frontmatter = parse_yaml_frontmatter(agent_file) - - # Extract metadata - name = frontmatter.get('name', agent_file.stem) - description = frontmatter.get('description', '') - model = frontmatter.get('model', 'inherit') - tools = frontmatter.get('tools', []) - - # Clean up description - if isinstance(description, str): - description = ' '.join(description.split()) - - agents.append({ - 'name': name, - 'description': description, - 'model': model, - 'tools': tools, - 'file_path': str(agent_file.relative_to(pack_dir)) - }) - - return sorted(agents, key=lambda a: a['name']) - - -def sanitize_for_json(obj: Any) -> Any: - """ - Convert objects to JSON-serializable format. - Handles date objects and other non-serializable types. - - Args: - obj: Object to sanitize - - Returns: - JSON-serializable version of the object - """ - from datetime import date, datetime - - if isinstance(obj, (date, datetime)): - return obj.isoformat() - elif isinstance(obj, dict): - return {k: sanitize_for_json(v) for k, v in obj.items()} - elif isinstance(obj, list): - return [sanitize_for_json(item) for item in obj] - else: - return obj - - -def parse_docs(pack_dir: str) -> List[Dict[str, Any]]: - """ - Parse documentation files from docs/**/*.md files. - - Args: - pack_dir: Name of the pack directory - - Returns: - List of doc dictionaries with title, sources, category, file_path - """ - docs = [] - docs_dir = Path(pack_dir) / 'docs' - - if not docs_dir.exists(): - return docs - - # Files to exclude from documentation parsing - EXCLUDE_FILES = {'README.md', 'INDEX.md', 'SOURCES.md'} - - # Find all .md files recursively (excluding .ai-index directory) - for doc_file in docs_dir.rglob('*.md'): - # Skip excluded files and files in .ai-index directory - if doc_file.name in EXCLUDE_FILES or '.ai-index' in doc_file.parts: - continue - - frontmatter = parse_yaml_frontmatter(doc_file) - - # Extract metadata - title = frontmatter.get('title', doc_file.stem.replace('-', ' ').title()) - category = frontmatter.get('category', doc_file.parent.name) - sources = frontmatter.get('sources', []) - - # Ensure sources is a list and sanitize for JSON - if not isinstance(sources, list): - sources = [] - sources = sanitize_for_json(sources) - - docs.append({ - 'title': title, - 'category': category, - 'sources': sources, - 'file_path': str(doc_file.relative_to(pack_dir)) - }) - - # Sort by category first, then by title - return sorted(docs, key=lambda d: (d['category'], d['title'])) - - def detect_repo_license(repo_root: Path, pack_path: str = ".") -> str: """Best-effort SPDX identifier from LICENSE files in a cloned repository.""" candidates = [ @@ -385,34 +202,6 @@ def generate_pack_data() -> List[Dict[str, Any]]: """ packs = [] - for pack_dir in DOCS_PACK_DIRS: - pack_path = Path(pack_dir) - - if not pack_path.exists(): - print(f"Warning: Pack directory {pack_dir} does not exist, skipping") - continue - - docs = parse_docs(pack_dir) - - plugin = parse_plugin_json(pack_dir) - overlay_plugin_version_from_marketplace(pack_dir, plugin) - - pack = { - 'name': pack_dir, - 'path': f'./{pack_dir}', - 'plugin': plugin, - 'skills': parse_skills(pack_dir), - 'agents': parse_agents(pack_dir), - 'docs': docs, - 'has_readme': (pack_path / 'README.md').exists() - } - - packs.append(pack) - - # Use title from plugin data for display - plugin_title = pack['plugin'].get('title', pack_dir) - print(f"✓ Parsed {plugin_title}: {len(pack['skills'])} skills, {len(pack['agents'])} agents, {len(docs)} docs") - federated = load_federated_packs() if federated: packs.extend(federated) diff --git a/scripts/pack_registry.py b/scripts/pack_registry.py index 1958c4e..c0f2bf6 100644 --- a/scripts/pack_registry.py +++ b/scripts/pack_registry.py @@ -120,62 +120,5 @@ def load_federated_modules( ] -def get_federation_module_dirs(repo_root: Optional[Path] = None) -> List[str]: - """Return ``federation/modules/`` paths that have a ``.catalog/collection.yaml`` on disk.""" - root = repo_root or _repo_root() - fed_root = root / "federation" / "modules" - if not fed_root.is_dir(): - return [] - return sorted( - f"federation/modules/{p.name}" - for p in fed_root.iterdir() - if p.is_dir() and (p / ".catalog" / "collection.yaml").is_file() - ) - - -def is_federation_module(pack_dir: str) -> bool: - """Return ``True`` if *pack_dir* lives under ``federation/modules/``.""" - return pack_dir.startswith("federation/modules/") - - -# Catalog `maturity` value that is included in GitHub Pages / docs/data.json generation. +# Catalog `maturity` value published to GitHub Pages / docs/data.json. DOCS_MATURITY_PUBLISH: str = "GREEN" - - -def load_pack_maturity(pack_dir: str, repo_root: Optional[Path] = None) -> Optional[str]: - """Return uppercase maturity from ``/.catalog/collection.yaml``, or None if missing/invalid.""" - root = repo_root or _repo_root() - path = root / pack_dir / ".catalog" / "collection.yaml" - if not path.is_file(): - return None - try: - with open(path, "r", encoding="utf-8") as f: - data = yaml.safe_load(f) or {} - except (OSError, yaml.YAMLError): - return None - m = data.get("maturity") - if isinstance(m, str) and m.strip(): - return m.strip().upper() - return None - - -def get_docs_pack_dirs( - repo_root: Optional[Path] = None, -) -> List[str]: - """Pack dirs included in GitHub Pages data.json: union registry packs whose catalog maturity is GREEN.""" - root = repo_root or _repo_root() - out: List[str] = [] - for p in get_union_pack_dirs(repo_root): - if load_pack_maturity(p, root) == DOCS_MATURITY_PUBLISH: - out.append(p) - return out - - -def get_docs_federation_module_dirs(repo_root: Optional[Path] = None) -> List[str]: - """Federation module dirs listed on GitHub Pages (catalog maturity GREEN only).""" - root = repo_root or _repo_root() - return [ - p - for p in get_federation_module_dirs(root) - if load_pack_maturity(p, root) == DOCS_MATURITY_PUBLISH - ] From ae579b21427696a331020103d9f3e8f6721ccda6 Mon Sep 17 00:00:00 2001 From: Daniele Martinoli Date: Fri, 26 Jun 2026 16:14:58 +0200 Subject: [PATCH 17/21] Replace CLAUDE.md with AGENTS.md; align with actual repo purpose Rename to the more standard AGENTS.md convention. Rewrite content to accurately describe the actual build flow: - Scripts in this repo do the cloning and building (no separate 'internal process') - Marketplace YAML is the single source of truth for pack discovery - Maturity gate: absent catalog => GREEN; non-GREEN catalog => skip - CI workflows (validate.yml, deploy-pages.yml) documented - Marketplace module fields documented with required/optional distinction - Generated files called out explicitly as read-only Co-Authored-By: Claude Sonnet 4.6 --- AGENTS.md | 91 ++++++++++++++++++++++++++++++++++++++++++++++ CLAUDE.md | 106 ------------------------------------------------------ 2 files changed, 91 insertions(+), 106 deletions(-) create mode 100644 AGENTS.md delete mode 100644 CLAUDE.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..35da3a2 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,91 @@ +# AGENTS.md + +Guidance for AI coding assistants working in this repository. + +## What This Repository Is + +This is the **catalog and marketplace** for Red Hat agentic collections. It serves two purposes: + +1. **Lola marketplace** — `marketplace/rh-agentic-collection.yml` declares which skill packs are available and where to fetch them. Users install packs with `lola install -f `. +2. **agentskills.io website** — the `docs/` directory is the static site. The scripts in `scripts/` build it by cloning each pack's source repo and extracting metadata. + +**No skills are authored or stored here.** Skills live in their own source repos (e.g., [agentic-collections-skills](https://github.com/RHEcosystemAppEng/agentic-collections-skills)). This repo only aggregates and presents them. + +## Repository Structure + +``` +agentic-collections-catalog/ +├── marketplace/ +│ └── rh-agentic-collection.yml # Single source of truth for pack discovery +├── catalog/ +│ └── schema.yaml # JSON Schema for .catalog/collection.yaml files in skills repos +├── docs/ # Static site (agentskills.io) +│ ├── index.html # SPA entry point +│ ├── app.js # Rendering and search logic (XSS-safe, no innerHTML) +│ ├── styles.css # Red Hat-themed styling +│ ├── data.json # Generated — do not edit manually +│ ├── mcp.json # MCP server metadata — do not edit manually +│ └── collections/ # Generated per-pack HTML pages +└── scripts/ # Build and verification scripts +``` + +## How the Build Works + +`make generate` runs `scripts/build_website.py`, which: + +1. Reads `marketplace/rh-agentic-collection.yml` to get the list of packs +2. For each pack: `git clone` its source repo into a temp directory +3. Reads `.catalog/collection.yaml` (if present) for metadata and maturity +4. Skips packs whose catalog declares a non-GREEN maturity; absent catalog → assumed GREEN +5. Reads `skills/*/SKILL.md` frontmatter, README, and `mcps.json` from the clone +6. Writes `docs/data.json` and generates `docs/collections/.html` + +The clones are ephemeral (deleted after each build). Nothing from external repos is committed here. + +## Marketplace File + +`marketplace/rh-agentic-collection.yml` is the **only** place that controls which packs appear on the site. Each module entry carries: + +| Field | Required | Purpose | +|-------|----------|---------| +| `name` | yes | Pack identifier (used in URLs and filenames) | +| `repository` | yes | Git URL to clone | +| `path` | yes | Subdirectory within the repo (`.` for root) | +| `version` | yes | Displayed version | +| `description` | yes | Short description shown on cards | +| `title` | yes | Display name shown on cards | +| `icon` | recommended | Emoji or HTTPS URL to an image | +| `tags` | optional | Filter tags | +| `ref` | optional | 40-char commit SHA to pin; absent = main branch | + +Do not add packs to `docs/data.json` or any other file directly. Add them to the marketplace YAML. + +## Scripts + +| Script | Purpose | Invoked by | +|--------|---------|------------| +| `build_website.py` | Orchestrates the full build | `make generate` | +| `generate_pack_data.py` | Clones repos, extracts pack/skill metadata | build | +| `generate_mcp_data.py` | Extracts MCP server configs from `mcps.json` | build | +| `generate_collection_pages.py` | Renders per-pack HTML pages | build | +| `catalog_site_bundle.py` | Resolves `.catalog/` fragment `#ref` pointers | build | +| `eval_site_enrichment.py` | Attaches ABEval report summaries to skills | build | +| `pack_registry.py` | Marketplace-driven pack discovery utilities | build | +| `check_site.py` | Interactive manual verification of `data.json` | manual | +| `test_local.sh` | Automated validation (JSON, HTML, XSS, credentials) | `make test` | +| `validate_mcp_types.py` | Sanity-checks MCP server type parsing | manual | + +## Key Rules + +- **Marketplace is the single source of truth.** Do not add packs, icons, or titles anywhere else. +- **Generated files are read-only.** `docs/data.json`, `docs/mcp.json`, and `docs/collections/*.html` are rebuilt on every run — manual edits will be overwritten. +- **No skills development here.** To create or modify skills, work in the appropriate skills source repo. +- **Schema changes need coordination.** Updating `catalog/schema.yaml` requires matching changes in all skills repos' `.catalog/` directories. +- **Security.** All DOM manipulation in `app.js` uses `textContent` and `createElement` — never `innerHTML` with external data. + +## CI + +| Workflow | Trigger | What it does | +|----------|---------|-------------| +| `validate.yml` | Push / PR | Runs `make test` (→ `test_local.sh`) | +| `deploy-pages.yml` | Push to main | Runs `make generate`, deploys `docs/` to GitHub Pages | diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index c61484a..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,106 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## What This Repository Is - -This is the **unified catalog and marketplace** for Red Hat agentic collections. It aggregates skills, MCP server configurations, and agentic packs from multiple source repositories into a single, browsable catalog served through [Lola](https://github.com/LobsterTrap/lola) and the [agentskills.io](https://agentskills.io) website. - -**This is NOT a skills development repository.** No skills are authored or maintained here. Skills live in their own source repositories (e.g., [agentic-collections-skills](https://github.com/RHEcosystemAppEng/agentic-collections-skills)). An internal process fetches content from those repos, evaluates it with its own scorecard, and assembles the catalog automatically. - -## Repository Structure - -``` -agentic-collections-catalog/ -├── marketplace/ # Lola marketplace definition -│ └── rh-agentic-collection.yml # Module registry — what users install via `lola install -f` -├── catalog/ # Collection schema for validation -│ └── schema.yaml -├── docs/ # Website source (agentskills.io) -│ ├── index.html # Landing page -│ ├── app.js # Application logic -│ ├── styles.css # Styling -│ ├── data.json # Generated catalog data (skills, packs, MCPs) -│ ├── mcp.json # MCP server metadata -│ └── collections/ # Generated per-pack HTML pages -├── scripts/ # Catalog build and maintenance scripts -├── README.md -└── LICENSE -``` - -## How the Catalog Works - -### Data Flow - -``` -Skills repos (multiple) Internal process This repo -┌─────────────────────┐ ┌──────────────────────┐ ┌─────────────────────┐ -│ agentic-collections-│ │ Fetch skills repos │ │ marketplace/ │ -│ skills/ │────>│ Evaluate & score │────>│ docs/ (website) │ -│ rh-sre/ │ │ Build catalog data │ │ data.json │ -│ ocp-admin/ │ │ Generate website │ │ mcp.json │ -│ ... │ └──────────────────────┘ └─────────────────────┘ -├─────────────────────┤ -│ other-skills-repo/ │────> (same process) -└─────────────────────┘ -``` - -Each skills repo contains packs with `.catalog/` metadata that describes its content. The internal process reads that metadata, applies its own evaluation and scoring, and publishes the results here. - -### Marketplace (Lola) - -`marketplace/rh-agentic-collection.yml` is the Lola module registry. Users consume it with: - -```bash -lola market add rh-agentic-collections -lola install -f rh-sre -``` - -This file defines which packs are available, their versions, and where to fetch them. - -### Website (agentskills.io) - -The `docs/` directory contains the static site served at agentskills.io. Key files: - -- `data.json` — Generated catalog data aggregating all packs, skills, and MCP servers -- `mcp.json` — MCP server metadata and tool descriptions -- `collections/*.html` — Per-pack detail pages - -### Collection Schema - -- `catalog/schema.yaml` — JSON Schema that defines the structure of `.catalog/collection.yaml` files in skills repos - -## Scripts - -Scripts in `scripts/` support catalog build and maintenance: - -| Script | Purpose | -|--------|---------| -| `build_website.py` | Generate `docs/data.json` from pack data | -| `generate_pack_data.py` | Extract pack metadata for the website | -| `generate_mcp_data.py` | Extract MCP server data for the website | -| `generate_collection_pages.py` | Generate per-pack HTML pages | -| `catalog_site_bundle.py` | Resolve `.catalog/` fragment refs for site embedding | -| `check_site.py` | Interactive site summary and manual testing checklist | -| `test_local.sh` | Automated site verification (data.json, HTML, XSS, credentials) | -| `validate_mcp_types.py` | Validate MCP server type parsing (command vs HTTP) | -| `eval_site_enrichment.py` | Enrich catalog with evaluation data | -| `pack_registry.py` | Discover packs from the marketplace (single source of truth) | - -## Key Considerations - -### No CI in This Repo - -This repository has no GitHub Actions workflows. The internal process that builds the catalog handles all validation, evaluation, and deployment. - -### No Skills Development - -If you need to create or modify skills, work in the appropriate skills source repository, not here. This repo only contains the infrastructure to aggregate and present skills. - -### Catalog Data is Generated - -Files like `docs/data.json` and `docs/collections/*.html` are generated artifacts. They are rebuilt by the internal process and should not be edited manually. - -### Schema Changes - -If the structure of `.catalog/collection.yaml` needs to change, update `catalog/schema.yaml` here, then coordinate with skills repos to update their `.catalog/` directories accordingly. From 808578a32a8319373bee8460b3b3bfc3ade6a72e Mon Sep 17 00:00:00 2001 From: Daniele Martinoli Date: Fri, 26 Jun 2026 16:21:15 +0200 Subject: [PATCH 18/21] Enhance pack card evaluation logic to include skill checks Updated the createPackCard function to improve badge display logic. Now, badges are shown if the pack has skills or if the evaluated count is zero. This change ensures better representation of pack evaluation status in the UI. Signed-off-by: Daniele Martinoli --- docs/app.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/app.js b/docs/app.js index b52f981..eaf3b77 100644 --- a/docs/app.js +++ b/docs/app.js @@ -298,10 +298,11 @@ function createPackCard(pack) { const badges = document.createElement('div'); badges.className = 'pack-card-badges'; - if (es && es.catalog_skill_count > 0) { + const hasSkills = pack.skills && pack.skills.length > 0; + if (hasSkills || (es && es.catalog_skill_count > 0)) { const badge = document.createElement('span'); badge.className = 'pack-eval-badge'; - if (es.evaluated_count === 0) { + if (!es || es.evaluated_count === 0) { badge.classList.add('is-none'); badge.textContent = 'NOT EVALUATED'; } else if (es.failed_count === 0) { From bebf4e165789b116e2c45c65d8c57b6a43f1e02c Mon Sep 17 00:00:00 2001 From: Daniele Martinoli Date: Fri, 26 Jun 2026 16:42:00 +0200 Subject: [PATCH 19/21] Refactor skill parsing logic in generate_pack_data.py Updated the parse_skills function to handle both multi-skill packs and single-skill repositories. The logic now checks for a SKILL.md file at the root of the pack directory, improving the flexibility of skill data retrieval. Signed-off-by: Daniele Martinoli --- docs/app.js | 3 +-- scripts/generate_pack_data.py | 21 +++++++++++++-------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/docs/app.js b/docs/app.js index eaf3b77..702fd2a 100644 --- a/docs/app.js +++ b/docs/app.js @@ -298,8 +298,7 @@ function createPackCard(pack) { const badges = document.createElement('div'); badges.className = 'pack-card-badges'; - const hasSkills = pack.skills && pack.skills.length > 0; - if (hasSkills || (es && es.catalog_skill_count > 0)) { + { const badge = document.createElement('span'); badge.className = 'pack-eval-badge'; if (!es || es.evaluated_count === 0) { diff --git a/scripts/generate_pack_data.py b/scripts/generate_pack_data.py index 53a335a..793fb51 100644 --- a/scripts/generate_pack_data.py +++ b/scripts/generate_pack_data.py @@ -27,17 +27,22 @@ def parse_yaml_frontmatter(file_path: Path) -> Dict[str, Any]: def parse_skills(pack_dir: str) -> List[Dict[str, Any]]: """ - Parse skills from skills/*/SKILL.md files. - - Args: - pack_dir: Name of the pack directory - - Returns: - List of skill dictionaries with name, description, file_path + Parse skills from skills/*/SKILL.md (multi-skill pack) or SKILL.md at root (single-skill repo). """ skills = [] - skills_dir = Path(pack_dir) / 'skills' + root = Path(pack_dir) + + # Single-skill repo: SKILL.md at the pack root + root_skill = root / 'SKILL.md' + if root_skill.is_file(): + frontmatter = parse_yaml_frontmatter(root_skill) + name = frontmatter.get('name', root.name) + description = frontmatter.get('description', '') + if isinstance(description, str): + description = ' '.join(description.split()) + return [{'name': name, 'description': description, 'file_path': 'SKILL.md'}] + skills_dir = root / 'skills' if not skills_dir.exists(): return skills From 3b788b9422ba3c09e61a86c48ac4787e1909011d Mon Sep 17 00:00:00 2001 From: Daniele Martinoli Date: Fri, 26 Jun 2026 16:46:35 +0200 Subject: [PATCH 20/21] review license link generation --- scripts/generate_collection_pages.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/generate_collection_pages.py b/scripts/generate_collection_pages.py index 25f6968..afc5541 100644 --- a/scripts/generate_collection_pages.py +++ b/scripts/generate_collection_pages.py @@ -664,6 +664,8 @@ def render_collection_page(pack: Dict[str, Any], mcp_data: List[Dict[str, Any]]) if not license_nav_href: license_nav_href = f"{blob_base}/LICENSE" license_nav_href_esc = html.escape(license_nav_href, quote=True) + license_id = str(pack.get("plugin", {}).get("license") or "").strip() + license_nav_label = f"{license_id} License" if license_id and license_id != "Unknown" else "License" return f""" @@ -690,7 +692,7 @@ def render_collection_page(pack: Dict[str, Any], mcp_data: List[Dict[str, Any]])
From 7bf794bc2c6094d507348331393c99820047580e Mon Sep 17 00:00:00 2001 From: Daniele Martinoli Date: Fri, 26 Jun 2026 16:55:01 +0200 Subject: [PATCH 21/21] Update OPEN_ITEMS.md and README.md to enhance documentation - Added a new section in OPEN_ITEMS.md for external contributions, detailing how to replace collection pack info in the absence of collection.yml. - Removed outdated instructions from README.md regarding git hooks installation for local development. Signed-off-by: Daniele Martinoli --- OPEN_ITEMS.md | 9 ++++++++- docs/README.md | 2 -- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/OPEN_ITEMS.md b/OPEN_ITEMS.md index c2fb60e..93aee2b 100644 --- a/OPEN_ITEMS.md +++ b/OPEN_ITEMS.md @@ -2,4 +2,11 @@ ### Marketplace configuration * How to collect `icon` field from Compass registration * How to collect `title` field from Compass registration -* Replace [mcp.json](./docs/mcp.json) \ No newline at end of file +* Replace [mcp.json](./docs/mcp.json) + +### External contributions +* How to replace collection pack info in absemce of collection.yml? + * README + * License + * List of skills + * Installation \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index 016f8f3..87ea479 100644 --- a/docs/README.md +++ b/docs/README.md @@ -12,8 +12,6 @@ This directory contains the generated GitHub Pages site for agentic-collections. ## Local Development -From the repository root, install git hooks once (`make install` then `scripts/install-hooks.sh`) so commits run [pre-commit](https://pre-commit.com/) (gitleaks + scoped `make validate` / skill design checks); see the main [README.md](../README.md#security). - Run locally: ```bash python -m http.server 8000