Skip to content

[ROB-3715] - Skills - #1953

Merged
Avi-Robusta merged 15 commits into
masterfrom
skills
Apr 27, 2026
Merged

Avi-Robusta merged 15 commits into
masterfrom
skills

Conversation

@Avi-Robusta

@Avi-Robusta Avi-Robusta commented Apr 26, 2026 •

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Introduced a Skills system: catalog loader, SKILL.md with YAML front-matter, and a fetch_skill tool for retrieving/executing skills.
  • Refactor

    • Replaced “runbooks” surfaces with “skills” across prompts, tooling, configs and toolsets; config key renamed to custom_skill_paths.
  • Documentation

    • Added Skills reference and READMEs; removed legacy runbooks docs.
  • Tests

    • Updated and added tests/fixtures for skills flows.
  • Chore

    • Updated ignore rules to exclude local Claude settings.

Avi-Robusta and others added 5 commits April 23, 2026 09:25
Initial refactor renaming "runbook" to "skills" across the codebase
by name only — no format or logic changes.

- Rename toolset directory: runbook/ -> skills/
- Rename classes: RunbookFetcher -> SkillsFetcher, RunbookToolset -> SkillsToolset
- Toolset name: "runbook" -> "skills" with deprecation mapping
- Update all imports and references
- Rename 10 eval fixture directories
- Update test_case.yaml: runbooks -> skills field, tags
- Update pyproject.toml tag: runbooks -> skills
- Preserve: fetch_runbook tool name, prompt templates, config fields

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: avi@robusta.dev <avi@robusta.dev>
- Create holmes/plugins/skills/skill_loader.py with Skill, SkillCatalog,
  SkillSource models and SKILL.md parsing/scanning/loading
- Refactor SkillsFetcher to use SkillCatalog instead of catalog.json
- Add metadata field to StructuredToolResult for restricted tool gating
- Replace custom_runbook_catalogs with custom_skill_paths in config
- Replace get_runbook_catalog() with get_skill_catalog()
- Update prompt system types: RunbookCatalog -> SkillCatalog
- Migrate 6 eval fixtures to SKILL.md format with path-based loading
- Rename runbook -> skills in 5 toolsets.yaml fixtures
- Delete tests/plugins/runbooks/ (old catalog tests)

Known bug: some eval toolsets.yaml files may still reference old
'runbook' toolset name - needs grep for remaining occurrences.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: avi@robusta.dev <avi@robusta.dev>
Missed in Step 2 - this was causing eval tests to fail with
'Toolset runbook does not exist' since the toolset was renamed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: avi@robusta.dev <avi@robusta.dev>
Complete rename of "runbook" to "skill" in all user-facing and
internal code:

Prompt templates:
- _runbook_instructions.jinja2 -> _skill_instructions.jinja2
- _runbooks_instructions.jinja2 -> _skills_instructions.jinja2
- All template content: runbook -> skill, fetch_runbook -> fetch_skill

Tool rename:
- fetch_runbook -> fetch_skill (tool name)
- runbook_id -> skill_id (parameter)
- <runbook> -> <skill> (XML tags in tool output)

Python code:
- PromptComponent.TIME_RUNBOOKS -> TIME_SKILLS
- generate_runbooks_args -> generate_skills_args
- runbook_catalog -> skill_catalog (context variables)
- runbooks_enabled -> skills_enabled (template variables)
- _should_enable_runbooks -> _should_enable_skills
- RobustaRunbookInstruction -> RobustaSkillInstruction
- get_runbook_catalog -> get_skill_catalog (DAL method)
- get_runbook_content -> get_skill_content (DAL method)
- _runbook_in_use -> _skill_in_use
- Cleaned dead code from holmes/plugins/runbooks/__init__.py

Tests and evals updated to match all renames.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: avi@robusta.dev <avi@robusta.dev>
Resolved conflicts:
- tool_calling_llm.py: kept OAuth tool discovery from master + our
  skill rename
- tools.py: kept both metadata field (ours) and oauth_tools field
  (master)

No new runbook references introduced by master.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: avi@robusta.dev <avi@robusta.dev>

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.

Tip: disable this comment in your organization's Code Review settings.

@github-actions

github-actions Bot commented Apr 26, 2026 •

Copy link
Copy Markdown
Contributor

✅ Docker images ready for d9db68f1 (built in 57s)

⚠️ Warning: does not support ARM (ARM images are built on release only - not on every PR)

Use these tags to pull the images for testing.

📋 Copy commands

⚠️ Temporary images are deleted after 30 days. Copy to a permanent registry before using them:

gcloud auth configure-docker us-central1-docker.pkg.dev
docker pull us-central1-docker.pkg.dev/robusta-development/temporary-builds/holmes:d9db68f1
docker tag us-central1-docker.pkg.dev/robusta-development/temporary-builds/holmes:d9db68f1 me-west1-docker.pkg.dev/robusta-development/development/holmes-dev:d9db68f1
docker push me-west1-docker.pkg.dev/robusta-development/development/holmes-dev:d9db68f1
docker pull us-central1-docker.pkg.dev/robusta-development/temporary-builds/holmes-operator:d9db68f1
docker tag us-central1-docker.pkg.dev/robusta-development/temporary-builds/holmes-operator:d9db68f1 me-west1-docker.pkg.dev/robusta-development/development/holmes-operator-dev:d9db68f1
docker push me-west1-docker.pkg.dev/robusta-development/development/holmes-operator-dev:d9db68f1

Patch Helm values in one line (choose the chart you use):

HolmesGPT chart:

helm upgrade --install holmesgpt ./helm/holmes \
  --set registry=me-west1-docker.pkg.dev/robusta-development/development \
  --set image=holmes-dev:d9db68f1 \
  --set operator.registry=me-west1-docker.pkg.dev/robusta-development/development \
  --set operator.image=holmes-operator-dev:d9db68f1

Robusta wrapper chart:

helm upgrade --install robusta robusta/robusta \
  --reuse-values \
  --set holmes.registry=me-west1-docker.pkg.dev/robusta-development/development \
  --set holmes.image=holmes-dev:d9db68f1 \
  --set holmes.operator.registry=me-west1-docker.pkg.dev/robusta-development/development \
  --set holmes.operator.image=holmes-operator-dev:d9db68f1

@github-actions

github-actions Bot commented Apr 26, 2026 •

Copy link
Copy Markdown
Contributor

📂 Previous Runs

📜 #5 · Run @ __69f55c5__ (#24991589472) — Apr 27, 11:16 UTC

✅ Results of HolmesGPT evals

Automatically triggered by commit 69f55c5 on branch skills

View workflow logs

Results of HolmesGPT evals

  • ask_holmes: 11/11 test cases were successful, 0 regressions
Status Test case Time Turns Tools Cost Total tokens Input Max input Output Max output Cached Non-cached Reasoning Compactions
✅ 09_crashpod 34.9s 5 10 $0.2364 102,626 100,697 23,062 1,929 755 76,473 24,224 — —
✅ 101_loki_historical_logs_pod_deleted 52.3s 6 12 $0.2901 136,591 133,762 25,876 2,829 872 107,073 26,689 — —
✅ 112_find_pvcs_by_uuid 17.8s 3 3 $0.1772 60,303 59,341 21,472 962 550 37,858 21,483 — —
✅ 12_job_crashing 37.5s 5 12 $0.2568 110,749 108,737 24,542 2,012 592 81,756 26,981 — —
✅ 176_network_policy_blocking_traffic_no_skills 44.4s 6 14 $0.2873 132,796 130,148 26,249 2,648 731 102,763 27,385 — —
✅ 227_count_configmaps_per_namespace[0] 23.4s 4 9 $0.1892 77,304 76,062 20,883 1,242 585 55,167 20,895 — —
✅ 243_pod_names_contain_service 38.1s 5 9 $0.2273 100,817 98,874 22,250 1,943 580 76,313 22,561 — —
✅ 24_misconfigured_pvc 40.6s 6 14 $0.2653 124,506 122,063 23,784 2,443 894 96,855 25,208 — —
✅ 43_current_datetime_from_prompt 5.8s 1 — $0.1105 17,099 16,907 16,907 192 192 0 16,907 — —
✅ 51_logs_summarize_errors 25.4s 4 5 $0.1810 76,507 75,532 20,661 975 302 54,859 20,673 — —
✅ 61_exact_match_counting 8.8s 2 1 $0.1231 34,483 34,226 17,310 257 189 16,906 17,320 — —
Total 29.9s avg 4.3 avg 8.9 avg $2.3442 973,781 956,349 26,249 17,432 894 706,023 250,326 — —
Benchmark Comparison Details

Baseline: latest ci-benchmark experiment on master

Status: Success - 74 test/model combinations loaded

Benchmark experiment:

No benchmark data available for comparison.

Benchmark has no cost, total tokens, cached tokens data. Will appear after the next weekly benchmark run.

Comparison indicators:

  • ±0% — diff under 10% (within noise threshold)
  • ↑N%/↓N% — diff 10-25%
  • ↑N%/↓N% — diff over 25% (significant)
📜 #4 · Run @ __08cc416__ (#24991233327) — Apr 27, 11:09 UTC

✅ Results of HolmesGPT evals

Automatically triggered by commit 08cc416 on branch skills

View workflow logs

Results of HolmesGPT evals

  • ask_holmes: 11/11 test cases were successful, 0 regressions
Status Test case Time Turns Tools Cost Total tokens Input Max input Output Max output Cached Non-cached Reasoning Compactions
✅ 09_crashpod 37.5s 5 10 $0.2449 105,225 103,092 23,564 2,133 887 78,528 24,564 — —
✅ 101_loki_historical_logs_pod_deleted 55.5s 7 13 $0.3070 158,170 155,086 26,053 3,084 878 128,465 26,621 — —
✅ 112_find_pvcs_by_uuid 20.4s 3 4 $0.1860 61,439 60,310 21,954 1,129 702 38,055 22,255 — —
✅ 12_job_crashing 44.0s 6 15 $0.2792 138,709 136,343 25,820 2,366 614 109,757 26,586 — —
✅ 176_network_policy_blocking_traffic_no_skills 39.8s 6 11 $0.2858 131,327 128,980 25,256 2,347 896 99,854 29,126 — —
✅ 227_count_configmaps_per_namespace[0] 26.0s 5 9 $0.2005 94,775 93,539 20,861 1,236 584 72,042 21,497 — —
✅ 243_pod_names_contain_service 31.6s 5 8 $0.2189 99,708 98,007 21,819 1,701 447 75,757 22,250 — —
✅ 24_misconfigured_pvc 31.2s 5 10 $0.2238 99,593 97,862 21,795 1,731 428 74,672 23,190 — —
✅ 43_current_datetime_from_prompt 4.6s 1 — $0.1088 17,033 16,907 16,907 126 126 0 16,907 — —
✅ 51_logs_summarize_errors 23.4s 4 5 $0.1898 77,954 76,799 21,298 1,155 397 55,489 21,310 — —
✅ 61_exact_match_counting 11.0s 3 3 $0.1381 52,769 52,410 17,895 359 224 34,504 17,906 — —
Total 29.5s avg 4.5 avg 8.8 avg $2.3828 1,036,702 1,019,335 26,053 17,367 896 767,123 252,212 — —
Benchmark Comparison Details

Baseline: latest ci-benchmark experiment on master

Status: Success - 74 test/model combinations loaded

Benchmark experiment:

No benchmark data available for comparison.

Benchmark has no cost, total tokens, cached tokens data. Will appear after the next weekly benchmark run.

Comparison indicators:

  • ±0% — diff under 10% (within noise threshold)
  • ↑N%/↓N% — diff 10-25%
  • ↑N%/↓N% — diff over 25% (significant)
📜 #3 · Run @ __f3ff678__ (#24990978032) — Apr 27, 11:02 UTC

✅ Results of HolmesGPT evals

Automatically triggered by commit f3ff678 on branch skills

View workflow logs

Results of HolmesGPT evals

  • ask_holmes: 11/11 test cases were successful, 0 regressions
Status Test case Time Turns Tools Cost Total tokens Input Max input Output Max output Cached Non-cached Reasoning Compactions
✅ 09_crashpod 36.3s 5 10 $0.2391 103,254 101,231 23,268 2,023 783 77,056 24,175 — —
✅ 101_loki_historical_logs_pod_deleted 46.6s 6 11 $0.2794 129,874 127,228 24,266 2,646 880 100,721 26,507 — —
✅ 112_find_pvcs_by_uuid 21.1s 3 3 $0.1781 60,356 59,364 21,485 992 561 37,868 21,496 — —
✅ 12_job_crashing 39.0s 6 14 $0.2725 136,741 134,503 25,364 2,238 588 108,367 26,136 — —
✅ 176_network_policy_blocking_traffic_no_skills 46.2s 6 15 $0.2971 133,231 130,413 26,629 2,818 740 101,936 28,477 — —
✅ 227_count_configmaps_per_namespace[0] 28.5s 6 10 $0.2131 113,372 111,866 20,608 1,506 519 91,040 20,826 — —
✅ 243_pod_names_contain_service 39.5s 5 9 $0.2286 100,847 98,797 22,235 2,050 574 76,549 22,248 — —
✅ 24_misconfigured_pvc 39.6s 6 12 $0.2487 121,160 119,003 23,007 2,157 524 95,343 23,660 — —
✅ 43_current_datetime_from_prompt 5.1s 1 — $0.1089 17,038 16,907 16,907 131 131 0 16,907 — —
✅ 51_logs_summarize_errors 24.9s 4 5 $0.1863 77,174 76,059 20,928 1,115 360 55,119 20,940 — —
✅ 61_exact_match_counting 11.6s 3 3 $0.1385 52,778 52,404 17,891 374 227 34,502 17,902 — —
Total 30.8s avg 4.6 avg 9.2 avg $2.3901 1,045,825 1,027,775 26,629 18,050 880 778,501 249,274 — —
Benchmark Comparison Details

Baseline: latest ci-benchmark experiment on master

Status: Success - 74 test/model combinations loaded

Benchmark experiment:

No benchmark data available for comparison.

Benchmark has no cost, total tokens, cached tokens data. Will appear after the next weekly benchmark run.

Comparison indicators:

  • ±0% — diff under 10% (within noise threshold)
  • ↑N%/↓N% — diff 10-25%
  • ↑N%/↓N% — diff over 25% (significant)
📜 #2 · Run @ __fb72c61__ (#24953432514) — Apr 26, 09:40 UTC

✅ Results of HolmesGPT evals

Automatically triggered by commit fb72c61 on branch skills

View workflow logs

Results of HolmesGPT evals

  • ask_holmes: 11/11 test cases were successful, 0 regressions
Status Test case Time Turns Tools Cost Total tokens Input Max input Output Max output Cached Non-cached Reasoning Compactions
✅ 09_crashpod 35.6s 6 11 $0.2490 123,930 121,751 22,793 2,179 582 98,385 23,366 — —
✅ 101_loki_historical_logs_pod_deleted 48.1s 6 11 $0.2895 137,312 134,471 25,865 2,841 929 108,036 26,435 — —
✅ 112_find_pvcs_by_uuid 17.1s 3 3 $0.1763 60,275 59,350 21,466 925 512 37,873 21,477 — —
✅ 12_job_crashing 34.2s 5 12 $0.2476 111,285 109,285 24,743 2,000 572 84,379 24,906 — —
✅ 176_network_policy_blocking_traffic_no_skills 41.0s 6 11 $0.2725 126,907 124,473 24,542 2,434 549 98,090 26,383 — —
✅ 227_count_configmaps_per_namespace[0] 26.4s 6 10 $0.2127 113,385 111,858 20,606 1,527 519 91,238 20,620 — —
✅ 243_pod_names_contain_service 31.5s 5 8 $0.2207 99,478 97,722 21,706 1,756 408 75,310 22,412 — —
✅ 24_misconfigured_pvc 36.9s 6 13 $0.2596 123,555 121,220 23,572 2,335 882 96,525 24,695 — —
✅ 43_current_datetime_from_prompt 4.7s 1 — $0.1089 17,035 16,907 16,907 128 128 0 16,907 — —
✅ 51_logs_summarize_errors 21.2s 4 5 $0.1874 77,624 76,525 21,145 1,099 343 55,368 21,157 — —
✅ 61_exact_match_counting 9.7s 2 1 $0.1253 34,624 34,297 17,381 327 259 16,906 17,391 — —
Total 27.9s avg 4.5 avg 8.5 avg $2.3495 1,025,410 1,007,859 25,865 17,551 929 762,110 245,749 — —
Benchmark Comparison Details

Baseline: latest ci-benchmark experiment on master

Status: Success - 74 test/model combinations loaded

Benchmark experiment:

No benchmark data available for comparison.

Benchmark has no cost, total tokens, cached tokens data. Will appear after the next weekly benchmark run.

Comparison indicators:

  • ±0% — diff under 10% (within noise threshold)
  • ↑N%/↓N% — diff 10-25%
  • ↑N%/↓N% — diff over 25% (significant)
📜 #1 · Run @ __a4bc86e__ (#24951967852) — Apr 26, 08:20 UTC

✅ Results of HolmesGPT evals

Automatically triggered by commit a4bc86e on branch skills

View workflow logs

Results of HolmesGPT evals

  • ask_holmes: 11/11 test cases were successful, 0 regressions
Status Test case Time Turns Tools Cost Total tokens Input Max input Output Max output Cached Non-cached Reasoning Compactions
✅ 09_crashpod 33.2s 5 10 $0.2398 104,690 102,626 23,517 2,064 592 78,736 23,890 — —
✅ 101_loki_historical_logs_pod_deleted 46.8s 6 11 $0.2788 127,697 124,965 24,907 2,732 755 99,000 25,965 — —
✅ 112_find_pvcs_by_uuid 17.7s 3 3 $0.1690 57,725 56,742 20,173 983 557 36,558 20,184 — —
✅ 12_job_crashing 41.0s 6 15 $0.2938 140,298 137,870 26,689 2,428 678 108,803 29,067 — —
✅ 176_network_policy_blocking_traffic_no_skills 46.1s 6 14 $0.2856 132,057 129,385 26,289 2,672 1,052 102,447 26,938 — —
✅ 227_count_configmaps_per_namespace[0] 27.0s 6 10 $0.2150 113,398 111,873 20,605 1,525 519 90,732 21,141 — —
✅ 243_pod_names_contain_service 36.6s 6 9 $0.2417 118,872 116,807 22,239 2,065 581 93,721 23,086 — —
✅ 24_misconfigured_pvc 36.2s 6 14 $0.2545 125,311 123,009 23,264 2,302 577 99,376 23,633 — —
✅ 43_current_datetime_from_prompt 5.0s 1 — $0.1088 17,032 16,907 16,907 125 125 0 16,907 — —
✅ 51_logs_summarize_errors 22.2s 4 5 $0.1841 77,251 76,251 21,024 1,000 310 55,215 21,036 — —
✅ 61_exact_match_counting 12.6s 3 3 $0.1404 52,963 52,529 17,953 434 287 34,565 17,964 — —
Total 29.5s avg 4.7 avg 9.4 avg $2.4113 1,067,294 1,048,964 26,689 18,330 1,052 799,153 249,811 — —
Benchmark Comparison Details

Baseline: latest ci-benchmark experiment on master

Status: Success - 74 test/model combinations loaded

Benchmark experiment:

No benchmark data available for comparison.

Benchmark has no cost, total tokens, cached tokens data. Will appear after the next weekly benchmark run.

Comparison indicators:

  • ±0% — diff under 10% (within noise threshold)
  • ↑N%/↓N% — diff 10-25%
  • ↑N%/↓N% — diff over 25% (significant)

✅ Results of HolmesGPT evals

Automatically triggered by commit ac195f6 on branch skills

View workflow logs

Results of HolmesGPT evals

  • ask_holmes: 11/11 test cases were successful, 0 regressions
Status Test case Time Turns Tools Cost Total tokens Input Max input Output Max output Cached Non-cached Reasoning Compactions
✅ 09_crashpod 30.8s 5 9 $0.2338 103,688 101,923 23,121 1,765 559 77,522 24,401 — —
✅ 101_loki_historical_logs_pod_deleted 66.7s 9 17 $0.3717 220,548 216,844 29,235 3,704 747 187,032 29,812 — —
✅ 112_find_pvcs_by_uuid 25.6s 5 4 $0.1987 95,340 94,064 20,813 1,276 333 73,238 20,826 — —
✅ 12_job_crashing 31.2s 5 11 $0.2449 109,568 107,784 24,181 1,784 528 81,965 25,819 — —
✅ 176_network_policy_blocking_traffic_no_skills 37.1s 5 13 $0.2454 103,631 101,343 22,816 2,288 621 77,121 24,222 — —
✅ 227_count_configmaps_per_namespace[0] 21.7s 4 9 $0.1932 77,298 76,066 20,883 1,232 584 54,237 21,829 — —
✅ 243_pod_names_contain_service 42.2s 5 9 $0.2349 101,139 99,032 22,358 2,107 602 75,753 23,279 — —
✅ 24_misconfigured_pvc 34.7s 5 12 $0.2350 102,300 100,194 22,622 2,106 565 77,092 23,102 — —
✅ 43_current_datetime_from_prompt 4.6s 1 — $0.1090 17,040 16,907 16,907 133 133 0 16,907 — —
✅ 51_logs_summarize_errors 23.9s 4 5 $0.1890 77,681 76,521 21,161 1,160 411 55,348 21,173 — —
✅ 61_exact_match_counting 8.4s 2 1 $0.1232 34,491 34,230 17,314 261 193 16,906 17,324 — —
Total 29.7s avg 4.5 avg 9.0 avg $2.3787 1,042,724 1,024,908 29,235 17,816 747 776,214 248,694 — —
Benchmark Comparison Details

Baseline: latest ci-benchmark experiment on master

Status: Success - 74 test/model combinations loaded

Benchmark experiment:

No benchmark data available for comparison.

Benchmark has no cost, total tokens, cached tokens data. Will appear after the next weekly benchmark run.

Comparison indicators:

  • ±0% — diff under 10% (within noise threshold)
  • ↑N%/↓N% — diff 10-25%
  • ↑N%/↓N% — diff over 25% (significant)
📖 Legend
Icon Meaning
✅ The test was successful
➖ The test was skipped
⚠️ The test failed but is known to be flaky or known to fail
🚧 The test had a setup failure (not a code regression)
🔧 The test failed due to mock data issues (not a code regression)
🚫 The test was throttled by API rate limits/overload
❌ The test failed and should be fixed before merging the PR
🔄 Re-run evals manually

⚠️ Warning: /eval comments always run using the workflow from master, not from this PR branch. If you modified the GitHub Action (e.g., added secrets or env vars), those changes won't take effect.

To test workflow changes, use the GitHub CLI or Actions UI instead:

gh workflow run eval-regression.yaml --repo HolmesGPT/holmesgpt --ref skills -f markers=regression -f filter=

Option 1: Comment on this PR with /eval:

/eval
tags: regression

Or with more options (one per line):

/eval
model: gpt-4o
tags: regression
id: 09_crashpod
iterations: 5

Run evals on a different branch (e.g., master) for comparison:

/eval
branch: master
tags: regression
Option Description
model Model(s) to test (default: same as automatic runs)
tags Pytest tags / markers (no default - runs all tests!)
id Eval ID / pytest -k filter (use /list to see valid eval names)
iterations Number of runs, max 10
branch Run evals on a different branch (for cross-branch comparison)

Quick re-run: Use /rerun to re-run the most recent /eval on this PR with the same parameters.

Option 2: Trigger via GitHub Actions UI → "Run workflow"

Option 3: Add PR labels to include extra evals (applies to both automatic runs and /eval comments):

Label Effect
evals-tag-<name> Run tests with tag <name> alongside regression
evals-id-<name> Run a specific eval by test ID
evals-model-<name> Override the model (use model list name, e.g. sonnet-4.5)

Examples: evals-tag-easy, evals-id-09_crashpod, evals-model-sonnet-4.5

🏷️ Valid tags

benchmark, chain-of-causation, compaction, confluence, context_window, coralogix, counting, database, datadog, datetime, db-connectors, easy, elasticsearch, embeds, fast, frontend, grafana, hard, images, integration, kafka, kubernetes, leaked-information, logs, loki, manual, mcp, medium, metrics, network, newrelic, no-cicd, numerical, one-test, port-forward, prometheus, question-answer, regression, skills, slackbot, storage, token-limit, toolset-limitation, traces, transparency

🤖 Valid models

deepseek-chat, deepseek-r1-reasoner, deepseek-reasoner, deepseek-v3.2-chat, gemini-3-flash-preview, gemini-3-pro-preview, gemini-3.1-pro-preview, gpt-4.1, gpt-5.2-high-reasoning, gpt-5.3-codex, gpt-5.4, haiku-4.5, kimi-2.5, kimi-2.5-openrouter, opus-4.5, opus-4.6, qwen-next-80B-instruct, qwen-next-80B-thinking, sonnet-4.5, sonnet-4.6


Commands: /eval · /rerun · /list

CLI: gh workflow run eval-regression.yaml --repo HolmesGPT/holmesgpt --ref skills -f markers=regression -f filter=

@coderabbitai

coderabbitai Bot commented Apr 26, 2026 •

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Replaces the runbook subsystem with a skills subsystem across the codebase: renames config fields and APIs, removes RunbookFetcher/catalog loading, adds a Skill loader and SkillsToolset/SkillsFetcher, and updates prompts, templates, tests, docs, and tooling metadata to use "skills" instead of "runbooks".

Changes

Cohort / File(s) Summary
Config & Toolset plumbing
holmes/config.py, holmes/core/toolset_manager.py
Rename custom_runbook_catalogs → custom_skill_paths; add/forward get_skill_catalog(); map deprecated "runbook" toolset name to "skills".
Prompt & Conversation layer
holmes/core/prompt.py, holmes/core/conversations.py, holmes/interactive.py, holmes/main.py, server.py
Replace RunbookCatalog types/params with SkillCatalog; rename callsites runbooks → skills; rename prompt enum TIME_RUNBOOKS → TIME_SKILLS; switch generate_runbooks_args → generate_skills_args.
Skill loader (new)
holmes/plugins/skills/skill_loader.py, holmes/plugins/skills/__init__.py
Add Skill models, parse SKILL.md with YAML frontmatter, scan/merge builtin/custom/remote skills into SkillCatalog; add RobustaSkillInstruction.
Toolsets — removal & addition
holmes/plugins/toolsets/runbook/runbook_fetcher.py (removed), holmes/plugins/toolsets/skills/skills_fetcher.py, holmes/plugins/toolsets/__init__.py
Remove RunbookFetcher/RunbookToolset; add SkillsFetcher/SkillsToolset that resolve local/remote skills and return <skill>...</skill> formatted results; update builtin registration.
DAL & instruction mapping
holmes/core/supabase_dal.py, tests/llm/utils/mock_dal.py
Rename DAL methods to get_skill_catalog/get_skill_content, map Supabase rows to RobustaSkillInstruction, add enabled/clusters filtering.
Prompt templates
holmes/plugins/prompts/* (_general_instructions.jinja2, _runbook_instructions.jinja2 removed, _skill_instructions.jinja2 added, _skills_instructions.jinja2 added, base_user_prompt.jinja2, investigation_procedure.jinja2)
Remove runbook partials and add skill-specific partials; switch gating flag to skills_enabled and update instruction text to reference "skill(s)".
Tooling & execution models
holmes/core/tools.py, holmes/core/tools_utils/tool_executor.py, holmes/core/tool_calling_llm.py
Update public field descriptions from "runbook authorization" → "skill authorization"; rename internal transient flag _runbook_in_use → _skill_in_use and gate restricted tools on skill fetch.
Runbooks module cleanup
holmes/plugins/runbooks/__init__.py, holmes/plugins/runbooks/*, holmes/plugins/runbooks/catalog.json, holmes/plugins/runbooks/README.md
Delete runbooks module, loader, catalog and README (previous runbook models/loaders removed).
Tests & fixtures
tests/... (many), tests/llm/fixtures/... (many SKILL.md/test_case/toolsets changes), tests/plugins/toolsets/test_skills.py (new)
Remove runbook-focused tests; add/modify tests and fixtures to use skills (rename fields, add SKILL.md frontmatter, update expectations to fetch_skill), add tests for SkillsFetcher.
Docs & config metadata
docs/reference/skills.md (new), docs/reference/runbooks.md (removed), docs/reference/http-api.md, docs/reference/.nav.yml, holmes/plugins/skills/CLAUDE.md, holmes/plugins/skills/README.md, .gitignore, pyproject.toml
Add Skills docs and migration guide; remove runbooks docs; update http-api key time_runbooks → time_skills; nav and pytest markers updated; .gitignore adds .claude/settings.json and .claude/skills/dev-skills/.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client
    participant Server as Server
    participant PromptBuilder as PromptBuilder
    participant SkillLoader as SkillCatalogLoader
    participant ToolExecutor as ToolExecutor
    participant Supabase as SupabaseDAL

    Client->>Server: send chat/request
    Server->>PromptBuilder: build_chat_messages(skills=...)
    PromptBuilder->>SkillLoader: ensure SkillCatalog (builtin + custom paths)
    SkillLoader->>Supabase: optionally fetch remote skills
    Supabase-->>SkillLoader: remote skill entries
    SkillLoader-->>PromptBuilder: SkillCatalog
    PromptBuilder->>ToolExecutor: include skill-aware tools (fetch_skill)
    ToolExecutor->>SkillLoader: resolve skill by name/ID
    SkillLoader->>Supabase: get_skill_content(id) [if remote]
    Supabase-->>ToolExecutor: skill content
    ToolExecutor-->>PromptBuilder: StructuredToolResult (content)
    PromptBuilder-->>Server: assembled messages
    Server-->>Client: response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested labels

enhancement

Suggested reviewers

  • moshemorad
  • arikalon1
  • aantn
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 46.91% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title '[ROB-3715] - Skills' is vague and generic. It provides only a ticket reference and the term 'Skills' without conveying what has actually changed or what the primary objective is in the changeset. Consider a more descriptive title that explains the main change, such as 'Replace runbooks system with skills-based system' or 'Migrate from runbooks to skills architecture'. This would clarify the scope of this large refactoring effort.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@netlify

netlify Bot commented Apr 26, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for holmes-docs ready!

Name Link
🔨 Latest commit ac195f6
🔍 Latest deploy log https://app.netlify.com/projects/holmes-docs/deploys/69ef45b8d126930008c1991e
😎 Deploy Preview https://deploy-preview-1953--holmes-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@github-actions

github-actions Bot commented Apr 26, 2026 •

Copy link
Copy Markdown
Contributor

🔬 CLI Performance Benchmark

🟡 Startup Time (no LLM)

Measures holmes version execution time (imports + initialization)

Metric PR Master Change
Cold Start 11.28s 11.65s -3.2%
Warm Mean 5.23s 5.23s +0.0%
Warm Min 5.15s 5.15s
Warm Max 5.37s 5.29s

🟡 Full CLI with LLM

Measures holmes ask execution time (OpenRouter + Haiku 4.5)

Metric PR Master Change
Cold Start 10.85s 16.32s -33.5%
Warm Mean 6.89s 7.02s -1.9%
Warm Min 6.77s 6.81s
Warm Max 7.08s 7.33s

PR: d9db68f1 | Master: 944d6b55 | Iterations: 5

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
tests/llm/fixtures/test_ask_holmes/162_get_skills/test_case.yaml (1)

52-53: ⚠️ Potential issue | 🟡 Minor

Update expected_output to use "skills" terminology.

The expected output validation text still references "runbook" but should use "skill" to align with the migration.

📝 Proposed fix
-# verifies that the runbook is pulled from robusta and also global instructions are read and followed
+# verifies that the skill is pulled from robusta and also global instructions are read and followed
 expected_output: |
-  The runbook provides systematic debugging steps for signup service issues, emphasizing the need to check the payments service dependency first. The runbook should specifically mention checking Stripe API key validation in the payments service, as this is the root cause of the signup failures. Mentions contacting the interlock team for assistance. There is no mention of the capricorn team
+  The skill provides systematic debugging steps for signup service issues, emphasizing the need to check the payments service dependency first. The skill should specifically mention checking Stripe API key validation in the payments service, as this is the root cause of the signup failures. Mentions contacting the interlock team for assistance. There is no mention of the capricorn team
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/llm/fixtures/test_ask_holmes/162_get_skills/test_case.yaml` around
lines 52 - 53, Update the YAML expected_output text for the test case identifier
"162_get_skills" by replacing "runbook" wording with "skill" (e.g., change "The
runbook provides..." to "The skill provides...", "The runbook should
specifically mention..." to "The skill should specifically mention...") while
keeping the rest of the sentence intact; edit the value under the
expected_output key so all occurrences of "runbook" are switched to "skill".
tests/llm/fixtures/test_ask_holmes/96_no_matching_skill/test_case.yaml (1)

1-12: ⚠️ Potential issue | 🟡 Minor

Terminology drift between metadata and prompt/expected output.

The directory and tag are now skills, but the user_prompt still says "Use the custom-crd runbook" and expected_output (lines 8–12) repeatedly references "runbook"/"runbooks". For consistency with the PR-wide rename, consider updating these strings to use "skill"/"skills" so the assertions and prompt match the new vocabulary the system is being primed with.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/llm/fixtures/test_ask_holmes/96_no_matching_skill/test_case.yaml`
around lines 1 - 12, Update the test fixture strings to match the PR-wide rename
from "runbook(s)" to "skill(s)": change the user_prompt value from "Use the
custom-crd runbook..." to "Use the custom-crd skill..." (or similar), and revise
all occurrences in expected_output that mention "runbook" or "runbooks" to
"skill" or "skills" so the prompt, tags, and assertions consistently use the
"skills" terminology; target the user_prompt and expected_output fields in this
fixture (test_ask_holmes/96_no_matching_skill/test_case.yaml) and keep the
original meaning of each assertion.
tests/llm/fixtures/test_ask_holmes/90_skill_basic_selection/test_case.yaml (1)

7-13: ⚠️ Potential issue | 🟡 Minor

Expected output still references "runbook" after rename to skills.

The test directory name is 90_skill_basic_selection and the tag is skills, but expected_output still mentions "Application Gateway troubleshooting runbook" and "as per the runbook instructions". Consider updating these to "skill" wording to stay consistent with the runbooks→skills migration.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/llm/fixtures/test_ask_holmes/90_skill_basic_selection/test_case.yaml`
around lines 7 - 13, The expected_output in test_case.yaml still uses "runbook"
wording; update the strings in the expected_output block to use "skill" instead
(e.g., change "Application Gateway troubleshooting runbook" to "Application
Gateway troubleshooting skill" and "as per the runbook instructions" to "as per
the skill instructions"), and ensure any other occurrences like "runbook
instructions" or "runbook" are replaced with the corresponding "skill" phrasing
to reflect the runbooks→skills migration (file: the expected_output field in the
test case for 90_skill_basic_selection, tag 'skills').
holmes/interactive.py (1)

2254-2262: 🛠️ Refactor suggestion | 🟠 Major

Add an explicit type hint for skills in run_interactive_loop.

The changed parameter is currently untyped, which weakens mypy coverage on this API surface.

🛠️ Proposed refactor
@@
 from holmes.version import check_version_async
+from holmes.plugins.skills.skill_loader import SkillCatalog
@@
 def run_interactive_loop(
@@
-    skills=None,
+    skills: Optional[SkillCatalog] = None,

As per coding guidelines, "Type hints are required throughout the codebase (mypy configuration in pyproject.toml)".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@holmes/interactive.py` around lines 2254 - 2262, The parameter "skills" in
run_interactive_loop is untyped; add an explicit type hint (e.g., change it to
skills: Optional[Sequence[Skill]]) and update imports accordingly (import
Sequence from typing and import or reference the Skill type from its
definition). Modify the function signature in run_interactive_loop to use the
new type and ensure any callers/assignments still satisfy
Optional[Sequence[Skill]]; if Skill lives in another module, add the appropriate
import (or use a forward-reference string "Skill") to keep mypy happy.
♻️ Duplicate comments (1)
holmes/plugins/prompts/_skills_instructions.jinja2 (1)

1-9: ⚠️ Potential issue | 🔴 Critical

Filename inconsistency with includes — see comment in base_user_prompt.jinja2.

This new partial is named _skills_instructions.jinja2 (plural) while base_user_prompt.jinja2 includes _skill_instructions.jinja2 (singular). Pick one canonical name and apply it consistently across all referencing templates (base_user_prompt.jinja2, investigation_procedure.jinja2, _general_instructions.jinja2 per the PR summary). The content of the block itself reads correctly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@holmes/plugins/prompts/_skills_instructions.jinja2` around lines 1 - 9, The
partial filename is inconsistent: the new template is
`_skills_instructions.jinja2` but `base_user_prompt.jinja2` (and other
templates) include `_skill_instructions.jinja2`; pick one canonical name (e.g.,
`_skills_instructions.jinja2`) and update all include statements to match that
name across templates mentioned in the PR (`base_user_prompt.jinja2`,
`investigation_procedure.jinja2`, `_general_instructions.jinja2`), or rename the
file to the singular form if you prefer that convention, ensuring the include
targets and file name are identical and updating any import/include references
accordingly.
🧹 Nitpick comments (15)
.gitignore (1)

173-173: Remove duplicate ignore rule for Claude skills path.

Line 173 duplicates the exact ignore rule already present at Line 5. Keep only one entry to avoid config noise.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.gitignore at line 173, Remove the duplicate ignore entry for
".claude/skills/dev-skills/": locate both occurrences of the exact rule
".claude/skills/dev-skills/" in the .gitignore and delete the redundant one so
only a single ignore line for that path remains.
tests/llm/fixtures/test_ask_holmes/176_network_policy_blocking_traffic_no_skills/toolsets.yaml (1)

4-4: Update the inline comment to reference “skills” instead of “runbook”.

The current comment is stale after the rename.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tests/llm/fixtures/test_ask_holmes/176_network_policy_blocking_traffic_no_skills/toolsets.yaml`
at line 4, Update the inline comment next to the "internet:" entry in
toolsets.yaml to mention "skills" instead of "runbook" (e.g., change "because
runbook is disabled" to "because skills are disabled" or similar) so the comment
reflects the renamed concept used by Holmes when fetching
networking/dns_troubleshooting_instructions.md.
tests/llm/fixtures/test_ask_holmes/90_skill_basic_selection/application-gateway-troubleshooting/SKILL.md (1)

8-8: Consider replacing “runbook” with “skill” in the overview text.

Keeps terminology consistent with the migration and avoids mixed wording in fixtures.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tests/llm/fixtures/test_ask_holmes/90_skill_basic_selection/application-gateway-troubleshooting/SKILL.md`
at line 8, Replace the term "runbook" with "skill" in the overview sentence that
currently reads "This runbook helps troubleshoot common Application Gateway
issues including 502 Bad Gateway errors." (search for the string "This runbook
helps troubleshoot" or the token "runbook" in SKILL.md) to maintain consistent
terminology with the migration; update the sentence to "This skill helps
troubleshoot common Application Gateway issues including 502 Bad Gateway
errors."
holmes/plugins/toolsets/internet/internet.py (1)

181-181: Description wording is ambiguous after the rename.

"Fetch skills if they are present" reads awkwardly out of context — fetch_webpage doesn't fetch skills, it fetches whatever URL is given. Consider clarifying that this tool should be used to fetch a webpage referenced by a skill (e.g., "Use this to fetch any URL referenced by a skill before starting your investigation, when no more specific tool like Confluence applies."). Otherwise the LLM may misinterpret the instruction.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@holmes/plugins/toolsets/internet/internet.py` at line 181, Update the
ambiguous description for the fetch_webpage tool (look for the description
string associated with fetch_webpage in internet.py) so it no longer implies the
tool "fetches skills"; instead state that it fetches any URL referenced by a
skill or investigation and should be used when no more specific tool (e.g.,
Confluence) applies—for example: "Fetch any URL referenced by a skill before
starting your investigation when no more specific tool like Confluence applies."
Ensure the change replaces the current phrasing exactly in the description value
for fetch_webpage.
tests/llm/fixtures/test_ask_holmes/165_alert_with_multiple_skills/test_case.yaml (1)

1-2: Comments are now stale after the runbooks → skills rename.

Lines 1–2 still reference "runbook"/"runbboks". Update them to match the new terminology so this fixture's intent stays clear.

📝 Proposed wording fix
-# This test is used to test the ability of HolmesGPT to find the relevant runbook for a given alert
-# TODO: Add check for the tool calls that actually checked that the relevant runbboks were fetched and used
+# This test is used to test the ability of HolmesGPT to find the relevant skill for a given alert
+# TODO: Add check for the tool calls that actually checked that the relevant skills were fetched and used
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tests/llm/fixtures/test_ask_holmes/165_alert_with_multiple_skills/test_case.yaml`
around lines 1 - 2, Update the header comments in the fixture to use the new
terminology: replace the singular "runbook" with "skill" in the first line and
fix the misspelled "runbboks" to "skills" in the second line so both lines
consistently reference "skill/skills" instead of "runbook/runbboks".
holmes/plugins/runbooks/__init__.py (1)

1-37: Module path still says runbooks/ while contents have been renamed to skills.

The class, docstring, and PR-wide migration are now skills-oriented, but this file lives at holmes/plugins/runbooks/__init__.py. New contributors will be confused finding RobustaSkillInstruction under a runbooks/ directory, and grep-driven refactors may miss it. Consider moving this module to holmes/plugins/skills/__init__.py (or similar) as part of this PR or as a documented follow-up.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@holmes/plugins/runbooks/__init__.py` around lines 1 - 37, The module
name/package (runbooks) no longer matches its contents (skills): move the module
out of the runbooks package into the skills package and update all imports and
exports that reference RobustaSkillInstruction (and any top-level symbols in
this file), adjust package __init__ exports, and fix any tests or code that
import the old runbooks module so they now import from skills; ensure the class
name RobustaSkillInstruction, its docstring, and the yaml dumper behavior remain
unchanged during the move.
server.py (1)

408-412: Follow-up action text still says "runbooks".

This PR replaces runbooks with skills throughout, but the articles follow-up still asks the LLM to "List the relevant runbooks and links" with notification text "Looking up and summarizing runbooks and links...". If the intent is a complete rename, update this string too; if "runbooks" is intentionally kept as user-facing terminology, ignore.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server.py` around lines 408 - 412, Update the user-facing strings in the
follow-up action with id="articles" (action_label="Articles") to reflect the
rename from "runbooks" to "skills": change the prompt value ("List the relevant
runbooks and links used. Write a short summary for each") and
pre_action_notification_text ("Looking up and summarizing runbooks and
links...") to use "skills" (or another intended term) so wording is consistent
across the codebase.
tests/llm/fixtures/test_ask_holmes/94_skill_transparency/database-troubleshooting/SKILL.md (1)

8-8: Body still refers to "this runbook".

Minor doc inconsistency now that the file is a SKILL.md. Optional cleanup:

📝 Proposed wording tweak
-This runbook helps diagnose database connection timeout issues in Kubernetes applications.
+This skill helps diagnose database connection timeout issues in Kubernetes applications.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tests/llm/fixtures/test_ask_holmes/94_skill_transparency/database-troubleshooting/SKILL.md`
at line 8, The body text uses the phrase "this runbook" which is inconsistent
with the file now being SKILL.md; update the sentence "This runbook helps
diagnose database connection timeout issues in Kubernetes applications." to use
consistent terminology (e.g., "This skill helps diagnose database connection
timeout issues in Kubernetes applications." or "This document helps
diagnose...") so SKILL.md wording matches file purpose and header; search for
the exact phrase "This runbook helps diagnose database connection timeout issues
in Kubernetes applications." to locate and replace it.
tests/llm/fixtures/test_ask_holmes/210_confluence_skill_fetch/test_case.yaml (1)

1-1: Stale comment terminology.

The header still says "internal runbook tool" but the test now guards against fetch_skill. Update for consistency:

📝 Proposed wording tweak
-# Test: Fetch runbook from Confluence (must use Confluence toolset, not internal runbook tool)
+# Test: Fetch runbook from Confluence (must use Confluence toolset, not internal skill tool)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/llm/fixtures/test_ask_holmes/210_confluence_skill_fetch/test_case.yaml`
at line 1, Update the test header comment to replace the outdated phrase
"internal runbook tool" with the guarded symbol `fetch_skill` so it reads
something like "Fetch runbook from Confluence (must use Confluence toolset, not
fetch_skill)"; locate and edit the header string in the test fixture for the
Confluence skill (the comment at the top of
test_ask_holmes/210_confluence_skill_fetch/test_case.yaml) and make the wording
consistent with the current guard.
tests/llm/fixtures/test_ask_holmes/94_skill_transparency/test_case.yaml (1)

7-11: Stale "runbook" wording in expected_output.

The migration renames runbooks → skills, but the expected output assertion still reads "Uses database troubleshooting runbook". Consider updating the wording to reflect the new vocabulary so the assertion remains consistent with the new system and prompt outputs.

✏️ Suggested wording
 expected_output: |
-  - Uses database troubleshooting runbook
+  - Uses database troubleshooting skill
   - Includes detailed "Investigation Steps Performed" section
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/llm/fixtures/test_ask_holmes/94_skill_transparency/test_case.yaml`
around lines 7 - 11, Update the stale wording in the test fixture's
expected_output: locate the expected_output block in
tests/llm/fixtures/test_ask_holmes/94_skill_transparency/test_case.yaml and
replace "Uses database troubleshooting runbook" with wording that uses the new
vocabulary (for example "Uses database troubleshooting skill") so the assertion
matches the migrated runbooks→skills terminology; ensure the rest of the
bulleted expectations remain unchanged and that the exact string in
expected_output aligns with the prompt/system output for the skill-based flow.
tests/plugins/toolsets/test_skills.py (2)

50-57: Consider also covering whitespace-only skill_id.

The AI summary describes coverage for "empty/blank" skill_id, but only "" is exercised here. If the fetcher trims/strips, a " " case is worth adding to lock in that contract.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/plugins/toolsets/test_skills.py` around lines 50 - 57, The test
test_SkillsFetcher_empty_id only asserts behavior for an empty string and misses
the whitespace-only contract; add a new assertion (or extend the existing test)
that calls SkillsFetcher(SkillsToolset())._invoke with {"skill_id": "   "} (or
similar whitespace) using create_mock_tool_invoke_context() and assert the
returned result.status is StructuredToolResultStatus.ERROR to ensure
whitespace-only IDs are treated like empty IDs by the SkillsFetcher._invoke
handling.

1-13: Unused os import and TEST_SKILLS_PATH.

os and the TEST_SKILLS_PATH constant are defined but never referenced in any of the tests below.

♻️ Suggested cleanup
-import os
-
 from holmes.core.tools import StructuredToolResultStatus
 from holmes.plugins.skills.skill_loader import Skill, SkillCatalog, SkillSource
 from holmes.plugins.toolsets.skills.skills_fetcher import (
     SkillsFetcher,
     SkillsToolset,
 )
 from tests.conftest import create_mock_tool_invoke_context
-
-TEST_SKILLS_PATH = os.path.join(
-    os.path.dirname(__file__), "fixtures", "skills"
-)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/plugins/toolsets/test_skills.py` around lines 1 - 13, Remove the unused
top-level import and constant: delete the import of os and the TEST_SKILLS_PATH
constant declaration in test_skills.py (they are unused symbols `os` and
`TEST_SKILLS_PATH`), leaving only the necessary imports from holmes and tests;
ensure no other code references `TEST_SKILLS_PATH` before committing.
holmes/core/toolset_manager.py (1)

121-127: Silent fallback when a path neither exists nor is a directory.

For each p in custom_skill_paths, the branch is decided solely by Path(p).is_dir(). If the user passes a typo or a non-existent path, is_dir() returns False and the code silently uses os.path.dirname(os.path.abspath(str(p))) — i.e., the (possibly unrelated) parent directory of the typo'd path becomes an additional search path. Consider warning/erroring when the path doesn't exist, or at least logging the resolved search path for visibility.

♻️ Possible adjustment
     if self.custom_skill_paths:
-        additional_search_paths = [
-            str(Path(p).resolve()) if Path(p).is_dir() else os.path.dirname(os.path.abspath(str(p)))
-            for p in self.custom_skill_paths
-        ]
+        additional_search_paths = []
+        for p in self.custom_skill_paths:
+            path_obj = Path(p)
+            if path_obj.is_dir():
+                additional_search_paths.append(str(path_obj.resolve()))
+            elif path_obj.is_file():
+                additional_search_paths.append(str(path_obj.resolve().parent))
+            else:
+                logging.warning(f"Custom skill path does not exist, skipping: {p}")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@holmes/core/toolset_manager.py` around lines 121 - 127, The current
construction of additional_search_paths silently treats non-existent
custom_skill_paths as files by falling back to
os.path.dirname(os.path.abspath(str(p))) when Path(p).is_dir() is False;
instead, in the block building additional_search_paths (referencing
self.custom_skill_paths, Path(p).is_dir(), and os.path.dirname/os.path.abspath),
validate each p exists with Path(p).exists() and: if it is a directory use
Path(p).resolve(), if it is an existing file use
os.path.dirname(os.path.abspath(str(p))), otherwise log a warning or raise an
error reporting the invalid path (include the original p and its resolved form)
so typos/non-existent paths are not silently converted into unrelated parent
directories.
holmes/core/prompt.py (1)

208-209: Simplify the skills_enabled expression.

SkillCatalog always defines a skills field, so the getattr(..., "skills", True) fallback is dead — and using True as the default is misleading (it would force-enable when the attribute is missing, which is the opposite of the intent). A direct check is clearer:

♻️ Proposed simplification
-        "skills_enabled": bool(skills and getattr(skills, "skills", True))
-        and is_enabled(PromptComponent.TIME_SKILLS),
+        "skills_enabled": bool(skills and skills.skills)
+        and is_enabled(PromptComponent.TIME_SKILLS),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@holmes/core/prompt.py` around lines 208 - 209, Replace the convoluted
expression used to compute skills_enabled with a direct check because
SkillCatalog always has a skills field; change the expression to use a simple
boolean check of skills combined with the existing feature flag check, e.g. set
"skills_enabled": bool(skills) and is_enabled(PromptComponent.TIME_SKILLS) in
the same place where the current expression is computed.
holmes/plugins/skills/skill_loader.py (1)

137-141: Move RobustaSkillInstruction import to a TYPE_CHECKING block and drop the dead runtime import.

Several problems on these lines:

  1. The annotation instr: "RobustaSkillInstruction" is a forward reference but the name is never imported at module scope (Ruff F821 in static analysis).
  2. The function-local from holmes.plugins.runbooks import RobustaSkillInstruction is unused at runtime — the body only accesses attributes on instr, never the class itself.
  3. # noqa: F811 is the wrong rule code (F811 is "redefinition of unused"); the actual diagnostic here is F821.
  4. Function-local imports violate the repo coding guideline: “ALWAYS place Python imports at the top of the file, not inside functions or methods.”
♻️ Proposed cleanup
 if TYPE_CHECKING:
     from holmes.core.supabase_dal import SupabaseDal
+    from holmes.plugins.runbooks import RobustaSkillInstruction
@@
 def map_robusta_instruction_to_skill(
     instr: "RobustaSkillInstruction",
 ) -> Skill:
     """Convert a Supabase RobustaSkillInstruction into a Skill."""
-    from holmes.plugins.runbooks import RobustaSkillInstruction  # noqa: F811
-
     description = instr.title

As per coding guidelines: "ALWAYS place Python imports at the top of the file, not inside functions or methods".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@holmes/plugins/skills/skill_loader.py` around lines 137 - 141, The
function-local import of RobustaSkillInstruction in
map_robusta_instruction_to_skill should be removed and the name imported only
for type checking: add "from typing import TYPE_CHECKING" at module top and
inside an "if TYPE_CHECKING:" block import "RobustaSkillInstruction" from
holmes.plugins.runbooks; remove the in-function "from holmes.plugins.runbooks
import RobustaSkillInstruction" and the "# noqa: F811" comment, keeping the
parameter annotation as the forward-reference string "RobustaSkillInstruction"
so static checkers (Ruff F821) are satisfied without a runtime import.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/reference/http-api.md`:
- Line 124: Update the docs and the config parsing to avoid silently breaking
existing integrations: in docs/reference/http-api.md add a prominent migration
note calling out the renaming from time_runbooks -> time_skills and instruct
users to update any behavior_controls / ENABLED_PROMPTS entries; additionally
update the server/config parsing logic that reads
ENABLED_PROMPTS/behavior_controls to accept both keys (time_runbooks as
deprecated alias for time_skills) and emit a deprecation warning when
time_runbooks is present, so integrations keep working while users migrate.

In `@holmes/plugins/skills/skill_loader.py`:
- Around line 137-154: map_robusta_instruction_to_skill is converting
catalog-only RobustaSkillInstruction entries into Skill objects with empty
content (using instr.instruction or ""), which causes remote skills loaded by
get_skill_catalog() to be treated as fully-resolved and never fetched from DAL;
change the runtime behavior so catalog entries remain metadata-only and are
always resolved via the DAL in skills_fetcher._invoke by making
_find_skill/_format_skill_result treat SkillSource.REMOTE entries as pointers
(call dal.get_skill_content(skill.name) to populate content before returning
SUCCESS) rather than returning the empty content, and move the
RobustaSkillInstruction import in skill_loader.py out of the function and into
the top-level TYPE_CHECKING imports to avoid the inline import.

In `@holmes/plugins/toolsets/skills/skills_fetcher.py`:
- Around line 139-149: The remote skill returned by _get_robusta_skill currently
returns raw YAML from RobustaSkillInstruction via skill_content.pretty(), so
update _get_robusta_skill to wrap that content in the same <skill> tag and LLM
instruction context used by _format_skill_result() (i.e., replicate the exact
wrapper and prompt lines used in _format_skill_result) before placing it into
the StructuredToolResult.data field; preserve the same StructuredToolResult
status, params, and metadata shape while ensuring the final data string matches
the local-skill format expected by the LLM.
- Around line 73-76: The endswith(".md") check in the skills_fetcher fallback is
dead/ misleading; remove the `not skill_id.endswith(".md")` condition from the
fallback branch that calls self._get_robusta_skill(skill_id, params) so the
DAL-based UUID-path fallback uses only the intended checks (self._dal and
self._dal.enabled), or if you intended to block path-like inputs instead add an
explicit validation step using normalize_skill_name/available_skills and a short
comment explaining the purpose; update the branch around `_get_robusta_skill`
and the surrounding comment accordingly to reflect the chosen behavior.

In `@tests/llm/fixtures/test_ask_holmes/162_get_skills/test_case.yaml`:
- Line 7: Update the inline comment that currently reads "simulates loading
runbooks and global instructions from a cluster environment" to use the new
terminology "skills" (e.g., "simulates loading skills and global instructions
from a cluster environment") so the YAML fixture comment reflects the migration;
locate the exact comment string in
tests/llm/fixtures/test_ask_holmes/162_get_skills/test_case.yaml and replace
"runbooks" with "skills".

In `@tests/llm/utils/mock_dal.py`:
- Around line 58-82: The fixture lookups in get_skill_catalog and
get_skill_content still hard-code the "runbook_" prefix (via calls to
_get_fixture_file_path with "runbook_catalog" and
f"runbook_content_{skill_id}"); either (A) add a concise inline comment above
both get_skill_catalog and get_skill_content explaining the prefix is
intentionally retained to match existing fixtures, or (B) update these calls to
use the new canonical prefix (e.g., "skill_catalog" and
f"skill_content_{skill_id}") and migrate/rename the corresponding fixture files
in the test fixtures set in one pass; ensure you update all uses of
_get_fixture_file_path in these methods and keep error handling/logging
unchanged.

In `@tests/test_skill_prompt.py`:
- Line 10: DummySkillCatalog defines a mutable class attribute skills = [True];
change it to an immutable type (e.g., skills = (True,) or frozenset({True})) to
avoid shared mutable state and RUF012; update the DummySkillCatalog class
definition to use the chosen immutable container so each test instance no longer
shares a mutable list.

---

Outside diff comments:
In `@holmes/interactive.py`:
- Around line 2254-2262: The parameter "skills" in run_interactive_loop is
untyped; add an explicit type hint (e.g., change it to skills:
Optional[Sequence[Skill]]) and update imports accordingly (import Sequence from
typing and import or reference the Skill type from its definition). Modify the
function signature in run_interactive_loop to use the new type and ensure any
callers/assignments still satisfy Optional[Sequence[Skill]]; if Skill lives in
another module, add the appropriate import (or use a forward-reference string
"Skill") to keep mypy happy.

In `@tests/llm/fixtures/test_ask_holmes/162_get_skills/test_case.yaml`:
- Around line 52-53: Update the YAML expected_output text for the test case
identifier "162_get_skills" by replacing "runbook" wording with "skill" (e.g.,
change "The runbook provides..." to "The skill provides...", "The runbook should
specifically mention..." to "The skill should specifically mention...") while
keeping the rest of the sentence intact; edit the value under the
expected_output key so all occurrences of "runbook" are switched to "skill".

In `@tests/llm/fixtures/test_ask_holmes/90_skill_basic_selection/test_case.yaml`:
- Around line 7-13: The expected_output in test_case.yaml still uses "runbook"
wording; update the strings in the expected_output block to use "skill" instead
(e.g., change "Application Gateway troubleshooting runbook" to "Application
Gateway troubleshooting skill" and "as per the runbook instructions" to "as per
the skill instructions"), and ensure any other occurrences like "runbook
instructions" or "runbook" are replaced with the corresponding "skill" phrasing
to reflect the runbooks→skills migration (file: the expected_output field in the
test case for 90_skill_basic_selection, tag 'skills').

In `@tests/llm/fixtures/test_ask_holmes/96_no_matching_skill/test_case.yaml`:
- Around line 1-12: Update the test fixture strings to match the PR-wide rename
from "runbook(s)" to "skill(s)": change the user_prompt value from "Use the
custom-crd runbook..." to "Use the custom-crd skill..." (or similar), and revise
all occurrences in expected_output that mention "runbook" or "runbooks" to
"skill" or "skills" so the prompt, tags, and assertions consistently use the
"skills" terminology; target the user_prompt and expected_output fields in this
fixture (test_ask_holmes/96_no_matching_skill/test_case.yaml) and keep the
original meaning of each assertion.

---

Duplicate comments:
In `@holmes/plugins/prompts/_skills_instructions.jinja2`:
- Around line 1-9: The partial filename is inconsistent: the new template is
`_skills_instructions.jinja2` but `base_user_prompt.jinja2` (and other
templates) include `_skill_instructions.jinja2`; pick one canonical name (e.g.,
`_skills_instructions.jinja2`) and update all include statements to match that
name across templates mentioned in the PR (`base_user_prompt.jinja2`,
`investigation_procedure.jinja2`, `_general_instructions.jinja2`), or rename the
file to the singular form if you prefer that convention, ensuring the include
targets and file name are identical and updating any import/include references
accordingly.

---

Nitpick comments:
In @.gitignore:
- Line 173: Remove the duplicate ignore entry for ".claude/skills/dev-skills/":
locate both occurrences of the exact rule ".claude/skills/dev-skills/" in the
.gitignore and delete the redundant one so only a single ignore line for that
path remains.

In `@holmes/core/prompt.py`:
- Around line 208-209: Replace the convoluted expression used to compute
skills_enabled with a direct check because SkillCatalog always has a skills
field; change the expression to use a simple boolean check of skills combined
with the existing feature flag check, e.g. set "skills_enabled": bool(skills)
and is_enabled(PromptComponent.TIME_SKILLS) in the same place where the current
expression is computed.

In `@holmes/core/toolset_manager.py`:
- Around line 121-127: The current construction of additional_search_paths
silently treats non-existent custom_skill_paths as files by falling back to
os.path.dirname(os.path.abspath(str(p))) when Path(p).is_dir() is False;
instead, in the block building additional_search_paths (referencing
self.custom_skill_paths, Path(p).is_dir(), and os.path.dirname/os.path.abspath),
validate each p exists with Path(p).exists() and: if it is a directory use
Path(p).resolve(), if it is an existing file use
os.path.dirname(os.path.abspath(str(p))), otherwise log a warning or raise an
error reporting the invalid path (include the original p and its resolved form)
so typos/non-existent paths are not silently converted into unrelated parent
directories.

In `@holmes/plugins/runbooks/__init__.py`:
- Around line 1-37: The module name/package (runbooks) no longer matches its
contents (skills): move the module out of the runbooks package into the skills
package and update all imports and exports that reference
RobustaSkillInstruction (and any top-level symbols in this file), adjust package
__init__ exports, and fix any tests or code that import the old runbooks module
so they now import from skills; ensure the class name RobustaSkillInstruction,
its docstring, and the yaml dumper behavior remain unchanged during the move.

In `@holmes/plugins/skills/skill_loader.py`:
- Around line 137-141: The function-local import of RobustaSkillInstruction in
map_robusta_instruction_to_skill should be removed and the name imported only
for type checking: add "from typing import TYPE_CHECKING" at module top and
inside an "if TYPE_CHECKING:" block import "RobustaSkillInstruction" from
holmes.plugins.runbooks; remove the in-function "from holmes.plugins.runbooks
import RobustaSkillInstruction" and the "# noqa: F811" comment, keeping the
parameter annotation as the forward-reference string "RobustaSkillInstruction"
so static checkers (Ruff F821) are satisfied without a runtime import.

In `@holmes/plugins/toolsets/internet/internet.py`:
- Line 181: Update the ambiguous description for the fetch_webpage tool (look
for the description string associated with fetch_webpage in internet.py) so it
no longer implies the tool "fetches skills"; instead state that it fetches any
URL referenced by a skill or investigation and should be used when no more
specific tool (e.g., Confluence) applies—for example: "Fetch any URL referenced
by a skill before starting your investigation when no more specific tool like
Confluence applies." Ensure the change replaces the current phrasing exactly in
the description value for fetch_webpage.

In `@server.py`:
- Around line 408-412: Update the user-facing strings in the follow-up action
with id="articles" (action_label="Articles") to reflect the rename from
"runbooks" to "skills": change the prompt value ("List the relevant runbooks and
links used. Write a short summary for each") and pre_action_notification_text
("Looking up and summarizing runbooks and links...") to use "skills" (or another
intended term) so wording is consistent across the codebase.

In
`@tests/llm/fixtures/test_ask_holmes/165_alert_with_multiple_skills/test_case.yaml`:
- Around line 1-2: Update the header comments in the fixture to use the new
terminology: replace the singular "runbook" with "skill" in the first line and
fix the misspelled "runbboks" to "skills" in the second line so both lines
consistently reference "skill/skills" instead of "runbook/runbboks".

In
`@tests/llm/fixtures/test_ask_holmes/176_network_policy_blocking_traffic_no_skills/toolsets.yaml`:
- Line 4: Update the inline comment next to the "internet:" entry in
toolsets.yaml to mention "skills" instead of "runbook" (e.g., change "because
runbook is disabled" to "because skills are disabled" or similar) so the comment
reflects the renamed concept used by Holmes when fetching
networking/dns_troubleshooting_instructions.md.

In
`@tests/llm/fixtures/test_ask_holmes/210_confluence_skill_fetch/test_case.yaml`:
- Line 1: Update the test header comment to replace the outdated phrase
"internal runbook tool" with the guarded symbol `fetch_skill` so it reads
something like "Fetch runbook from Confluence (must use Confluence toolset, not
fetch_skill)"; locate and edit the header string in the test fixture for the
Confluence skill (the comment at the top of
test_ask_holmes/210_confluence_skill_fetch/test_case.yaml) and make the wording
consistent with the current guard.

In
`@tests/llm/fixtures/test_ask_holmes/90_skill_basic_selection/application-gateway-troubleshooting/SKILL.md`:
- Line 8: Replace the term "runbook" with "skill" in the overview sentence that
currently reads "This runbook helps troubleshoot common Application Gateway
issues including 502 Bad Gateway errors." (search for the string "This runbook
helps troubleshoot" or the token "runbook" in SKILL.md) to maintain consistent
terminology with the migration; update the sentence to "This skill helps
troubleshoot common Application Gateway issues including 502 Bad Gateway
errors."

In
`@tests/llm/fixtures/test_ask_holmes/94_skill_transparency/database-troubleshooting/SKILL.md`:
- Line 8: The body text uses the phrase "this runbook" which is inconsistent
with the file now being SKILL.md; update the sentence "This runbook helps
diagnose database connection timeout issues in Kubernetes applications." to use
consistent terminology (e.g., "This skill helps diagnose database connection
timeout issues in Kubernetes applications." or "This document helps
diagnose...") so SKILL.md wording matches file purpose and header; search for
the exact phrase "This runbook helps diagnose database connection timeout issues
in Kubernetes applications." to locate and replace it.

In `@tests/llm/fixtures/test_ask_holmes/94_skill_transparency/test_case.yaml`:
- Around line 7-11: Update the stale wording in the test fixture's
expected_output: locate the expected_output block in
tests/llm/fixtures/test_ask_holmes/94_skill_transparency/test_case.yaml and
replace "Uses database troubleshooting runbook" with wording that uses the new
vocabulary (for example "Uses database troubleshooting skill") so the assertion
matches the migrated runbooks→skills terminology; ensure the rest of the
bulleted expectations remain unchanged and that the exact string in
expected_output aligns with the prompt/system output for the skill-based flow.

In `@tests/plugins/toolsets/test_skills.py`:
- Around line 50-57: The test test_SkillsFetcher_empty_id only asserts behavior
for an empty string and misses the whitespace-only contract; add a new assertion
(or extend the existing test) that calls SkillsFetcher(SkillsToolset())._invoke
with {"skill_id": "   "} (or similar whitespace) using
create_mock_tool_invoke_context() and assert the returned result.status is
StructuredToolResultStatus.ERROR to ensure whitespace-only IDs are treated like
empty IDs by the SkillsFetcher._invoke handling.
- Around line 1-13: Remove the unused top-level import and constant: delete the
import of os and the TEST_SKILLS_PATH constant declaration in test_skills.py
(they are unused symbols `os` and `TEST_SKILLS_PATH`), leaving only the
necessary imports from holmes and tests; ensure no other code references
`TEST_SKILLS_PATH` before committing.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 124e299d-0ff3-4030-b209-eb6f0a3bd85a

📥 Commits

Reviewing files that changed from the base of the PR and between 2be63a9 and d3fabc4.

📒 Files selected for processing (127)
  • .gitignore
  • docs/reference/http-api.md
  • holmes/config.py
  • holmes/core/conversations.py
  • holmes/core/prompt.py
  • holmes/core/supabase_dal.py
  • holmes/core/tool_calling_llm.py
  • holmes/core/tools.py
  • holmes/core/tools_utils/tool_executor.py
  • holmes/core/toolset_manager.py
  • holmes/interactive.py
  • holmes/main.py
  • holmes/plugins/prompts/_general_instructions.jinja2
  • holmes/plugins/prompts/_runbook_instructions.jinja2
  • holmes/plugins/prompts/_runbooks_instructions.jinja2
  • holmes/plugins/prompts/_skill_instructions.jinja2
  • holmes/plugins/prompts/_skills_instructions.jinja2
  • holmes/plugins/prompts/base_user_prompt.jinja2
  • holmes/plugins/prompts/investigation_procedure.jinja2
  • holmes/plugins/runbooks/__init__.py
  • holmes/plugins/skills/__init__.py
  • holmes/plugins/skills/builtin/__init__.py
  • holmes/plugins/skills/skill_loader.py
  • holmes/plugins/toolsets/__init__.py
  • holmes/plugins/toolsets/internet/internet.py
  • holmes/plugins/toolsets/investigator/investigator_instructions.jinja2
  • holmes/plugins/toolsets/runbook/runbook_fetcher.py
  • holmes/plugins/toolsets/skills/__init__.py
  • holmes/plugins/toolsets/skills/skills_fetcher.py
  • holmes/utils/global_instructions.py
  • pyproject.toml
  • server.py
  • tests/config_class/test_custom_runbook_catalogs.py
  • tests/config_class/test_custom_skill_paths.py
  • tests/core/test_prompt.py
  • tests/core/test_toolset_manager.py
  • tests/llm/fixtures/test_ask_holmes/104b_postgres_missing_index_pgstat/postgres-performance/SKILL.md
  • tests/llm/fixtures/test_ask_holmes/104b_postgres_missing_index_pgstat/test_case.yaml
  • tests/llm/fixtures/test_ask_holmes/158_slack_chat_correct_date/toolsets.yaml
  • tests/llm/fixtures/test_ask_holmes/162_get_skills/app/payments-deployment.yaml
  • tests/llm/fixtures/test_ask_holmes/162_get_skills/app/secrets.yaml
  • tests/llm/fixtures/test_ask_holmes/162_get_skills/app/signup-deployment.yaml
  • tests/llm/fixtures/test_ask_holmes/162_get_skills/global_instructions.json
  • tests/llm/fixtures/test_ask_holmes/162_get_skills/issue_data.json
  • tests/llm/fixtures/test_ask_holmes/162_get_skills/resource_instructions.json
  • tests/llm/fixtures/test_ask_holmes/162_get_skills/runbook_catalog.json
  • tests/llm/fixtures/test_ask_holmes/162_get_skills/runbook_content_40296b5c-2cb5-41df-b1f5-93441f894c44.json
  • tests/llm/fixtures/test_ask_holmes/162_get_skills/runbook_content_8fe8e24d-6b53-47a6-92a5-b389938fa823.json
  • tests/llm/fixtures/test_ask_holmes/162_get_skills/runbook_content_b2ffd311-f339-46a2-b379-1e53eb0bc1ed.json
  • tests/llm/fixtures/test_ask_holmes/162_get_skills/test_case.yaml
  • tests/llm/fixtures/test_ask_holmes/165_alert_with_multiple_skills/issue_data.json
  • tests/llm/fixtures/test_ask_holmes/165_alert_with_multiple_skills/pod_not_ready.yaml
  • tests/llm/fixtures/test_ask_holmes/165_alert_with_multiple_skills/runbook_catalog.json
  • tests/llm/fixtures/test_ask_holmes/165_alert_with_multiple_skills/runbook_content_40296b5c-2cb5-41df-b1f5-93441f894c44.json
  • tests/llm/fixtures/test_ask_holmes/165_alert_with_multiple_skills/runbook_content_8fe8e24d-6b53-47a6-92a5-b389938fa823.json
  • tests/llm/fixtures/test_ask_holmes/165_alert_with_multiple_skills/runbook_content_b2ffd311-f339-46a2-b379-1e53eb0bc1ed.json
  • tests/llm/fixtures/test_ask_holmes/165_alert_with_multiple_skills/test_case.yaml
  • tests/llm/fixtures/test_ask_holmes/176_network_policy_blocking_traffic_no_skills/backend.yaml
  • tests/llm/fixtures/test_ask_holmes/176_network_policy_blocking_traffic_no_skills/frontend.yaml
  • tests/llm/fixtures/test_ask_holmes/176_network_policy_blocking_traffic_no_skills/manifest.yaml
  • tests/llm/fixtures/test_ask_holmes/176_network_policy_blocking_traffic_no_skills/test_case.yaml
  • tests/llm/fixtures/test_ask_holmes/176_network_policy_blocking_traffic_no_skills/toolsets.yaml
  • tests/llm/fixtures/test_ask_holmes/197_bash_secrets_denied/test_case.yaml
  • tests/llm/fixtures/test_ask_holmes/208_confluence_page_fetch/test_case.yaml
  • tests/llm/fixtures/test_ask_holmes/209_confluence_url_lookup/test_case.yaml
  • tests/llm/fixtures/test_ask_holmes/210_confluence_skill_fetch/test_case.yaml
  • tests/llm/fixtures/test_ask_holmes/210_confluence_skill_fetch/toolsets.yaml
  • tests/llm/fixtures/test_ask_holmes/232_newline_in_tool_params/test_case.yaml
  • tests/llm/fixtures/test_ask_holmes/256_hello_token_limit/test_case.yaml
  • tests/llm/fixtures/test_ask_holmes/25_misconfigured_ingress_class/toolsets.yaml
  • tests/llm/fixtures/test_ask_holmes/42_dns_issues_result_new_tools/test_case.yaml
  • tests/llm/fixtures/test_ask_holmes/42_dns_issues_result_new_tools_no_runbook/test_case.yaml
  • tests/llm/fixtures/test_ask_holmes/42_dns_issues_result_old_tools/test_case.yaml
  • tests/llm/fixtures/test_ask_holmes/42_dns_issues_steps_new_all_tools/test_case.yaml
  • tests/llm/fixtures/test_ask_holmes/42_dns_issues_steps_new_tools/test_case.yaml
  • tests/llm/fixtures/test_ask_holmes/42_dns_issues_steps_old_tools/test_case.yaml
  • tests/llm/fixtures/test_ask_holmes/55_kafka_skill/app/README.md
  • tests/llm/fixtures/test_ask_holmes/55_kafka_skill/app/accounting-app/Dockerfile
  • tests/llm/fixtures/test_ask_holmes/55_kafka_skill/app/accounting-app/app.py
  • tests/llm/fixtures/test_ask_holmes/55_kafka_skill/app/accounting-app/build_and_publish.sh
  • tests/llm/fixtures/test_ask_holmes/55_kafka_skill/app/accounting-app/requirements.txt
  • tests/llm/fixtures/test_ask_holmes/55_kafka_skill/app/build_and_publish_all.sh
  • tests/llm/fixtures/test_ask_holmes/55_kafka_skill/app/finance-app/Dockerfile
  • tests/llm/fixtures/test_ask_holmes/55_kafka_skill/app/finance-app/app.py
  • tests/llm/fixtures/test_ask_holmes/55_kafka_skill/app/finance-app/build_and_publish.sh
  • tests/llm/fixtures/test_ask_holmes/55_kafka_skill/app/finance-app/requirements.txt
  • tests/llm/fixtures/test_ask_holmes/55_kafka_skill/app/invoices-app/Dockerfile
  • tests/llm/fixtures/test_ask_holmes/55_kafka_skill/app/invoices-app/app.py
  • tests/llm/fixtures/test_ask_holmes/55_kafka_skill/app/invoices-app/build_and_publish.sh
  • tests/llm/fixtures/test_ask_holmes/55_kafka_skill/app/invoices-app/requirements.txt
  • tests/llm/fixtures/test_ask_holmes/55_kafka_skill/app/kafka-manifest.yaml
  • tests/llm/fixtures/test_ask_holmes/55_kafka_skill/app/orders-app/Dockerfile
  • tests/llm/fixtures/test_ask_holmes/55_kafka_skill/app/orders-app/app.py
  • tests/llm/fixtures/test_ask_holmes/55_kafka_skill/app/orders-app/build_and_publish.sh
  • tests/llm/fixtures/test_ask_holmes/55_kafka_skill/app/orders-app/requirements.txt
  • tests/llm/fixtures/test_ask_holmes/55_kafka_skill/kafka_lag_instructions.md
  • tests/llm/fixtures/test_ask_holmes/55_kafka_skill/test_case.yaml
  • tests/llm/fixtures/test_ask_holmes/55_kafka_skill/toolsets.yaml
  • tests/llm/fixtures/test_ask_holmes/57_cluster_name_confusion/toolsets.yaml
  • tests/llm/fixtures/test_ask_holmes/89_skill_missing_cloudwatch/lambda-performance-troubleshooting/SKILL.md
  • tests/llm/fixtures/test_ask_holmes/89_skill_missing_cloudwatch/test_case.yaml
  • tests/llm/fixtures/test_ask_holmes/89_skill_missing_cloudwatch/toolsets.yaml
  • tests/llm/fixtures/test_ask_holmes/90_skill_basic_selection/application-gateway-troubleshooting/SKILL.md
  • tests/llm/fixtures/test_ask_holmes/90_skill_basic_selection/test_case.yaml
  • tests/llm/fixtures/test_ask_holmes/94_skill_transparency/database-troubleshooting/SKILL.md
  • tests/llm/fixtures/test_ask_holmes/94_skill_transparency/manifest.yaml
  • tests/llm/fixtures/test_ask_holmes/94_skill_transparency/test_case.yaml
  • tests/llm/fixtures/test_ask_holmes/95_skill_memory_leak_detection/manifest.yaml
  • tests/llm/fixtures/test_ask_holmes/95_skill_memory_leak_detection/python-memory-troubleshooting/SKILL.md
  • tests/llm/fixtures/test_ask_holmes/95_skill_memory_leak_detection/test_case.yaml
  • tests/llm/fixtures/test_ask_holmes/96_no_matching_skill/manifest.yaml
  • tests/llm/fixtures/test_ask_holmes/96_no_matching_skill/service-troubleshooting/SKILL.md
  • tests/llm/fixtures/test_ask_holmes/96_no_matching_skill/test_case.yaml
  • tests/llm/test_ask_holmes.py
  • tests/llm/utils/braintrust.py
  • tests/llm/utils/default_toolsets.yaml
  • tests/llm/utils/mock_dal.py
  • tests/llm/utils/test_case_utils.py
  • tests/llm/utils/test_toolset.py
  • tests/plugins/runbooks/test_catalog.py
  • tests/plugins/toolsets/fixtures/skills/test_runbook.md
  • tests/plugins/toolsets/test_runbook.py
  • tests/plugins/toolsets/test_skills.py
  • tests/test_cache.py
  • tests/test_interactive.py
  • tests/test_scheduled_prompts.py
  • tests/test_skill_prompt.py
💤 Files with no reviewable changes (7)
  • holmes/plugins/prompts/_runbooks_instructions.jinja2
  • tests/llm/fixtures/test_ask_holmes/104b_postgres_missing_index_pgstat/test_case.yaml
  • holmes/plugins/prompts/_runbook_instructions.jinja2
  • tests/plugins/toolsets/test_runbook.py
  • tests/config_class/test_custom_runbook_catalogs.py
  • tests/plugins/runbooks/test_catalog.py
  • holmes/plugins/toolsets/runbook/runbook_fetcher.py

Comment thread docs/reference/http-api.md
Comment thread holmes/plugins/toolsets/skills/skills_fetcher.py
Comment thread holmes/plugins/toolsets/skills/skills_fetcher.py
Comment thread tests/llm/fixtures/test_ask_holmes/162_get_skills/test_case.yaml Outdated
Comment thread tests/llm/utils/mock_dal.py
Comment thread tests/test_skill_prompt.py Outdated
Avi-Robusta and others added 4 commits April 26, 2026 10:04
Critical: Remote skills from Supabase catalog had empty content,
causing fetch_skill to return empty <skill> blocks instead of
fetching actual content from DAL. Fixed by routing REMOTE skills
through _get_robusta_skill which calls dal.get_skill_content().

Major: Remote skill content from Supabase was returned as raw YAML
without the <skill> tag wrapping and LLM instructions that local
skills have. Fixed by wrapping remote content through the same
_format_skill_result path.

Minor fixes:
- RobustaRunbookInstruction -> RobustaSkillInstruction
- get_runbook_catalog -> get_skill_catalog (DAL method)
- get_runbook_content -> get_skill_content (DAL method)
- DummySkillCatalog.skills: mutable list -> tuple
- Updated stale comment in 162_get_skills fixture
- Added clarifying comment in mock_dal.py about fixture file prefix

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: avi@robusta.dev <avi@robusta.dev>
Accidentally committed in previous commit. This is a local
Claude Code config file that shouldn't be tracked.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: avi@robusta.dev <avi@robusta.dev>
…skills

Signed-off-by: avi@robusta.dev <avi@robusta.dev>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
holmes/plugins/toolsets/skills/skills_fetcher.py (3)

26-26: Use Field(default_factory=list) for the Pydantic field.

Ruff RUF012 flags the mutable default. Pydantic v2 actually copies model-field defaults safely, so this is not a real bug, but switching to Field(default_factory=list) keeps Ruff quiet and makes intent explicit. Same is also unnecessary because available_skills is always passed via super().__init__(...) in the constructor; consider dropping the class-level default entirely and just declaring the type.

♻️ Suggested change
-from typing import List, Optional
+from typing import List, Optional
+
+from pydantic import Field
@@
-    available_skills: List[str] = []
+    available_skills: List[str] = Field(default_factory=list)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@holmes/plugins/toolsets/skills/skills_fetcher.py` at line 26, Replace the
mutable class-level default for the Pydantic field available_skills with an
explicit Field(default_factory=list) (import Field from pydantic) or remove the
class-level default entirely and only declare the type if the constructor always
supplies it; update the declaration of available_skills to use
Field(default_factory=list) to satisfy Ruff RUF012 and make intent explicit
(look for the available_skills field in the SkillsFetcher model/class and adjust
the import/annotation accordingly).

165-166: Minor: prefer {e!s} over str(e) (RUF010).

A small style nit flagged by Ruff. The blind except Exception itself is reasonable here since it's guarding an external Supabase call.

♻️ Suggested change
-            except Exception as e:
-                err_msg = f"Failed to fetch skill with UUID '{link}': {str(e)}"
+            except Exception as e:
+                err_msg = f"Failed to fetch skill with UUID '{link}': {e!s}"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@holmes/plugins/toolsets/skills/skills_fetcher.py` around lines 165 - 166, In
the except block that builds err_msg (currently "err_msg = f\"Failed to fetch
skill with UUID '{link}': {str(e)}\"") change the f-string to use the !s
conversion for the exception (e.g., {e!s}) instead of calling str(e); update the
err_msg assignment in the same except clause in skills_fetcher.py so it becomes:
err_msg = f"Failed to fetch skill with UUID '{link}': {e!s}".

44-44: Inconsistent terminology across description, parameter, and error strings.

The tool advertises a skill_id parameter ("either a UUID or a skill name"), but the description and error strings interchange "skill link", "skill path", and "skill_id". This shows up in the LLM-visible description (line 44 "by skill link"), the empty-input error (line 62 "Skill link cannot be empty. Please provide a valid skill path."), the no-DAL error (line 174), and the one-liner (line 184 uses the local path variable for skill_id). Standardizing on "skill_id" (or "skill") across these strings reduces LLM confusion and matches the actual parameter name.

Also applies to: 47-47, 62-62, 174-174, 183-184

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@holmes/plugins/toolsets/skills/skills_fetcher.py` at line 44, Standardize all
LLM-visible terminology to use "skill_id" (or "skill") instead of mixed terms
like "skill link" or "skill path": update the tool description string that
currently says "by skill link" to reference "by skill_id", change the
empty-input error ("Skill link cannot be empty...") to "Skill_id cannot be
empty. Please provide a valid skill_id.", update the no-DAL error to use
"skill_id", and ensure the one-liner that currently uses the local variable path
maps/renames it to skill_id (or uses skill_id when building the response) so
parameter names and error messages consistently reference skill_id across the
functions and messages in skills_fetcher.py.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@holmes/plugins/toolsets/skills/skills_fetcher.py`:
- Around line 173-180: The error message in the no-DAL branch of
skills_fetcher.py is misleading because it assumes the link is a UUID; update
the err_msg to correctly reflect that a remote skill (use the variable link)
requires the data access layer (DAL) which is not enabled, e.g. "Remote skill
'<link>' requires the data access layer (DAL), which is not enabled."; keep the
same control flow and return
StructuredToolResult(status=StructuredToolResultStatus.ERROR, error=err_msg,
params=params) so callers receive the clearer message.

---

Nitpick comments:
In `@holmes/plugins/toolsets/skills/skills_fetcher.py`:
- Line 26: Replace the mutable class-level default for the Pydantic field
available_skills with an explicit Field(default_factory=list) (import Field from
pydantic) or remove the class-level default entirely and only declare the type
if the constructor always supplies it; update the declaration of
available_skills to use Field(default_factory=list) to satisfy Ruff RUF012 and
make intent explicit (look for the available_skills field in the SkillsFetcher
model/class and adjust the import/annotation accordingly).
- Around line 165-166: In the except block that builds err_msg (currently
"err_msg = f\"Failed to fetch skill with UUID '{link}': {str(e)}\"") change the
f-string to use the !s conversion for the exception (e.g., {e!s}) instead of
calling str(e); update the err_msg assignment in the same except clause in
skills_fetcher.py so it becomes: err_msg = f"Failed to fetch skill with UUID
'{link}': {e!s}".
- Line 44: Standardize all LLM-visible terminology to use "skill_id" (or
"skill") instead of mixed terms like "skill link" or "skill path": update the
tool description string that currently says "by skill link" to reference "by
skill_id", change the empty-input error ("Skill link cannot be empty...") to
"Skill_id cannot be empty. Please provide a valid skill_id.", update the no-DAL
error to use "skill_id", and ensure the one-liner that currently uses the local
variable path maps/renames it to skill_id (or uses skill_id when building the
response) so parameter names and error messages consistently reference skill_id
across the functions and messages in skills_fetcher.py.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ecb70fbc-6c7d-47e4-9aa2-c7270960be2e

📥 Commits

Reviewing files that changed from the base of the PR and between d3fabc4 and e358624.

📒 Files selected for processing (6)
  • .gitignore
  • holmes/plugins/toolsets/skills/skills_fetcher.py
  • tests/core/test_prompt.py
  • tests/llm/fixtures/test_ask_holmes/162_get_skills/test_case.yaml
  • tests/llm/utils/mock_dal.py
  • tests/test_skill_prompt.py
✅ Files skipped from review due to trivial changes (1)
  • tests/llm/fixtures/test_ask_holmes/162_get_skills/test_case.yaml
🚧 Files skipped from review as they are similar to previous changes (2)
  • .gitignore
  • tests/test_skill_prompt.py

Comment thread holmes/plugins/toolsets/skills/skills_fetcher.py
…bing

- Add SkillsConfig with local_skills_path field for toolset-level config
- Restore custom_skill_paths on Config and ToolsetManager for backwards compat
- Both paths merge in SkillsToolset: top-level custom_skill_paths +
  toolsets.skills.config.local_skills_path
- Remove unnecessary _deprecated_mappings for additional_search_paths
  (was never user-facing)
- Restore config tests for custom_skill_paths
- Remove obsolete toolset_manager skill path tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: avi@robusta.dev <avi@robusta.dev>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
holmes/plugins/toolsets/skills/skills_fetcher.py (1)

186-193: ⚠️ Potential issue | 🟡 Minor

Misleading error wording in the no-DAL branch.

This branch is also reached from line 92–93 when the catalog matched a REMOTE skill but DAL is disabled — link is a skill name in that path, not a UUID. Reword to remove the UUID assumption, e.g.:

✏️ Proposed wording
-            err_msg = "Skill link appears to be a UUID, but no remote data access layer (dal) is enabled."
+            err_msg = (
+                f"Cannot fetch remote skill '{link}': the data access layer (DAL) is not enabled."
+            )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@holmes/plugins/toolsets/skills/skills_fetcher.py` around lines 186 - 193, The
error message in the no-DAL branch (where err_msg is set before returning a
StructuredToolResult with StructuredToolResultStatus.ERROR) wrongly assumes the
link is a UUID; change the err_msg to a neutral message that does not assume
UUIDs (e.g., indicate the skill/link appears to be remote or requires a remote
data access layer but DAL is disabled) and keep the rest of the return values
(StructuredToolResult, error=err_msg, params) unchanged so callers still receive
the same error type and params.
🧹 Nitpick comments (2)
holmes/plugins/toolsets/skills/skills_fetcher.py (2)

178-185: Narrow the exception and use the f-string conversion flag.

Catching Exception here is overly broad and Ruff flags str(e) (RUF010). Per the coding guideline to surface detailed error context for LLM self-correction, the message is fine but consider letting truly unexpected errors propagate while keeping the wrapping for known DAL/transport failures. Minimal cleanup:

♻️ Proposed cleanup
-            except Exception as e:
-                err_msg = f"Failed to fetch skill with UUID '{link}': {str(e)}"
-                logging.error(err_msg)
+            except Exception as e:  # noqa: BLE001 - DAL/network failures are best-effort
+                err_msg = f"Failed to fetch skill with UUID '{link}': {e!s}"
+                logging.exception(err_msg)
                 return StructuredToolResult(
                     status=StructuredToolResultStatus.ERROR,
                     error=err_msg,
                     params=params,
                 )

Also note link may be a name (not a UUID) when reached from the catalog-REMOTE path — see the related comment on the no-DAL branch.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@holmes/plugins/toolsets/skills/skills_fetcher.py` around lines 178 - 185, The
current broad "except Exception as e" in the skill fetch path should be narrowed
to only catch expected DAL/transport errors (e.g., the project's DAL/transport
exception classes such as DalError/TransportError/HTTPError) and re-raise any
other unexpected exceptions so they can propagate; change the error message to
use the f-string conversion flag (e.g., {e!r}) to preserve full error detail,
and adjust the message to avoid assuming 'link' is a UUID (it may be a name on
the catalog-REMOTE path) when constructing err_msg in the except block
surrounding the skill fetch logic in skills_fetcher.py.

37-41: Use Field(default_factory=list) instead of mutable default.

The class attribute available_skills: List[str] = [] violates RUF012. Since Tool is a Pydantic BaseModel, even though __init__ reinitializes the field via super().__init__(available_skills=...), the class-level default remains a shared mutable object. Define it as available_skills: List[str] = Field(default_factory=list) instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@holmes/plugins/toolsets/skills/skills_fetcher.py` around lines 37 - 41, The
class-level mutable default on SkillsFetcher.available_skills must be replaced
with a Pydantic safe factory: change the annotation from available_skills:
List[str] = [] to use Field(default_factory=list) so the Tool (Pydantic
BaseModel) does not share a single list across instances; update the import to
ensure Field is available and keep other class attributes (_skill_catalog, _dal)
unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@holmes/plugins/toolsets/skills/skills_fetcher.py`:
- Around line 203-236: SkillsToolset currently reads self.config in __init__ and
calls load_skill_catalog there, but config is applied later via override_with(),
and the isinstance(self.config, dict) check is wrong; move the skill-catalog
loading and tool registration into prerequisites_callable(self, config:
dict[str, Any]) instead. In prerequisites_callable validate the incoming config
against SkillsConfig (or the appropriate Pydantic model), merge
validated.local_skills_path with additional_search_paths to build
all_skill_paths, call load_skill_catalog(dal=dal,
custom_skill_paths=all_skill_paths or None), and then set self.tools =
[SkillsFetcher(self, skill_catalog=skill_catalog, dal=dal)]; remove catalog
loading from __init__ and rely on prerequisites_callable for health checks and
config-aware setup.

---

Duplicate comments:
In `@holmes/plugins/toolsets/skills/skills_fetcher.py`:
- Around line 186-193: The error message in the no-DAL branch (where err_msg is
set before returning a StructuredToolResult with
StructuredToolResultStatus.ERROR) wrongly assumes the link is a UUID; change the
err_msg to a neutral message that does not assume UUIDs (e.g., indicate the
skill/link appears to be remote or requires a remote data access layer but DAL
is disabled) and keep the rest of the return values (StructuredToolResult,
error=err_msg, params) unchanged so callers still receive the same error type
and params.

---

Nitpick comments:
In `@holmes/plugins/toolsets/skills/skills_fetcher.py`:
- Around line 178-185: The current broad "except Exception as e" in the skill
fetch path should be narrowed to only catch expected DAL/transport errors (e.g.,
the project's DAL/transport exception classes such as
DalError/TransportError/HTTPError) and re-raise any other unexpected exceptions
so they can propagate; change the error message to use the f-string conversion
flag (e.g., {e!r}) to preserve full error detail, and adjust the message to
avoid assuming 'link' is a UUID (it may be a name on the catalog-REMOTE path)
when constructing err_msg in the except block surrounding the skill fetch logic
in skills_fetcher.py.
- Around line 37-41: The class-level mutable default on
SkillsFetcher.available_skills must be replaced with a Pydantic safe factory:
change the annotation from available_skills: List[str] = [] to use
Field(default_factory=list) so the Tool (Pydantic BaseModel) does not share a
single list across instances; update the import to ensure Field is available and
keep other class attributes (_skill_catalog, _dal) unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 099b76db-42b9-4c58-93c4-4437998069a3

📥 Commits

Reviewing files that changed from the base of the PR and between e358624 and a4bc86e.

📒 Files selected for processing (3)
  • holmes/plugins/toolsets/skills/skills_fetcher.py
  • tests/config_class/test_custom_skill_paths.py
  • tests/core/test_toolset_manager.py
💤 Files with no reviewable changes (1)
  • tests/core/test_toolset_manager.py
✅ Files skipped from review due to trivial changes (1)
  • tests/config_class/test_custom_skill_paths.py

Comment thread holmes/plugins/toolsets/skills/skills_fetcher.py Outdated
…docs

- Remove allowed_restricted_tools from Skill model, skill_loader,
  skills_fetcher — was added prematurely for Step 3 (per-skill
  restricted tool gating) which hasn't been implemented yet
- Remove metadata field from StructuredToolResult (same reason)
- Add docs/reference/skills.md with migration guide from runbooks,
  SKILL.md format, and configuration instructions
- Migrate CLAUDE.md and README.md from runbooks/ to skills/ with
  minimal changes (word rename + updated paths/format)
- Remove old holmes/plugins/runbooks/ module entirely
- Update docs nav: Runbooks -> Skills

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: avi@robusta.dev <avi@robusta.dev>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
holmes/core/supabase_dal.py (1)

642-664: ⚠️ Potential issue | 🟡 Minor

Possible AttributeError if runbook field is missing/null.

row.get("runbook").get("instructions") chains .get() calls assuming row["runbook"] is always a dict. If the row has runbook = None or the key is absent, this raises AttributeError (and unlike get_skill_catalog this method does not wrap the body in a try/except, so the caller in skills_fetcher._get_robusta_skill will surface the exception). Add a guard or wrap in try/except like get_skill_catalog does.

         row = res.data[0]
         id = row.get("runbook_id")
         symptom = row.get("symptoms")
         title = row.get("subject_name")
-        raw_instruction = row.get("runbook").get("instructions")
+        runbook_blob = row.get("runbook") or {}
+        raw_instruction = runbook_blob.get("instructions")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@holmes/core/supabase_dal.py` around lines 642 - 664, The code assumes
row.get("runbook") is a dict and calls .get("instructions") directly which
raises AttributeError if runbook is None or missing; change to first fetch
runbook = row.get("runbook") and then set raw_instruction =
runbook.get("instructions") if runbook else None, or wrap the whole extraction
in a try/except like get_skill_catalog to catch AttributeError/TypeError, log a
descriptive error including skill_id and the row, and handle by setting
instruction = None or str(raw_instruction) / returning None as appropriate so
the caller (skills_fetcher._get_robusta_skill) doesn't crash.
♻️ Duplicate comments (2)
holmes/plugins/toolsets/skills/skills_fetcher.py (2)

186-208: ⚠️ Potential issue | 🟠 Major

SkillsToolset still loads the catalog in __init__, before user config is applied.

The HolmesGPT toolset lifecycle applies user config (local_skills_path etc.) via override_with() after instantiation, so anything depending on self.config cannot run in __init__. As a result, additional_search_paths is the only knob currently respected and any config-driven skill paths are ignored. Move the load_skill_catalog(...) call and SkillsFetcher registration into prerequisites_callable(config) (validating the dict against a SkillsConfig Pydantic class, then merging paths and building the catalog), matching the pattern used by ServiceNow/RabbitMQ/NewRelic/Prometheus/MongoDB toolsets.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@holmes/plugins/toolsets/skills/skills_fetcher.py` around lines 186 - 208, The
SkillsToolset currently calls load_skill_catalog in __init__, which runs before
user config is applied; move the load_skill_catalog(...) call and
creation/registration of the SkillsFetcher tool into
prerequisites_callable(config) instead: validate the passed config dict against
a SkillsConfig Pydantic model, merge config-provided paths (e.g.,
local_skills_path) with additional_search_paths, call
load_skill_catalog(dal=dal, custom_skill_paths=merged_paths) inside
prerequisites_callable, then instantiate SkillsFetcher(self,
skill_catalog=skill_catalog, dal=dal) and attach it to the toolset (e.g., set
self.tools or return it as the prerequisites result) so the catalog respects
user overrides.

171-178: ⚠️ Potential issue | 🟡 Minor

Misleading error message in the no-DAL branch (still present).

This branch is now reached for any DAL-disabled remote-content fetch, including when link is a normalized skill name from a REMOTE catalog entry routed via line 74 — not just UUID-style IDs. The "appears to be a UUID" wording is incorrect in that path.

-            err_msg = "Skill link appears to be a UUID, but no remote data access layer (dal) is enabled."
+            err_msg = (
+                f"Cannot fetch remote skill '{link}': remote data access layer (DAL) is not enabled."
+            )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@holmes/plugins/toolsets/skills/skills_fetcher.py` around lines 171 - 178, The
error message in the DAL-disabled branch of the remote fetch path (where err_msg
is set and a StructuredToolResult with status=StructuredToolResultStatus.ERROR
is returned) wrongly refers to a UUID; update the err_msg and corresponding
logging.error call to a generic, accurate message indicating that remote data
access (DAL) is disabled and remote-content fetches (including normalized remote
catalog names or UUIDs) cannot proceed, and ensure StructuredToolResult.error
uses that new message so callers see a correct explanation.
🧹 Nitpick comments (4)
tests/plugins/toolsets/test_skills.py (2)

1-13: Unused TEST_SKILLS_PATH and os import.

TEST_SKILLS_PATH is defined but never referenced by any test. If you don't plan to add fixture-backed tests in this file, drop both the constant and the os import.

-import os
-
 from holmes.core.tools import StructuredToolResultStatus
 from holmes.plugins.skills.skill_loader import Skill, SkillCatalog, SkillSource
 from holmes.plugins.toolsets.skills.skills_fetcher import (
     SkillsFetcher,
     SkillsToolset,
 )
 from tests.conftest import create_mock_tool_invoke_context
-
-TEST_SKILLS_PATH = os.path.join(
-    os.path.dirname(__file__), "fixtures", "skills"
-)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/plugins/toolsets/test_skills.py` around lines 1 - 13, Remove the unused
TEST_SKILLS_PATH constant and the unused os import from
tests/plugins/toolsets/test_skills.py; specifically delete the TEST_SKILLS_PATH
symbol and the top-level "import os" line (or, alternatively, modify tests to
actually reference TEST_SKILLS_PATH in test setup if fixtures are intended),
ensuring no other code references TEST_SKILLS_PATH or os after the change.

16-23: Add coverage for the _dal fallback path.

The current tests cover catalog hit, miss-without-DAL, and empty skill_id, but _invoke's remote-skill paths (catalog hit with SkillSource.REMOTE → _get_robusta_skill, and the no-catalog-match-but-DAL-enabled fallback) are uncovered. Consider adding a test that injects a mock SupabaseDal (with enabled=True and a stubbed get_skill_content) to cover those branches and the <skill> wrapping for remote content.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/plugins/toolsets/test_skills.py` around lines 16 - 23, Add a new test
that exercises the `_dal` fallback and remote-skill branch by instantiating
SkillsFetcher(SkillsToolset()) but injecting a mock SupabaseDal (set
enabled=True and stub get_skill_content to return remote skill text) into the
fetcher's `_dal` attribute and/or mocking catalog lookup to simulate no catalog
match or a SkillSource.REMOTE catalog entry; call SkillsFetcher._invoke with the
target skill_id, then assert the result is success (or expected status) and that
returned content is wrapped with the expected `<skill>...</skill>`
remote-wrapping and that get_skill_content was called—this will cover both the
`_get_robusta_skill` remote path and the DAL fallback path.
holmes/plugins/toolsets/skills/skills_fetcher.py (1)

24-28: Mutable default for class attribute available_skills.

available_skills: List[str] = [] at class scope is a shared mutable default (Ruff RUF012). Even though __init__ passes a fresh list to super().__init__, the class-level default can be silently mutated by tooling that introspects the schema. Prefer Field(default_factory=list) (since Tool appears to be a Pydantic model based on the # type: ignore[call-arg] usage) or drop the default and require it.

-class SkillsFetcher(Tool):
-    toolset: "SkillsToolset"
-    available_skills: List[str] = []
+class SkillsFetcher(Tool):
+    toolset: "SkillsToolset"
+    available_skills: List[str] = Field(default_factory=list)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@holmes/plugins/toolsets/skills/skills_fetcher.py` around lines 24 - 28, The
class attribute available_skills on SkillsFetcher is defined as a shared mutable
default (List[str] = []) — change it to use a Pydantic field with a
default_factory to avoid shared-state bugs: replace the class-level List default
with something like Field(default_factory=list) on available_skills (keeping the
type List[str]) or remove the class default entirely and ensure
__init__/super().__init__ provides a fresh list; update imports to include Field
if necessary and keep references to SkillsFetcher, available_skills, and Tool
consistent.
holmes/plugins/skills/skill_loader.py (1)

117-131: os.walk traversal does not follow Path.resolve guarantees and may double-traverse symlinks.

scan_skill_directory resolves the input directory but then calls os.walk(directory), which by default does not follow symlinks (good) but will silently include nested directories regardless of resolve(). Additionally, len(Path(root).resolve().relative_to(directory).parts) resolves root on every iteration; for large trees this is unnecessary work. A simpler depth calc using string prefix or os.walk's native depth is cheaper. Optional cleanup:

-    for root, dirs, files in os.walk(directory):
-        depth = len(Path(root).resolve().relative_to(directory).parts)
+    for root, dirs, files in os.walk(directory):
+        depth = len(Path(root).relative_to(directory).parts)
         if depth >= max_depth:
             dirs.clear()
             continue
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@holmes/plugins/skills/skill_loader.py` around lines 117 - 131, Resolve the
input directory once (e.g., base_dir = Path(directory).resolve()) and call
os.walk on str(base_dir) with topdown=True and followlinks=False to preserve the
original symlink behavior; compute depth cheaply by using os.path.relpath(root,
start=str(base_dir)) and counting path components (handle '.' as depth 0)
instead of resolving root each iteration, and keep the existing logic that
clears dirs when depth >= max_depth and calls parse_skill_file (references:
scan_skill_directory loop, SKILL_FILENAME, parse_skill_file).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@holmes/plugins/skills/__init__.py`:
- Around line 19-24: The helper _repr_str currently does an unconditional
s.replace("\\n", "\n") which will mangle literal backslash-n sequences; change
it to avoid blind replacement — either remove the replace entirely and rely on
dumper.represent_scalar with block style, or guard the replace so it only runs
when the string appears double-escaped (e.g., if "\\n" in s and "\n" not in s)
or when a source-specific flag indicates double-escaping (add an optional
parameter or call-site conditional). Update _repr_str to perform the guarded
replacement (or none) and still call
dumper.represent_scalar("tag:yaml.org,2002:str", s, style="|" if "\n" in s else
None).

In `@holmes/plugins/skills/CLAUDE.md`:
- Line 1: Fix the grammar in the CLAUDE.md opening sentence: change the phrase
"an AI-driven troubleshooting agents" to either "an AI-driven troubleshooting
agent" (singular) or remove the article to read "AI-driven troubleshooting
agents" so the article/noun agree; update the line that starts "You are an
expert in automated diagnostics and skill creation for an AI-driven
troubleshooting agents." to the chosen corrected version.

In `@holmes/plugins/skills/README.md`:
- Line 31: The README references the skill template inconsistently—use a single
filename across both mentions; replace the reference to "prompt.md" with
"skill-format.prompt.md" (or vice versa) so both Line 27 and Line 31 refer to
the exact same template file name (e.g., update the text "Start with the
Template: Use `prompt.md`" to "Use `skill-format.prompt.md`") to ensure
consistent template naming in the README.
- Around line 13-17: The README references a non-existent example file
"kube-prometheus-stack.yaml" under the "Structured Skill" section used by
`holmes investigate`; update this by either adding the missing structured skill
file to the skills directory or replacing the reference with an existing
structured skill example (use a real filename present in the skills directory)
so the example in the README points to a valid structured skill; ensure the
surrounding text still mentions "Structured Skill" and "`holmes investigate`" to
keep context.

In `@holmes/plugins/skills/skill_loader.py`:
- Around line 134-150: The import for RobustaSkillInstruction should be moved
out of the function and placed at the top of the module: remove the in-function
line "from holmes.plugins.skills import RobustaSkillInstruction  # noqa: F811",
add a top-level import for RobustaSkillInstruction alongside the other imports,
and drop the unnecessary "# noqa: F811" comment; then keep
map_robusta_instruction_to_skill unchanged to reference RobustaSkillInstruction
directly (there is no circular-import risk per the review).

In `@holmes/plugins/toolsets/skills/skills_fetcher.py`:
- Around line 142-154: In _get_robusta_skill, the remote Skill.description is
set to only skill_content.title which omits the symptom added by
map_robusta_instruction_to_skill; update the description creation in the
Skill(...) call inside _get_robusta_skill to mirror the catalog format (e.g.,
f"{skill_content.title} — {skill_content.symptom}" when symptom exists) so
remote skills include the symptom the same way as local/catalog skills; adjust
to check for the presence/truthiness of skill_content.symptom before
concatenating.

---

Outside diff comments:
In `@holmes/core/supabase_dal.py`:
- Around line 642-664: The code assumes row.get("runbook") is a dict and calls
.get("instructions") directly which raises AttributeError if runbook is None or
missing; change to first fetch runbook = row.get("runbook") and then set
raw_instruction = runbook.get("instructions") if runbook else None, or wrap the
whole extraction in a try/except like get_skill_catalog to catch
AttributeError/TypeError, log a descriptive error including skill_id and the
row, and handle by setting instruction = None or str(raw_instruction) /
returning None as appropriate so the caller (skills_fetcher._get_robusta_skill)
doesn't crash.

---

Duplicate comments:
In `@holmes/plugins/toolsets/skills/skills_fetcher.py`:
- Around line 186-208: The SkillsToolset currently calls load_skill_catalog in
__init__, which runs before user config is applied; move the
load_skill_catalog(...) call and creation/registration of the SkillsFetcher tool
into prerequisites_callable(config) instead: validate the passed config dict
against a SkillsConfig Pydantic model, merge config-provided paths (e.g.,
local_skills_path) with additional_search_paths, call
load_skill_catalog(dal=dal, custom_skill_paths=merged_paths) inside
prerequisites_callable, then instantiate SkillsFetcher(self,
skill_catalog=skill_catalog, dal=dal) and attach it to the toolset (e.g., set
self.tools or return it as the prerequisites result) so the catalog respects
user overrides.
- Around line 171-178: The error message in the DAL-disabled branch of the
remote fetch path (where err_msg is set and a StructuredToolResult with
status=StructuredToolResultStatus.ERROR is returned) wrongly refers to a UUID;
update the err_msg and corresponding logging.error call to a generic, accurate
message indicating that remote data access (DAL) is disabled and remote-content
fetches (including normalized remote catalog names or UUIDs) cannot proceed, and
ensure StructuredToolResult.error uses that new message so callers see a correct
explanation.

---

Nitpick comments:
In `@holmes/plugins/skills/skill_loader.py`:
- Around line 117-131: Resolve the input directory once (e.g., base_dir =
Path(directory).resolve()) and call os.walk on str(base_dir) with topdown=True
and followlinks=False to preserve the original symlink behavior; compute depth
cheaply by using os.path.relpath(root, start=str(base_dir)) and counting path
components (handle '.' as depth 0) instead of resolving root each iteration, and
keep the existing logic that clears dirs when depth >= max_depth and calls
parse_skill_file (references: scan_skill_directory loop, SKILL_FILENAME,
parse_skill_file).

In `@holmes/plugins/toolsets/skills/skills_fetcher.py`:
- Around line 24-28: The class attribute available_skills on SkillsFetcher is
defined as a shared mutable default (List[str] = []) — change it to use a
Pydantic field with a default_factory to avoid shared-state bugs: replace the
class-level List default with something like Field(default_factory=list) on
available_skills (keeping the type List[str]) or remove the class default
entirely and ensure __init__/super().__init__ provides a fresh list; update
imports to include Field if necessary and keep references to SkillsFetcher,
available_skills, and Tool consistent.

In `@tests/plugins/toolsets/test_skills.py`:
- Around line 1-13: Remove the unused TEST_SKILLS_PATH constant and the unused
os import from tests/plugins/toolsets/test_skills.py; specifically delete the
TEST_SKILLS_PATH symbol and the top-level "import os" line (or, alternatively,
modify tests to actually reference TEST_SKILLS_PATH in test setup if fixtures
are intended), ensuring no other code references TEST_SKILLS_PATH or os after
the change.
- Around line 16-23: Add a new test that exercises the `_dal` fallback and
remote-skill branch by instantiating SkillsFetcher(SkillsToolset()) but
injecting a mock SupabaseDal (set enabled=True and stub get_skill_content to
return remote skill text) into the fetcher's `_dal` attribute and/or mocking
catalog lookup to simulate no catalog match or a SkillSource.REMOTE catalog
entry; call SkillsFetcher._invoke with the target skill_id, then assert the
result is success (or expected status) and that returned content is wrapped with
the expected `<skill>...</skill>` remote-wrapping and that get_skill_content was
called—this will cover both the `_get_robusta_skill` remote path and the DAL
fallback path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9e49ca49-6d7c-4a81-ac48-b34977098efc

📥 Commits

Reviewing files that changed from the base of the PR and between a4bc86e and fb72c61.

📒 Files selected for processing (15)
  • docs/reference/.nav.yml
  • docs/reference/runbooks.md
  • docs/reference/skills.md
  • holmes/core/supabase_dal.py
  • holmes/core/tools.py
  • holmes/plugins/runbooks/README.md
  • holmes/plugins/runbooks/__init__.py
  • holmes/plugins/runbooks/catalog.json
  • holmes/plugins/skills/CLAUDE.md
  • holmes/plugins/skills/README.md
  • holmes/plugins/skills/__init__.py
  • holmes/plugins/skills/skill_loader.py
  • holmes/plugins/toolsets/skills/skills_fetcher.py
  • tests/llm/utils/mock_dal.py
  • tests/plugins/toolsets/test_skills.py
💤 Files with no reviewable changes (4)
  • holmes/plugins/runbooks/catalog.json
  • holmes/plugins/runbooks/README.md
  • docs/reference/runbooks.md
  • holmes/plugins/runbooks/init.py
✅ Files skipped from review due to trivial changes (3)
  • docs/reference/.nav.yml
  • holmes/core/tools.py
  • docs/reference/skills.md

Comment thread holmes/plugins/skills/__init__.py
Comment thread holmes/plugins/skills/CLAUDE.md
Comment thread holmes/plugins/skills/README.md
Comment thread holmes/plugins/skills/README.md
Comment thread holmes/plugins/skills/skill_loader.py
Comment thread holmes/plugins/toolsets/skills/skills_fetcher.py
RoiGlinik
RoiGlinik previously approved these changes Apr 27, 2026
- get_skill_catalog: add .eq("enabled", True) to query, filter rows
where clusters is not null and self.cluster not in clusters
- get_resource_instructions: add .eq("enabled", True), iterate rows to
find first matching cluster (null = all clusters)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Fixed skill catalog to correctly display only enabled skills, exclude
incomplete entries, and properly apply cluster filtering.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Signed-off-by: avi@robusta.dev <avi@robusta.dev>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The build-binary CI step failed because PyInstaller --add-data still
referenced the deleted holmes/plugins/runbooks/ directory.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: avi@robusta.dev <avi@robusta.dev>
RoiGlinik
RoiGlinik previously approved these changes Apr 27, 2026
Avi-Robusta and others added 2 commits April 27, 2026 14:09
- _get_robusta_skill now builds description as "{title} — {symptom}"
  matching map_robusta_instruction_to_skill for consistency
- Move RobustaSkillInstruction import to top-level in skill_loader.py

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: avi@robusta.dev <avi@robusta.dev>
@Avi-Robusta
Avi-Robusta merged commit 54416b4 into master Apr 27, 2026
21 of 22 checks passed
@Avi-Robusta
Avi-Robusta deleted the skills branch April 27, 2026 11:25
naomi-robusta pushed a commit that referenced this pull request Apr 28, 2026
The master merge included the Skills PR (#1953) which renamed
Config.get_runbook_catalog() to get_skill_catalog() and the
build_chat_messages parameter from runbooks= to skills=.

https://claude.ai/code/session_013q1t5sReMaC3SboNv3LZ1m
Signed-off-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants