Fix mirror pull failing on nested skill bundles, concurrent pulls, and dashboard sync crash - #78
Open
Ascurse wants to merge 5 commits into
Open
Fix mirror pull failing on nested skill bundles, concurrent pulls, and dashboard sync crash#78Ascurse wants to merge 5 commits into
Ascurse wants to merge 5 commits into
Conversation
_list_local_skill_dirs globbed SKILL.md at any depth, so example bundles shipped inside a skill (e.g. claudeception/examples/*) were listed as local skills. They are absent from the remote manifest, so a mirror pull deleted them as stale: the parent directory went first, then rmtree hit the now-missing nested paths and FileNotFoundError rolled back the whole pull. With a nested-skill bundle present, every pull failed and restored the backup, so no skill was ever downloaded. Discovery is now bounded to the supported layouts (<root>/<name> and, for the hermes root, <root>/<category>/<name>), push reuses the same walk, and the stale-delete loop skips a directory already removed with its parent. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
'skillclaw dashboard sync' crashed with "TypeError: Object of type date is not JSON serializable" whenever a skill's YAML frontmatter contained a bare date, since PyYAML parses those into datetime.date. Dump them as strings. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
pull_skills is invoked from several places at once — the launcher's auto-pull, the 30s reload poller, the CLI, and any second instance — all against the same directory. Two overlapping pulls corrupt each other: one moves a bundle out of staging while the other is still reading it, producing a burst of FileNotFoundError. Rollback then failed on its own: rmtree in place left partially removed directories behind and copytree aborted with "Directory not empty", so the safety net was gone exactly when it was needed. Add a non-blocking flock around pull_skills (a busy lock returns locked_out and the cycle is skipped), and rename the broken directory aside before restoring the backup copy. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The skills directory was walked in three places with three different rules: skill_hub (sync), skill_manager (loading and injection) and dashboard_ingest (snapshot). The latter two globbed SKILL.md recursively without a depth bound, so example bundles nested inside a skill counted as standalone skills — the agent was offered 105 skills instead of 102 and the dashboard listed 106. Move the walk into skill_bundle.iter_skill_md_paths and route all three call sites through it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The mirror pull unconditionally copied the whole skills directory into a backup and then again into a staging dir before comparing hashes. With the 30s poller this rewrote every skill on disk twice a minute and kept ~200MB of backups alive, even though nothing ever changed. Compare hashes first and return early when there is no stale skill, no duplicate local dir and every bundle matches the manifest. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Collaborator
|
The underlying reports are valuable, but this branch is not mergeable as-is:
I recommend splitting discovery/layout normalization from pull locking/rollback so each safety change can be reviewed and tested independently. |
opencdlee-dotcom
approved these changes
Aug 17, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Four independent failures hit while running SkillClaw against a Hermes skills directory. Each is reproducible and fixed separately.
1. Mirror pull fails permanently when a skill bundle contains nested SKILL.md files
_list_local_skill_dirsglobsSKILL.mdat any depth, so examples shipped inside a skill (e.g.claudeception/examples/*) are listed as standalone local skills. They are absent from the remote manifest, so mirror pull deletes them as stale — the parent directory first, thenrmtreehits the now-missing nested paths withignore_errors=False:Every pull rolled back, so nothing was ever downloaded.
push_skillshad the same unbounded glob and would have uploaded those nested examples to the hub as separate skills.2. Concurrent pulls corrupt the skills directory
pull_skillsis called by the launcher's auto-pull, the 30s reload poller and the CLI, with no mutual exclusion. Two overlapping pulls walk the same directory and one moves bundles out of staging while the other is still reading them, producing a burst ofFileNotFoundErroracross unrelated skills.3. Backup rollback fails exactly when needed
The rollback path does
rmtree(skills_dir)thencopytree(backup_dir, skills_dir). When the tree is being mutated concurrently,rmtreeleaves directories behind and the restore aborts:4.
dashboard synccrashes on date frontmatterTypeError: Object of type date is not JSON serializable— PyYAML parses a baredate:value in skill frontmatter intodatetime.date, whichjson.dumpsrejects.Changes
<root>/<name>, and<root>/<category>/<name>for the hermes root); nestedSKILL.mdfiles are bundle contents, not skills.skill_bundle.iter_skill_md_pathsand routeskill_hub,skill_manageranddashboard_ingestthrough it — they previously used three different rules, which is why the loader reported 105 skills and the dashboard 106 where the disk had 102.pull_skillswith a non-blockingflock; a busy lock returnslocked_outand the cycle is skipped rather than racing. Surfaced in the CLI so a skipped pull is not mistaken for a successful empty one.date/datetimevalues when writing the dashboard snapshot.Verification
.claude/skillsset:iter_skill_md_paths,SkillHub._list_local_skillsandSkillManagerall return the same 3 skills; before the change the latter two returned 5.Falseand the lock is released afterwards. Four concurrentskillclaw skills pullruns against a live daemon — exactly one proceeds, three reportSkipped: another pull is already running, and the skills directory is untouched.Directory not empty.pull complete: 0 downloaded, 102 skipped, 0 deleted, 102 total remoteon every cycle, with no rollbacks, and loader/hub/dashboard all report 102.🤖 Generated with Claude Code