fix: skills-sh install fails for deeply nested repo structures (#2980)

* fix(run_agent): ensure _fire_first_delta() is called for tool generation events

Added calls to _fire_first_delta() in the AIAgent class to improve the handling of tool generation events, ensuring timely notifications during the processing of function calls and tool usage.

* fix(run_agent): improve timeout handling for chat completions

Enhanced the timeout configuration for chat completions in the AIAgent class by introducing customizable connection, read, and write timeouts using environment variables. This ensures more robust handling of API requests during streaming operations.

* fix(run_agent): reduce default stream read timeout for chat completions

Updated the default stream read timeout from 120 seconds to 60 seconds in the AIAgent class, enhancing the timeout configuration for chat completions. This change aims to improve responsiveness during streaming operations.

* fix(run_agent): enhance streaming error handling and retry logic

Improved the error handling and retry mechanism for streaming requests in the AIAgent class. Introduced a configurable maximum number of stream retries and refined the handling of transient network errors, allowing for retries with fresh connections. Non-transient errors now trigger a fallback to non-streaming only when appropriate, ensuring better resilience during API interactions.

* fix: skills-sh install fails for deeply nested repo structures

Skills in repos with deep directory nesting (e.g.
cli-tool/components/skills/development/senior-backend/) could not be
installed because the candidate path generation and shallow root-dir
scan never reached them.

Added GitHubSource._find_skill_in_repo_tree() which uses the GitHub
Trees API to recursively search the entire repo tree in a single API
call. This is used as a final fallback in
SkillsShSource._discover_identifier() when the standard candidate
paths and shallow scan both fail.

Fixes installation of skills from repos like davila7/claude-code-templates
where skills are nested 4+ levels deep.

Reported by user Samuraixheart.
This commit is contained in:
Teknium 2026-03-25 09:31:05 -07:00 committed by GitHub
parent c6f4515f73
commit 5dbe2d9d73
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 298 additions and 22 deletions

View file

@ -305,6 +305,154 @@ class TestSkillsShSource:
assert bundle.files["SKILL.md"] == "# react"
assert mock_get.called
@patch("tools.skills_hub._write_index_cache")
@patch("tools.skills_hub._read_index_cache", return_value=None)
@patch("tools.skills_hub.httpx.get")
@patch.object(GitHubSource, "fetch")
def test_fetch_falls_back_to_tree_search_for_deeply_nested_skills(
self, mock_fetch, mock_get, _mock_read_cache, _mock_write_cache,
):
"""Skills in deeply nested dirs (e.g. cli-tool/components/skills/dev/my-skill/)
are found via the GitHub Trees API when candidate paths and shallow scan fail."""
tree_entries = [
{"path": "README.md", "type": "blob"},
{"path": "cli-tool/components/skills/development/my-skill/SKILL.md", "type": "blob"},
{"path": "cli-tool/components/skills/development/other-skill/SKILL.md", "type": "blob"},
]
def _httpx_get_side_effect(url, **kwargs):
resp = MagicMock()
if "/api/search" in url:
resp.status_code = 404
return resp
if url.endswith("/contents/"):
# Root listing for shallow scan — return empty so it falls through
resp.status_code = 200
resp.json = lambda: []
return resp
if "/contents/" in url:
# All contents API calls fail (candidate paths miss)
resp.status_code = 404
return resp
if url.endswith("owner/repo"):
# Repo info → default branch
resp.status_code = 200
resp.json = lambda: {"default_branch": "main"}
return resp
if "/git/trees/main" in url:
resp.status_code = 200
resp.json = lambda: {"tree": tree_entries}
return resp
# skills.sh detail page
resp.status_code = 200
resp.text = "<h1>my-skill</h1>"
return resp
mock_get.side_effect = _httpx_get_side_effect
resolved_bundle = SkillBundle(
name="my-skill",
files={"SKILL.md": "# My Skill"},
source="github",
identifier="owner/repo/cli-tool/components/skills/development/my-skill",
trust_level="community",
)
mock_fetch.side_effect = lambda ident: resolved_bundle if "cli-tool/components" in ident else None
bundle = self._source().fetch("skills-sh/owner/repo/my-skill")
assert bundle is not None
assert bundle.source == "skills.sh"
assert bundle.files["SKILL.md"] == "# My Skill"
# Verify the tree-resolved identifier was used for the final GitHub fetch
mock_fetch.assert_any_call("owner/repo/cli-tool/components/skills/development/my-skill")
class TestFindSkillInRepoTree:
"""Tests for GitHubSource._find_skill_in_repo_tree."""
def _source(self):
auth = MagicMock(spec=GitHubAuth)
auth.get_headers.return_value = {"Accept": "application/vnd.github.v3+json"}
return GitHubSource(auth=auth)
@patch("tools.skills_hub.httpx.get")
def test_finds_deeply_nested_skill(self, mock_get):
tree_entries = [
{"path": "README.md", "type": "blob"},
{"path": "cli-tool/components/skills/development/senior-backend/SKILL.md", "type": "blob"},
{"path": "cli-tool/components/skills/development/other/SKILL.md", "type": "blob"},
]
def _side_effect(url, **kwargs):
resp = MagicMock()
if url.endswith("/davila7/claude-code-templates"):
resp.status_code = 200
resp.json = lambda: {"default_branch": "main"}
elif "/git/trees/main" in url:
resp.status_code = 200
resp.json = lambda: {"tree": tree_entries}
else:
resp.status_code = 404
return resp
mock_get.side_effect = _side_effect
result = self._source()._find_skill_in_repo_tree("davila7/claude-code-templates", "senior-backend")
assert result == "davila7/claude-code-templates/cli-tool/components/skills/development/senior-backend"
@patch("tools.skills_hub.httpx.get")
def test_finds_root_level_skill(self, mock_get):
tree_entries = [
{"path": "my-skill/SKILL.md", "type": "blob"},
]
def _side_effect(url, **kwargs):
resp = MagicMock()
if "/contents" not in url and "/git/" not in url:
resp.status_code = 200
resp.json = lambda: {"default_branch": "main"}
elif "/git/trees/main" in url:
resp.status_code = 200
resp.json = lambda: {"tree": tree_entries}
else:
resp.status_code = 404
return resp
mock_get.side_effect = _side_effect
result = self._source()._find_skill_in_repo_tree("owner/repo", "my-skill")
assert result == "owner/repo/my-skill"
@patch("tools.skills_hub.httpx.get")
def test_returns_none_when_skill_not_found(self, mock_get):
tree_entries = [
{"path": "other-skill/SKILL.md", "type": "blob"},
]
def _side_effect(url, **kwargs):
resp = MagicMock()
if "/contents" not in url and "/git/" not in url:
resp.status_code = 200
resp.json = lambda: {"default_branch": "main"}
elif "/git/trees/main" in url:
resp.status_code = 200
resp.json = lambda: {"tree": tree_entries}
else:
resp.status_code = 404
return resp
mock_get.side_effect = _side_effect
result = self._source()._find_skill_in_repo_tree("owner/repo", "nonexistent")
assert result is None
@patch("tools.skills_hub.httpx.get")
def test_returns_none_when_repo_api_fails(self, mock_get):
mock_get.return_value = MagicMock(status_code=404)
result = self._source()._find_skill_in_repo_tree("owner/repo", "my-skill")
assert result is None
class TestWellKnownSkillSource:
def _source(self):