From 7a58906b1206d9eb658ec9f4c4a15ac9d2cc7b4f Mon Sep 17 00:00:00 2001 From: Daniele Martinoli Date: Fri, 26 Jun 2026 17:13:25 +0200 Subject: [PATCH 1/9] Refactor website build process to streamline evaluation logic - Removed the evaluation enrichment step from the website build script, simplifying the build process. - Integrated the evaluation enrichment directly into the pack data generation, ensuring packs are enriched before cleanup. Signed-off-by: Daniele Martinoli --- scripts/generate_pack_data.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/generate_pack_data.py b/scripts/generate_pack_data.py index 793fb51..478c6b7 100644 --- a/scripts/generate_pack_data.py +++ b/scripts/generate_pack_data.py @@ -10,6 +10,7 @@ import pack_registry from catalog_site_bundle import bundle_catalog_for_site +from eval_site_enrichment import apply_eval_enrichment from generate_mcp_data import parse_mcp_file def parse_yaml_frontmatter(file_path: Path) -> Dict[str, Any]: @@ -188,6 +189,8 @@ def load_federated_packs() -> List[Dict[str, Any]]: } if cat_bundle is not None: pack["collection"] = cat_bundle + # Enrich with eval reports from the cloned source repo before cleanup + apply_eval_enrichment([pack], clone_dest) packs.append(pack) catalog_status = "with catalog" if cat_bundle else "README only" mcp_status = f", {len(mcp_servers)} MCP server(s)" if mcp_servers else "" From 0e1875a9e4e84b837202fcbea1323eef39db4a84 Mon Sep 17 00:00:00 2001 From: Daniele Martinoli Date: Fri, 26 Jun 2026 17:35:26 +0200 Subject: [PATCH 2/9] Update documentation and scripts for agentic-collections-catalog - Renamed references from 'agentic-collections' to 'agentic-collections-catalog' across documentation and scripts for consistency. - Updated URLs in app.js, index.html, and build_website.py to point to the new repository. - Adjusted evaluation logic in scripts to accommodate the new repository structure. Signed-off-by: Daniele Martinoli --- docs/app.js | 14 +- docs/index.html | 12 +- scripts/build_website.py | 6 +- scripts/check_site.py | 2 +- scripts/eval_site_enrichment.py | 380 ++++++++++++++++++++++++++++++++ scripts/pack_registry.py | 7 +- 6 files changed, 399 insertions(+), 22 deletions(-) create mode 100644 scripts/eval_site_enrichment.py diff --git a/docs/app.js b/docs/app.js index 702fd2a..55e5c6c 100644 --- a/docs/app.js +++ b/docs/app.js @@ -1,5 +1,5 @@ /** - * agentic-collections Documentation Site + * agentic-collections-catalog Documentation Site * * SECURITY: All DOM manipulation uses textContent and createElement * to prevent XSS vulnerabilities. No innerHTML with user data. @@ -17,7 +17,7 @@ const JS_STYLE_TOKENS = { }; /** Default LICENSE in upstream repo (collection legal_resources may override). */ -const UPSTREAM_REPO_LICENSE_URL = 'https://github.com/RHEcosystemAppEng/agentic-collections/blob/main/LICENSE'; +const UPSTREAM_REPO_LICENSE_URL = 'https://github.com/RHEcosystemAppEng/agentic-collections-catalog/blob/main/LICENSE'; /** * Update toolbar counter badges @@ -816,7 +816,7 @@ function showPackDetails(packName) { const readmeButton = document.createElement('a'); readmeButton.className = 'readme-button'; readmeButton.textContent = 'README'; - readmeButton.href = `https://github.com/RHEcosystemAppEng/agentic-collections/tree/main/${pack.name}`; + readmeButton.href = pack.repository || '#'; readmeButton.target = '_blank'; meta.appendChild(readmeButton); } @@ -884,7 +884,7 @@ function showPackDetails(packName) { const installPre = document.createElement('pre'); const installCode = document.createElement('code'); - installCode.textContent = `lola market add rh-agentic-collections https://raw.githubusercontent.com/RHEcosystemAppEng/agentic-collections/main/marketplace/rh-agentic-collection.yml + installCode.textContent = `lola market add rh-agentic-collections https://raw.githubusercontent.com/RHEcosystemAppEng/agentic-collections-catalog/main/marketplace/rh-agentic-collection.yml lola install -f ${moduleName}`; installPre.appendChild(installCode); codeWrapper.appendChild(installPre); @@ -1089,7 +1089,7 @@ lola install -f ${moduleName}`; docsLink.style.paddingTop = '1rem'; docsLink.style.borderTop = '1px solid var(--border)'; const link = document.createElement('a'); - link.href = `https://github.com/RHEcosystemAppEng/agentic-collections/tree/main/${pack.name}/docs`; + link.href = pack.repository || '#'; link.target = '_blank'; link.textContent = 'View full documentation on GitHub →'; link.style.color = 'var(--primary)'; @@ -2268,8 +2268,8 @@ function appendSkillEvalBlock(li, skill) { }; const commitSha = String(ev.commit_sha || '').trim(); const commitShort = commitSha ? commitSha.slice(0, 8) : 'N/A'; - const commitHref = commitSha - ? `https://github.com/RHEcosystemAppEng/agentic-collections/commit/${commitSha}` + const commitHref = commitSha && ev.repository + ? `${String(ev.repository).replace(/\/$/, '')}/commit/${commitSha}` : ''; const prUrl = String(ev.related_pr || '').trim(); const prMatch = prUrl.match(/\/pull\/(\d+)(?:\/|$)/); diff --git a/docs/index.html b/docs/index.html index 7ebec99..c89afb8 100644 --- a/docs/index.html +++ b/docs/index.html @@ -39,8 +39,8 @@

Red Hat Agentic Catalog

@@ -52,11 +52,11 @@

Red Hat Agentic Catalog

-
lola market add rh-agentic-collections https://raw.githubusercontent.com/RHEcosystemAppEng/agentic-collections/main/marketplace/rh-agentic-collection.yml
+
lola market add rh-agentic-collections https://raw.githubusercontent.com/RHEcosystemAppEng/agentic-collections-catalog/main/marketplace/rh-agentic-collection.yml

Then lola install -f <module> — applies to current folder only.

@@ -76,7 +76,7 @@

Red Hat Agentic Catalog

@@ -112,7 +112,7 @@

diff --git a/scripts/build_website.py b/scripts/build_website.py index b4e2769..dec3762 100644 --- a/scripts/build_website.py +++ b/scripts/build_website.py @@ -57,10 +57,10 @@ def build_website(): # Combine into final output output = { 'repository': { - 'name': 'agentic-collections', + 'name': 'agentic-collections-catalog', 'owner': 'Red Hat Ecosystem Engineering', - 'description': 'Agentic collections for Red Hat platforms and products', - 'url': 'https://github.com/RHEcosystemAppEng/agentic-collections' + 'description': 'Agentic collections catalog and website for Red Hat platforms and products', + 'url': 'https://github.com/RHEcosystemAppEng/agentic-collections-catalog' }, 'packs': pack_data, 'mcp_servers': mcp_data, diff --git a/scripts/check_site.py b/scripts/check_site.py index 87ab007..cd8282d 100755 --- a/scripts/check_site.py +++ b/scripts/check_site.py @@ -64,7 +64,7 @@ def print_checklist(): 1. Open http://localhost:8000 in your browser 2. Verify the header: - □ Title displays: "agentic-collections" + □ Title displays: "agentic-collections-catalog" □ Subtitle displays □ Search bar is visible diff --git a/scripts/eval_site_enrichment.py b/scripts/eval_site_enrichment.py new file mode 100644 index 0000000..580ba39 --- /dev/null +++ b/scripts/eval_site_enrichment.py @@ -0,0 +1,380 @@ +""" +Attach ABEvalFlow report summaries to catalog skills at site build time. + +Reads eval///report.json (latest only; not collection.yaml). +""" + +from __future__ import annotations + +import json +import statistics +from datetime import datetime +from pathlib import Path +from typing import Any, Dict, List, Optional + +PASS_RECOMMENDATION = "pass" + + +def _github_blob_url(repo_url: str, ref: str, rel_path: str) -> str: + base = f"{repo_url.rstrip('/')}/blob/{ref or 'main'}" + return f"{base}/{rel_path.lstrip('/')}" + + +def _parse_iso_max(a: str, b: str) -> str: + try: + da = datetime.fromisoformat(a.replace("Z", "+00:00")) + db = datetime.fromisoformat(b.replace("Z", "+00:00")) + except (ValueError, TypeError): + return a if a >= b else b + return a if da >= db else b + + +def _iso_to_datetime(iso_value: str) -> Optional[datetime]: + """Best-effort ISO timestamp parser for generated_at comparisons.""" + try: + return datetime.fromisoformat(str(iso_value).replace("Z", "+00:00")) + except (ValueError, TypeError): + return None + + +def _safe_int(value: Any, default: int = 0) -> int: + try: + if value is None: + return default + return int(value) + except (ValueError, TypeError): + return default + + +def _confidence_fields(n_treatment: int, n_control: int) -> Dict[str, str]: + n = min(n_treatment, n_control) + if n >= 20: + return { + "confidence_level": "HIGH", + "confidence_reason": f"{n} trials; strong signal strength", + } + if n >= 5: + return { + "confidence_level": "MEDIUM", + "confidence_reason": f"{n} trials; acceptable but can be improved", + } + return { + "confidence_level": "LOW", + "confidence_reason": f"Only {n} trial{'s' if n != 1 else ''}; recommended minimum is 5", + } + + +def _coverage_fields(n_treatment: int) -> Dict[str, str]: + if n_treatment <= 0: + return {"coverage_status": "NONE", "coverage_label": "Coverage: not evaluated"} + if n_treatment < 5: + return {"coverage_status": "PARTIAL", "coverage_label": f"Coverage: PARTIAL ({n_treatment} scenario{'s' if n_treatment != 1 else ''} tested)"} + if n_treatment < 20: + return {"coverage_status": "MODERATE", "coverage_label": f"Coverage: MODERATE ({n_treatment} scenarios tested)"} + return {"coverage_status": "STRONG", "coverage_label": f"Coverage: STRONG ({n_treatment} scenarios tested)"} + + +def load_eval_report( + root: Path, pack_name: str, skill_name: str, + repo_url: str = "", ref: str = "main", +) -> Optional[Dict[str, Any]]: + """ + Load and normalize eval///report.json from a cloned source repo. + repo_url and ref are used to build GitHub URLs pointing back to the source repo. + Returns None if file missing or invalid. + """ + base = root / "eval" / pack_name / skill_name + report_json = base / "report.json" + if not report_json.is_file(): + return None + try: + raw = json.loads(report_json.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError): + return None + + summary = raw.get("summary") or {} + treat = summary.get("treatment") or {} + ctrl = summary.get("control") or {} + prov = raw.get("provenance") or {} + trials = raw.get("trials") or {} + treatment_trials = trials.get("treatment") or [] + + n_trials_t = _safe_int(treat.get("n_trials")) + n_trials_c = _safe_int(ctrl.get("n_trials")) + n_failed_t = _safe_int(treat.get("n_failed")) + + conf = _confidence_fields(n_trials_t, n_trials_c) + cov = _coverage_fields(n_trials_t) + latest_trial = treatment_trials[-1] if treatment_trials else {} + latest_trial_name = latest_trial.get("trial_name") if isinstance(latest_trial, dict) else None + latest_trial_passed = latest_trial.get("passed") if isinstance(latest_trial, dict) else None + latest_trial_reward = latest_trial.get("reward") if isinstance(latest_trial, dict) else None + + rel_json = f"eval/{pack_name}/{skill_name}/report.json" + has_md = (root / "eval" / pack_name / skill_name / "report.md").is_file() + + out: Dict[str, Any] = { + "recommendation": str(summary.get("recommendation", "")).lower() or None, + "llm": summary.get("llm"), + "related_pr": summary.get("related_pr"), + "mean_reward_gap": summary.get("mean_reward_gap"), + "mean_reward_treatment": treat.get("mean_reward"), + "mean_reward_control": ctrl.get("mean_reward"), + "pass_rate_treatment": treat.get("pass_rate"), + "pass_rate_control": ctrl.get("pass_rate"), + "n_trials_treatment": treat.get("n_trials"), + "n_trials_control": ctrl.get("n_trials"), + "n_passed_treatment": treat.get("n_passed"), + "n_passed_control": ctrl.get("n_passed"), + "n_failed_treatment": treat.get("n_failed"), + "n_failed_control": ctrl.get("n_failed"), + "fisher_p_value": summary.get("fisher_p_value"), + "ttest_p_value": summary.get("ttest_p_value"), + "generated_at": prov.get("generated_at"), + "commit_sha": prov.get("commit_sha"), + "pipeline_run_id": prov.get("pipeline_run_id"), + "report_json_path": rel_json, + "has_report_md": has_md, + "scenarios_tested": n_trials_t, + "failed_trials": n_failed_t, + "has_failures": n_failed_t > 0, + "latest_trial_name": latest_trial_name, + "latest_trial_passed": latest_trial_passed, + "latest_trial_reward": latest_trial_reward, + **conf, + **cov, + } + out["repository"] = repo_url + out["report_json_url"] = _github_blob_url(repo_url, ref, rel_json) if repo_url else None + out["report_dir_url"] = _github_blob_url(repo_url, ref, f"eval/{pack_name}/{skill_name}") if repo_url else None + if has_md: + rel_md = f"eval/{pack_name}/{skill_name}/report.md" + out["report_md_path"] = rel_md + out["report_md_url"] = _github_blob_url(repo_url, ref, rel_md) if repo_url else None + else: + out["report_md_path"] = None + out["report_md_url"] = None + + return out + + +def _iter_catalog_skills(collection: Dict[str, Any]) -> List[Dict[str, Any]]: + contents = collection.get("contents") or {} + out: List[Dict[str, Any]] = [] + for key in ("orchestration_skills", "skills"): + for s in contents.get(key) or []: + if isinstance(s, dict): + out.append(s) + return out + + +def _rollup_from_evaluations( + evals: List[Optional[Dict[str, Any]]], catalog_skill_count: int +) -> Dict[str, Any]: + if catalog_skill_count == 0: + return { + "catalog_skill_count": 0, + "evaluated_count": 0, + "passed_count": 0, + "failed_count": 0, + "pass_rate": None, + "median_uplift": None, + "latest_generated_at": None, + "coverage_label": "0/0 skills evaluated", + "latest_pipeline_run_id": None, + "latest_report_json_url": None, + "latest_report_md_url": None, + "latest_mean_reward_treatment": None, + "latest_mean_reward_control": None, + "coverage_pct": 0.0, + "coverage_state": "NONE", + "confidence_level": "NONE", + "confidence_label": "NO DATA", + "verified_execution_count": 0, + "total_trials_treatment": 0, + "trust_summary": "No evaluation data", + "requires_more_evaluation": True, + } + + evaluated = [e for e in evals if e is not None] + n_eval = len(evaluated) + passed = sum( + 1 + for e in evaluated + if e.get("recommendation") == PASS_RECOMMENDATION + ) + failed = max(n_eval - passed, 0) + pass_rate = (passed / n_eval) if n_eval > 0 else None + + # ABEvalFlow no longer emits `uplift`; use mean_reward_gap as uplift proxy. + uplifts = [ + float(e["mean_reward_gap"]) + for e in evaluated + if e.get("mean_reward_gap") is not None + and isinstance(e["mean_reward_gap"], (int, float)) + ] + median_uplift = statistics.median(uplifts) if uplifts else None + + dates = [e["generated_at"] for e in evaluated if e.get("generated_at")] + latest = None + if dates: + latest = dates[0] + for d in dates[1:]: + latest = _parse_iso_max(str(latest), str(d)) + + latest_eval: Optional[Dict[str, Any]] = None + for ev in evaluated: + if latest_eval is None: + latest_eval = ev + continue + current_dt = _iso_to_datetime(str(ev.get("generated_at"))) + latest_dt = _iso_to_datetime(str(latest_eval.get("generated_at"))) + if current_dt and latest_dt: + if current_dt >= latest_dt: + latest_eval = ev + elif str(ev.get("generated_at") or "") >= str(latest_eval.get("generated_at") or ""): + latest_eval = ev + + coverage_label = f"{n_eval}/{catalog_skill_count} skills evaluated" + coverage_pct = (n_eval / catalog_skill_count * 100.0) if catalog_skill_count > 0 else 0.0 + if coverage_pct >= 95: + coverage_state = "FULL" + elif coverage_pct >= 50: + coverage_state = "PARTIAL" + elif coverage_pct > 0: + coverage_state = "LOW" + else: + coverage_state = "NONE" + + total_trials_treatment = sum( + _safe_int(e.get("n_trials_treatment")) for e in evaluated + ) + verified_execution_count = total_trials_treatment + if total_trials_treatment >= 20: + confidence_level = "HIGH" + elif total_trials_treatment >= 5: + confidence_level = "MEDIUM" + elif total_trials_treatment > 0: + confidence_level = "LOW" + else: + confidence_level = "NONE" + confidence_label = ( + "HIGH CONFIDENCE" + if confidence_level == "HIGH" + else ("MEDIUM CONFIDENCE" if confidence_level == "MEDIUM" else ("LOW CONFIDENCE" if confidence_level == "LOW" else "NO DATA")) + ) + requires_more_evaluation = coverage_pct < 50.0 or total_trials_treatment < 5 + trust_summary = ( + f"Based on {total_trials_treatment} trial{'s' if total_trials_treatment != 1 else ''} · {coverage_state.lower()} coverage" + if total_trials_treatment > 0 + else "No evaluation data" + ) + + return { + "catalog_skill_count": catalog_skill_count, + "evaluated_count": n_eval, + "passed_count": passed, + "failed_count": failed, + "pass_rate": pass_rate, + "median_uplift": median_uplift, + "latest_generated_at": latest, + "coverage_label": coverage_label, + "latest_pipeline_run_id": latest_eval.get("pipeline_run_id") if latest_eval else None, + "latest_report_json_url": latest_eval.get("report_json_url") if latest_eval else None, + "latest_report_md_url": latest_eval.get("report_md_url") if latest_eval else None, + "latest_mean_reward_treatment": latest_eval.get("mean_reward_treatment") if latest_eval else None, + "latest_mean_reward_control": latest_eval.get("mean_reward_control") if latest_eval else None, + "coverage_pct": round(coverage_pct, 1), + "coverage_state": coverage_state, + "confidence_level": confidence_level, + "confidence_label": confidence_label, + "verified_execution_count": verified_execution_count, + "total_trials_treatment": total_trials_treatment, + "trust_summary": trust_summary, + "requires_more_evaluation": requires_more_evaluation, + } + + +def apply_eval_enrichment(packs: List[Dict[str, Any]], root: Path) -> None: + """ + Mutate each pack: attach skill['evaluation'] for catalog-listed skills when + eval///report.json exists; set pack['evaluation_summary'] rollup. + """ + root = root.resolve() + for pack in packs: + coll = pack.get("collection") + if not isinstance(coll, dict): + continue + + skills_ref = _iter_catalog_skills(coll) + catalog_skill_count = len(skills_ref) + attached: List[Optional[Dict[str, Any]]] = [] + + pack_name = pack.get("name") or "" + repo_url = pack.get("repository") or "" + ref = pack.get("ref") or "main" + for skill in skills_ref: + name = skill.get("name") + if not name or not isinstance(name, str): + attached.append(None) + continue + ev = load_eval_report(root, pack_name, name, repo_url=repo_url, ref=ref) + if ev: + ev["catalog_skill_count"] = catalog_skill_count + skill["evaluation"] = ev + attached.append(ev) + else: + attached.append(None) + + pack["evaluation_summary"] = _rollup_from_evaluations(attached, catalog_skill_count) + + +def _self_test() -> None: + """Quick sanity check (run: python eval_site_enrichment.py).""" + import tempfile + + sample = { + "submission_name": "x", + "provenance": { + "generated_at": "2026-05-05T06:42:31.793527Z", + "commit_sha": "abc", + "pipeline_run_id": "run1", + }, + "summary": { + "treatment": {"n_trials": 5}, + "control": {"n_trials": 5}, + "mean_reward_gap": 0.05, + "ttest_p_value": None, + "fisher_p_value": 0.04, + "recommendation": "pass", + }, + } + with tempfile.TemporaryDirectory() as td: + root = Path(td) + p = root / "eval" / "rh-sre" / "remediation" + p.mkdir(parents=True) + (p / "report.json").write_text(json.dumps(sample), encoding="utf-8") + (p / "report.md").write_text("# ok", encoding="utf-8") + + ev = load_eval_report(root, "rh-sre", "remediation") + assert ev is not None + assert ev["mean_reward_gap"] == 0.05 + assert ev["has_report_md"] is True + + pack = { + "name": "rh-sre", + "collection": { + "contents": { + "skills": [{"name": "remediation", "description": "d"}], + "orchestration_skills": [], + } + }, + } + apply_eval_enrichment([pack], root) + assert pack["evaluation_summary"]["evaluated_count"] == 1 + assert pack["evaluation_summary"]["passed_count"] == 1 + print("eval_site_enrichment self-test OK") + + +if __name__ == "__main__": + _self_test() diff --git a/scripts/pack_registry.py b/scripts/pack_registry.py index c0f2bf6..ffd6f4c 100644 --- a/scripts/pack_registry.py +++ b/scripts/pack_registry.py @@ -66,8 +66,6 @@ def load_marketplace_module_by_path( return None -MAIN_REPO_URL = "https://github.com/RHEcosystemAppEng/agentic-collections" - FEDERATION_REF_SHA_RE = re.compile(r"^[0-9a-f]{40}$", re.IGNORECASE) @@ -104,7 +102,7 @@ def validate_federated_module_entry(module: Dict[str, Any]) -> List[str]: def load_federated_modules( marketplace_path: Optional[Path] = None, ) -> List[Dict[str, Any]]: - """Return modules whose repository differs from the main repo (federated packs).""" + """Return all marketplace modules that have an external repository.""" path = marketplace_path or (_repo_root() / DEFAULT_MARKETPLACE) if not path.exists(): return [] @@ -115,8 +113,7 @@ def load_federated_modules( return [] return [ m for m in modules - if isinstance(m, dict) - and m.get("repository", "").rstrip("/") != MAIN_REPO_URL + if isinstance(m, dict) and m.get("repository", "").strip() ] From fcd4884f16e64e50031d21a89e7902a83a49dc30 Mon Sep 17 00:00:00 2001 From: Daniele Martinoli Date: Fri, 26 Jun 2026 17:43:23 +0200 Subject: [PATCH 3/9] Refactor documentation and scripts for agentic-collections-catalog - Removed references to the 'catalog' directory in AGENTS.md and updated schema coordination instructions. - Adjusted GitHub Actions workflow to exclude 'catalog/**' path. - Deleted unused modal code from app.js and updated index.html title and section headers for consistency. - Revised README.md to reflect the new repository name and updated validation command. Signed-off-by: Daniele Martinoli --- .github/workflows/deploy-pages.yml | 1 - AGENTS.md | 4 +- docs/README.md | 4 +- docs/app.js | 469 ----------------------------- docs/index.html | 9 +- scripts/check_site.py | 4 +- 6 files changed, 6 insertions(+), 485 deletions(-) diff --git a/.github/workflows/deploy-pages.yml b/.github/workflows/deploy-pages.yml index 4857a6b..85e6430 100644 --- a/.github/workflows/deploy-pages.yml +++ b/.github/workflows/deploy-pages.yml @@ -6,7 +6,6 @@ on: branches: [main] paths: - 'marketplace/**' - - 'catalog/**' - 'scripts/**' - 'docs/**' - '.github/workflows/deploy-pages.yml' diff --git a/AGENTS.md b/AGENTS.md index 35da3a2..42916ba 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,8 +17,6 @@ This is the **catalog and marketplace** for Red Hat agentic collections. It serv 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) @@ -80,7 +78,7 @@ Do not add packs to `docs/data.json` or any other file directly. Add them to the - **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. +- **Schema changes need coordination.** Updating `.catalog/collection.yaml` in skills repos requires consistent field usage across all packs. - **Security.** All DOM manipulation in `app.js` uses `textContent` and `createElement` — never `innerHTML` with external data. ## CI diff --git a/docs/README.md b/docs/README.md index 87ea479..9264b23 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,6 @@ # Documentation Site -This directory contains the generated GitHub Pages site for agentic-collections. +This directory contains the generated GitHub Pages site for agentic-collections-catalog. ## Files @@ -42,7 +42,7 @@ When changing docs presentation metadata (`styles.css`, `app.js`, `mcp.json`): 2. Run validation checks: ```bash -make validate +make test ``` 3. Review validation output before merge. diff --git a/docs/app.js b/docs/app.js index 55e5c6c..0bf65af 100644 --- a/docs/app.js +++ b/docs/app.js @@ -737,475 +737,6 @@ function toggleInstallCallout() { toggle.setAttribute('aria-expanded', callout.classList.contains('collapsed') ? 'false' : 'true'); } -/** - * Show pack details modal (XSS-safe) - */ -function showPackDetails(packName) { - const pack = data.packs.find(p => p.name === packName); - if (!pack) return; - - const modal = document.getElementById('pack-modal'); - const details = document.getElementById('pack-details'); - - // Clear previous content - details.textContent = ''; - - // Create modal header - const header = document.createElement('div'); - header.className = 'modal-header'; - - // Close button - const closeBtn = document.createElement('span'); - closeBtn.className = 'close'; - closeBtn.textContent = '×'; - closeBtn.onclick = () => { - modal.style.display = 'none'; - document.body.style.overflow = ''; // Restore background scrolling - }; - header.appendChild(closeBtn); - - const headerTop = document.createElement('div'); - headerTop.className = 'modal-header-top'; - - const titleGroup = document.createElement('div'); - titleGroup.className = 'modal-title-group'; - - const h2 = document.createElement('h2'); - h2.textContent = pack.plugin?.title || pack.plugin?.name || pack.name; - titleGroup.appendChild(h2); - - // Owner subtitle - const ownerSubtitle = document.createElement('div'); - ownerSubtitle.className = 'pack-owner-subtitle'; - ownerSubtitle.textContent = 'By Red Hat'; - ownerSubtitle.style.color = 'var(--text-muted)'; - ownerSubtitle.style.fontSize = '0.95rem'; - ownerSubtitle.style.marginTop = '0.5rem'; - titleGroup.appendChild(ownerSubtitle); - - // Counts (skills + agents + MCPs) - const counts = document.createElement('div'); - counts.className = 'modal-counts'; - const countParts = []; - if (pack.skills.length > 0) { - countParts.push(`${pack.skills.length} skill${pack.skills.length !== 1 ? 's' : ''}`); - } - if (pack.agents.length > 0) { - countParts.push(`${pack.agents.length} agent${pack.agents.length !== 1 ? 's' : ''}`); - } - // Count both Official and Community MCPs for this pack - const packMCPCount = [...allMCPServers, ...allCommunityMCPServers].filter(server => server.pack === pack.name).length; - if (packMCPCount > 0) { - countParts.push(`${packMCPCount} MCP`); - } - counts.textContent = countParts.join(' '); - titleGroup.appendChild(counts); - - headerTop.appendChild(titleGroup); - - // Meta (version badge + readme button if available) - const meta = document.createElement('div'); - meta.className = 'modal-meta'; - - const versionBadge = document.createElement('span'); - versionBadge.className = 'version-badge'; - versionBadge.textContent = `v${pack.plugin.version || '0.0.0'}`; - meta.appendChild(versionBadge); - - if (pack.has_readme) { - const readmeButton = document.createElement('a'); - readmeButton.className = 'readme-button'; - readmeButton.textContent = 'README'; - readmeButton.href = pack.repository || '#'; - readmeButton.target = '_blank'; - meta.appendChild(readmeButton); - } - - headerTop.appendChild(meta); - header.appendChild(headerTop); - - // Description - if (pack.plugin.description) { - const desc = document.createElement('div'); - desc.className = 'modal-description'; - desc.textContent = pack.plugin.description; - header.appendChild(desc); - } - - // Plugin name field (if different from title) - if (pack.plugin.name && pack.plugin.name !== pack.plugin.title) { - const pluginNameDiv = document.createElement('div'); - pluginNameDiv.style.marginTop = '1rem'; - pluginNameDiv.style.fontSize = '0.9rem'; - pluginNameDiv.style.color = 'var(--text-muted)'; - - const label = document.createElement('strong'); - label.textContent = 'Plugin name: '; - label.style.color = 'var(--text-primary)'; - pluginNameDiv.appendChild(label); - - const nameCode = document.createElement('code'); - nameCode.textContent = pack.plugin.name; - nameCode.style.backgroundColor = JS_STYLE_TOKENS.subtleErrorBackground; - nameCode.style.padding = '0.25rem 0.5rem'; - nameCode.style.borderRadius = '4px'; - nameCode.style.fontSize = '0.85rem'; - pluginNameDiv.appendChild(nameCode); - - header.appendChild(pluginNameDiv); - } - - details.appendChild(header); - - // Create modal body - const body = document.createElement('div'); - body.className = 'modal-body'; - - // Installation section (Lola package manager) - const installSection = document.createElement('div'); - installSection.className = 'modal-section'; - - const installHeader = document.createElement('div'); - installHeader.className = 'modal-section-header'; - installHeader.textContent = 'INSTALLATION'; - installSection.appendChild(installHeader); - - const lolaNote = document.createElement('div'); - lolaNote.style.color = 'var(--text-muted)'; - lolaNote.style.fontSize = '0.85rem'; - lolaNote.style.marginBottom = '0.75rem'; - lolaNote.textContent = 'Lola. Applies to current folder only.'; - installSection.appendChild(lolaNote); - - const moduleName = pack.name; - - const codeWrapper = document.createElement('div'); - codeWrapper.className = 'install-code-wrapper'; - - const installPre = document.createElement('pre'); - const installCode = document.createElement('code'); - installCode.textContent = `lola market add rh-agentic-collections https://raw.githubusercontent.com/RHEcosystemAppEng/agentic-collections-catalog/main/marketplace/rh-agentic-collection.yml -lola install -f ${moduleName}`; - installPre.appendChild(installCode); - codeWrapper.appendChild(installPre); - - const copyBtn = document.createElement('button'); - copyBtn.className = 'copy-button'; - copyBtn.textContent = 'Copy'; - copyBtn.onclick = () => copyToClipboard(installCode.textContent, copyBtn); - codeWrapper.appendChild(copyBtn); - - installSection.appendChild(codeWrapper); - - const optNote = document.createElement('div'); - optNote.style.color = 'var(--text-muted)'; - optNote.style.fontSize = '0.8rem'; - optNote.style.marginTop = '0.5rem'; - optNote.textContent = 'Add -a claude-code or -a cursor to target one assistant.'; - installSection.appendChild(optNote); - - const lolaLink = document.createElement('div'); - lolaLink.style.marginTop = '0.5rem'; - lolaLink.style.fontSize = '0.85rem'; - const lolaAnchor = document.createElement('a'); - lolaAnchor.href = 'https://github.com/LobsterTrap/lola'; - lolaAnchor.target = '_blank'; - lolaAnchor.textContent = 'Lola →'; - lolaAnchor.style.color = 'var(--primary)'; - lolaAnchor.style.textDecoration = 'none'; - lolaAnchor.onmouseover = () => { lolaAnchor.style.textDecoration = 'underline'; }; - lolaAnchor.onmouseout = () => { lolaAnchor.style.textDecoration = 'none'; }; - lolaLink.appendChild(lolaAnchor); - installSection.appendChild(lolaLink); - - body.appendChild(installSection); - - // Agents section (shown first) - if (pack.agents.length > 0) { - const agentsSection = document.createElement('div'); - agentsSection.className = 'modal-section'; - - const agentsHeader = document.createElement('div'); - agentsHeader.className = 'modal-section-header'; - agentsHeader.textContent = 'AGENTS'; - agentsSection.appendChild(agentsHeader); - - const agentsList = document.createElement('div'); - agentsList.className = 'item-list'; - - pack.agents.forEach(agent => { - const agentDef = document.createElement('div'); - agentDef.className = 'agent-definition'; - - // Agent syntax block - const syntaxBlock = document.createElement('div'); - syntaxBlock.className = 'definition-syntax'; - const syntaxCode = document.createElement('code'); - syntaxCode.textContent = agent.name; - syntaxBlock.appendChild(syntaxCode); - agentDef.appendChild(syntaxBlock); - - // Agent description (with expand/collapse for long text) - const desc = document.createElement('div'); - desc.className = 'definition-description'; - desc.appendChild(createExpandableText(agent.description, 200)); - agentDef.appendChild(desc); - - agentsList.appendChild(agentDef); - }); - - agentsSection.appendChild(agentsList); - body.appendChild(agentsSection); - } - - // Skills section (shown second) - if (pack.skills.length > 0) { - const skillsSection = document.createElement('div'); - skillsSection.className = 'modal-section'; - - const skillsHeader = document.createElement('div'); - skillsHeader.className = 'modal-section-header'; - skillsHeader.textContent = 'SKILLS'; - skillsSection.appendChild(skillsHeader); - - const skillsList = document.createElement('div'); - skillsList.className = 'item-list'; - - pack.skills.forEach(skill => { - const skillDef = document.createElement('div'); - skillDef.className = 'skill-definition'; - - // Skill syntax block - const syntaxBlock = document.createElement('div'); - syntaxBlock.className = 'definition-syntax'; - const syntaxCode = document.createElement('code'); - syntaxCode.textContent = skill.name; - syntaxBlock.appendChild(syntaxCode); - skillDef.appendChild(syntaxBlock); - - // Skill description (with expand/collapse for long text) - const desc = document.createElement('div'); - desc.className = 'definition-description'; - desc.appendChild(createExpandableText(skill.description, 200)); - skillDef.appendChild(desc); - - skillsList.appendChild(skillDef); - }); - - skillsSection.appendChild(skillsList); - body.appendChild(skillsSection); - } - - // Docs section (shown third, if documentation exists) - if (pack.docs && pack.docs.length > 0) { - const docsSection = document.createElement('div'); - docsSection.className = 'modal-section'; - - const docsHeader = document.createElement('div'); - docsHeader.className = 'modal-section-header'; - docsHeader.textContent = 'DOCS'; - docsSection.appendChild(docsHeader); - - // Group docs by category - const docsByCategory = {}; - pack.docs.forEach(doc => { - const category = doc.category || 'general'; - if (!docsByCategory[category]) { - docsByCategory[category] = []; - } - docsByCategory[category].push(doc); - }); - - // Render each category - Object.keys(docsByCategory).sort().forEach(category => { - const categorySection = document.createElement('div'); - categorySection.style.marginBottom = '1.5rem'; - - // Category header - const categoryHeader = document.createElement('div'); - categoryHeader.className = 'modal-section-label'; - categoryHeader.textContent = category.charAt(0).toUpperCase() + category.slice(1); - categorySection.appendChild(categoryHeader); - - // Doc list for this category - const docsList = document.createElement('div'); - docsList.className = 'item-list'; - - docsByCategory[category].forEach(doc => { - const docDef = document.createElement('div'); - docDef.className = 'skill-definition'; - - // Doc title - const titleBlock = document.createElement('div'); - titleBlock.className = 'definition-syntax'; - const titleCode = document.createElement('code'); - titleCode.textContent = doc.title; - titleBlock.appendChild(titleCode); - docDef.appendChild(titleBlock); - - // Sources section - if (doc.sources && doc.sources.length > 0) { - const sourcesDiv = document.createElement('div'); - sourcesDiv.className = 'definition-description'; - - doc.sources.forEach((source, index) => { - // Add separator between sources - if (index > 0) { - sourcesDiv.appendChild(document.createElement('br')); - } - - // Source title label - const sourceLabel = document.createElement('span'); - sourceLabel.textContent = 'Source: '; - sourceLabel.style.color = 'var(--text-muted)'; - sourceLabel.style.fontSize = '0.85rem'; - sourcesDiv.appendChild(sourceLabel); - - // Source link - const sourceLink = document.createElement('a'); - sourceLink.href = source.url; - sourceLink.target = '_blank'; - sourceLink.textContent = source.title; - sourceLink.style.color = 'var(--primary)'; - sourceLink.style.textDecoration = 'none'; - sourceLink.onmouseover = () => { sourceLink.style.textDecoration = 'underline'; }; - sourceLink.onmouseout = () => { sourceLink.style.textDecoration = 'none'; }; - sourcesDiv.appendChild(sourceLink); - }); - - docDef.appendChild(sourcesDiv); - } - - docsList.appendChild(docDef); - }); - - categorySection.appendChild(docsList); - docsSection.appendChild(categorySection); - }); - - // Link to full documentation on GitHub - const docsLink = document.createElement('div'); - docsLink.style.marginTop = '1.5rem'; - docsLink.style.paddingTop = '1rem'; - docsLink.style.borderTop = '1px solid var(--border)'; - const link = document.createElement('a'); - link.href = pack.repository || '#'; - link.target = '_blank'; - link.textContent = 'View full documentation on GitHub →'; - link.style.color = 'var(--primary)'; - link.style.textDecoration = 'none'; - link.style.fontWeight = '600'; - link.onmouseover = () => { link.style.textDecoration = 'underline'; }; - link.onmouseout = () => { link.style.textDecoration = 'none'; }; - docsLink.appendChild(link); - docsSection.appendChild(docsLink); - - body.appendChild(docsSection); - } - - // MCP section (shown last, if MCP servers exist for this pack) - // Include both Official and Community MCP servers for this pack - const packMCPServers = [...allMCPServers, ...allCommunityMCPServers].filter(server => server.pack === pack.name); - if (packMCPServers.length > 0) { - const mcpSection = document.createElement('div'); - mcpSection.className = 'modal-section'; - - const mcpHeader = document.createElement('div'); - mcpHeader.className = 'modal-section-header'; - mcpHeader.textContent = 'MCP'; - mcpSection.appendChild(mcpHeader); - - const mcpList = document.createElement('div'); - mcpList.className = 'item-list'; - - packMCPServers.forEach(server => { - const mcpDef = document.createElement('div'); - mcpDef.className = 'skill-definition mcp-link-item'; - mcpDef.style.cursor = 'pointer'; - mcpDef.style.transition = 'all 0.2s ease'; - mcpDef.style.borderLeft = '3px solid transparent'; - mcpDef.style.paddingLeft = '1rem'; - - // MCP server name with icon and arrow (clickable) - const nameBlock = document.createElement('div'); - nameBlock.className = 'definition-syntax'; - nameBlock.style.display = 'flex'; - nameBlock.style.alignItems = 'center'; - nameBlock.style.gap = '0.5rem'; - nameBlock.style.justifyContent = 'space-between'; - - const nameGroup = document.createElement('div'); - nameGroup.style.display = 'flex'; - nameGroup.style.alignItems = 'center'; - nameGroup.style.gap = '0.5rem'; - - // Custom icon (if available) - if (server.icon) { - nameGroup.appendChild(createIconElement(server.icon, 'item-icon')); - } - - const nameCode = document.createElement('code'); - nameCode.textContent = server.title || server.name; - nameCode.style.color = 'var(--primary)'; - nameGroup.appendChild(nameCode); - - // HTTP remote indicator (if applicable) - if (server.type === 'http') { - const icon = document.createElement('span'); - icon.textContent = '🌐'; - icon.title = 'HTTP Remote Server'; - icon.style.fontSize = '0.9rem'; - nameGroup.appendChild(icon); - } - - nameBlock.appendChild(nameGroup); - - // Add arrow indicator - const arrow = document.createElement('span'); - arrow.textContent = '→'; - arrow.style.color = 'var(--text-muted)'; - arrow.style.fontSize = '1.2rem'; - arrow.style.transition = 'transform 0.2s ease'; - nameBlock.appendChild(arrow); - - mcpDef.appendChild(nameBlock); - - // MCP owner subtitle - const ownerText = document.createElement('div'); - ownerText.className = 'definition-description'; - ownerText.textContent = `By ${server.owner || 'Red Hat'}`; - ownerText.style.color = 'var(--text-muted)'; - ownerText.style.fontSize = '0.85rem'; - ownerText.style.marginTop = '0.25rem'; - mcpDef.appendChild(ownerText); - - // Hover effects - mcpDef.onmouseenter = () => { - mcpDef.style.borderLeftColor = 'var(--primary)'; - mcpDef.style.backgroundColor = JS_STYLE_TOKENS.softAccentBackground; - arrow.style.transform = 'translateX(4px)'; - }; - - mcpDef.onmouseleave = () => { - mcpDef.style.borderLeftColor = 'transparent'; - mcpDef.style.backgroundColor = 'transparent'; - arrow.style.transform = 'translateX(0)'; - }; - - // Click to open MCP details - mcpDef.onclick = () => showMCPDetails(server.name, server.pack); - - mcpList.appendChild(mcpDef); - }); - - mcpSection.appendChild(mcpList); - body.appendChild(mcpSection); - } - - details.appendChild(body); - modal.style.display = 'flex'; - document.body.style.overflow = 'hidden'; // Prevent background scrolling -} /** * Show MCP server details modal (XSS-safe) diff --git a/docs/index.html b/docs/index.html index c89afb8..60ae320 100644 --- a/docs/index.html +++ b/docs/index.html @@ -3,7 +3,7 @@ - Agentic Skills packs + Red Hat Agentic Catalog @@ -86,7 +86,7 @@

Red Hat Agentic Catalog

- Agentic Skills packs + Agentic Collections

Ready-to-use capability packs designed for Claude Code and Cursor to automate complex domain-specific tasks.

@@ -115,11 +115,6 @@

Generated from the agentic-collections-catalog repository on GitHub.

- - -