diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json
index 12970656c..ae4a0eb63 100644
--- a/.claude-plugin/marketplace.json
+++ b/.claude-plugin/marketplace.json
@@ -88,7 +88,7 @@
"name": "ci",
"source": "./plugins/ci",
"description": "A plugin to work with OpenShift CI and analyze Prow job results",
- "version": "0.0.55",
+ "version": "0.0.56",
"category": "ci",
"keywords": [
"prow",
diff --git a/.github/workflows/lint-plugins.yml b/.github/workflows/lint-plugins.yml
index b98a7f5d3..64505c3d1 100644
--- a/.github/workflows/lint-plugins.yml
+++ b/.github/workflows/lint-plugins.yml
@@ -19,6 +19,7 @@ jobs:
persist-credentials: false
- name: Run skillsaw
- uses: stbenjam/skillsaw@3e1188f446413e6d6818c98644d2d6a84e4038e7 # v0.14.1
+ uses: stbenjam/skillsaw@a1d7340ef19cd0949f884a5381155843589dbad8 # v0.15.0
with:
strict: true
+ no-custom-rules: false
diff --git a/.github/workflows/lint-review.yml b/.github/workflows/lint-review.yml
index 0c7f74506..e23ecc1df 100644
--- a/.github/workflows/lint-review.yml
+++ b/.github/workflows/lint-review.yml
@@ -25,4 +25,4 @@ jobs:
persist-credentials: false
- name: Post review comments
- uses: stbenjam/skillsaw/review@3e1188f446413e6d6818c98644d2d6a84e4038e7 # v0.14.1
+ uses: stbenjam/skillsaw/review@a1d7340ef19cd0949f884a5381155843589dbad8 # v0.15.0
diff --git a/.skillsaw.yaml b/.skillsaw.yaml
index 9ccb6afc3..fd5bdefc3 100644
--- a/.skillsaw.yaml
+++ b/.skillsaw.yaml
@@ -1,6 +1,6 @@
# skillsaw configuration
# https://github.com/stbenjam/skillsaw
-version: "0.14.1"
+version: "0.15.0"
strict: true
rules:
@@ -111,6 +111,7 @@ custom-rules:
# Use exclude: [] to disable all excludes including defaults
exclude:
- "node_modules/**"
+ - "**/__pycache__/**"
- "**/template/**"
- "**/templates/**"
- "**/_template/**"
diff --git a/Makefile b/Makefile
index 3aa19ccc3..02c324059 100644
--- a/Makefile
+++ b/Makefile
@@ -4,7 +4,7 @@
CONTAINER_RUNTIME ?= $(shell command -v podman 2>/dev/null || echo docker)
# skillsaw image
-SKILLSAW_IMAGE = ghcr.io/stbenjam/skillsaw:0.14.1
+SKILLSAW_IMAGE = ghcr.io/stbenjam/skillsaw:0.15.0
# Detect if SELinux is enforcing and add security option
SELINUX_OPT := $(shell if command -v getenforce >/dev/null 2>&1 && [ "$$(getenforce 2>/dev/null)" = "Enforcing" ]; then echo "--security-opt label=disable"; fi)
diff --git a/docs/index.html b/docs/index.html
index f0675747a..3a0036d83 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -512,7 +512,7 @@
ai-helpers
{
"name": "ci",
"description": "Tools for working with OpenShift CI and analyzing Prow job results",
- "version": "0.0.55",
+ "version": "0.0.56",
"has_readme": true,
"commands": [
{
diff --git a/plugins/ci/.claude-plugin/plugin.json b/plugins/ci/.claude-plugin/plugin.json
index b86bc1f71..d1d26b339 100644
--- a/plugins/ci/.claude-plugin/plugin.json
+++ b/plugins/ci/.claude-plugin/plugin.json
@@ -1,7 +1,7 @@
{
"name": "ci",
"description": "Tools for working with OpenShift CI and analyzing Prow job results",
- "version": "0.0.55",
+ "version": "0.0.56",
"author": {
"name": "github.com/openshift-eng"
}
diff --git a/plugins/ci/skills/payload-autodl-json/SKILL.md b/plugins/ci/skills/payload-autodl-json/SKILL.md
index 70ce752c0..bcbead421 100644
--- a/plugins/ci/skills/payload-autodl-json/SKILL.md
+++ b/plugins/ci/skills/payload-autodl-json/SKILL.md
@@ -213,6 +213,14 @@ After staging reverts, find rows matching `candidate_pr_url` and set:
- **PASS** (confirmed cause): `revert_pr_status`: `"open"`
- **FAIL** (innocent): `revert_pr_url`: `""`, `revert_pr_status`: `""` (draft was closed)
+## Validation
+
+Use [scripts/validate.py](scripts/validate.py) to check your file against the canonical schema:
+
+```bash
+python3 scripts/validate.py
+```
+
## See Also
- Related Skill: `payload-analysis` — creates this file in Step 8
diff --git a/plugins/ci/skills/payload-results-yaml/SKILL.md b/plugins/ci/skills/payload-results-yaml/SKILL.md
index eaca210e9..06a57120e 100644
--- a/plugins/ci/skills/payload-results-yaml/SKILL.md
+++ b/plugins/ci/skills/payload-results-yaml/SKILL.md
@@ -214,6 +214,14 @@ For a given candidate's action entry (matched by `pr_url` and `type`), update it
Scan all candidates. If any candidate has an action with `type: "experiment"` and `status: "pending"`, the file has in-progress experiments awaiting Phase 2 collection. Phase 2 processes only pending experiments — candidates with other statuses are left unchanged.
+## Validation
+
+Use [scripts/validate.py](scripts/validate.py) to check your file against the canonical schema:
+
+```bash
+python3 scripts/validate.py
+```
+
## See Also
- Related Skill: `payload-analysis` — creates the results file
diff --git a/plugins/ci/skills/prow-job-analyze-resource/SKILL.md b/plugins/ci/skills/prow-job-analyze-resource/SKILL.md
index 7b02c2bae..7d78b96b6 100644
--- a/plugins/ci/skills/prow-job-analyze-resource/SKILL.md
+++ b/plugins/ci/skills/prow-job-analyze-resource/SKILL.md
@@ -579,6 +579,7 @@ Output:
- Supports regex patterns for resource matching
- `generate_html_report.py` - Generates interactive HTML report from JSON
- Scripts output status messages to stderr for progress display. JSON output to stdout is clean.
+ - See [SCRIPTS.md](SCRIPTS.md) for detailed documentation of every script.
5. **Pod Log Glog Format Support:**
- The parser automatically detects and parses glog format logs
diff --git a/plugins/ci/skills/prow-job-analyze-resource/create_inline_html_files.py b/plugins/ci/skills/prow-job-analyze-resource/create_inline_html_files.py
deleted file mode 100644
index a75e21e3c..000000000
--- a/plugins/ci/skills/prow-job-analyze-resource/create_inline_html_files.py
+++ /dev/null
@@ -1,333 +0,0 @@
-#!/usr/bin/env python3
-"""Create HTML files with line numbers for inline viewing."""
-
-import os
-import sys
-import hashlib
-import html as html_module
-from pathlib import Path
-
-
-def create_html_files_for_logs(logs_dir, build_id):
- """Create .html files with line numbers for log files under 1MB."""
- MAX_INLINE_SIZE = 1 * 1024 * 1024 # 1MB
- links_dir = os.path.join(logs_dir, '_links')
-
- # Create _links directory if it doesn't exist
- os.makedirs(links_dir, exist_ok=True)
-
- html_count = 0
- file_mapping = {} # Map from original path to HTML path
-
- # Walk through all log files
- for root, dirs, filenames in os.walk(logs_dir):
- # Skip the _links directory itself
- if '_links' in root:
- continue
-
- for filename in filenames:
- file_path = os.path.join(root, filename)
-
- try:
- # Get file size
- size = os.path.getsize(file_path)
-
- if size < MAX_INLINE_SIZE:
- # Get relative path from logs_dir
- rel_path = os.path.relpath(file_path, logs_dir)
-
- # Generate unique HTML name by hashing the full path
- path_hash = hashlib.md5(rel_path.encode()).hexdigest()[:8]
- html_name = f"{filename}.{path_hash}.html"
- html_path = os.path.join(links_dir, html_name)
-
- # Read original file content
- with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
- content = f.read()
-
- # Split into lines and add line numbers
- lines = content.split('\n')
- line_count = len(lines)
- line_number_width = len(str(line_count))
-
- # Build content with line numbers
- numbered_lines = []
- for i, line in enumerate(lines, 1):
- escaped_line = html_module.escape(line)
- line_num = str(i).rjust(line_number_width)
- numbered_lines.append(f'{line_num} {escaped_line}')
-
- numbered_content = '\n'.join(numbered_lines)
-
- # Wrap in HTML
- html_content = f'''
-
-
-
- {html_module.escape(filename)}
-
-
-
-
-
-
-
-
-
Invalid regex pattern
-
-
-
-
-'''
-
- # Write HTML file
- with open(html_path, 'w', encoding='utf-8') as f:
- f.write(html_content)
-
- # Store mapping
- rel_html_path = f"logs/_links/{html_name}"
- file_mapping[rel_path] = rel_html_path
- html_count += 1
-
- except Exception as e:
- print(f"WARNING: Could not create HTML for {file_path}: {e}", file=sys.stderr)
-
- print(f"Created {html_count} .html files for inline viewing", file=sys.stderr)
- return file_mapping
-
-
-def main():
- if len(sys.argv) < 3:
- print("Usage: create_inline_html_files.py ")
- sys.exit(1)
-
- logs_dir = sys.argv[1]
- build_id = sys.argv[2]
-
- if not os.path.exists(logs_dir):
- print(f"ERROR: Logs directory not found: {logs_dir}", file=sys.stderr)
- sys.exit(1)
-
- file_mapping = create_html_files_for_logs(logs_dir, build_id)
-
- # Output mapping as JSON for use by other scripts
- import json
- print(json.dumps(file_mapping))
-
-
-if __name__ == '__main__':
- main()
diff --git a/plugins/jira/skills/status-analysis/SKILL.md b/plugins/jira/skills/status-analysis/SKILL.md
index bf3b18866..5e4cef4ec 100644
--- a/plugins/jira/skills/status-analysis/SKILL.md
+++ b/plugins/jira/skills/status-analysis/SKILL.md
@@ -102,6 +102,9 @@ This skill is composed of four sub-modules. Read each when executing the analysi
| External Links | `external-links.md` | GitHub PR and GitLab MR integration |
| Formatting | `formatting.md` | Output templates for different modes |
| Data Gatherer | `scripts/gather_status_data.py` | Async batch data collection (update-weekly-status) |
+| Issue Summarizer | `scripts/summarize_issue.py` | Structured per-issue summaries for LLM analysis (update-weekly-status, generate-feature-updates) |
+| Issue Triage | `scripts/triage_issues.py` | Classifies issues as significant or skippable by activity and R/Y/G status (generate-feature-updates) |
+| Update Validator | `scripts/validate_feature_updates.py` | Validates generated feature-update markdown and HTML (generate-feature-updates) |
## Configuration Parameters
diff --git a/plugins/must-gather/skills/must-gather-analyzer/SKILL.md b/plugins/must-gather/skills/must-gather-analyzer/SKILL.md
index f12f23b00..de3184400 100644
--- a/plugins/must-gather/skills/must-gather-analyzer/SKILL.md
+++ b/plugins/must-gather/skills/must-gather-analyzer/SKILL.md
@@ -189,6 +189,22 @@ Shows monitoring information:
- Alerts (state, namespace, name, active since, labels)
- Total of pending/firing alerts
+#### OVN Database Analysis
+```bash
+./scripts/analyze_ovn_dbs.py
+```
+
+Analyzes OVN Northbound and Southbound databases collected per-node under
+`network_logs/` (requires `ovsdb-tool`).
+
+#### Windows Node Logs Analysis
+```bash
+./scripts/analyze_windows_logs.py
+```
+
+Parses Windows node logs from `host_service_logs/windows/log_files/` and
+identifies issues.
+
### 3. Interpret and Report
After running the scripts:
diff --git a/plugins/olm-team/skills/k8s-ocp-olm-expert/SKILL.md b/plugins/olm-team/skills/k8s-ocp-olm-expert/SKILL.md
index ea65c8056..cc4b70b23 100644
--- a/plugins/olm-team/skills/k8s-ocp-olm-expert/SKILL.md
+++ b/plugins/olm-team/skills/k8s-ocp-olm-expert/SKILL.md
@@ -15,6 +15,10 @@ You are an elite software engineer with deep, specialized expertise in Kubernete
The configuration file should be located at: `~/.config/claude-code/olm-agent-config.json`
+The expected structure is defined by [config-template.json](config-template.json)
+(JSON schema) with a filled-in example in [config-example.json](config-example.json);
+`/olm-team:configure-agent` creates the file from these.
+
```bash
# Check if configuration file exists
if [ -f ~/.config/claude-code/olm-agent-config.json ]; then