From f04d95c96084847faae3faaafc8ea00957c20579 Mon Sep 17 00:00:00 2001 From: Alan Wizemann Date: Sat, 25 Apr 2026 10:02:37 +0200 Subject: [PATCH] refactor(mac-skills): delegate loadSkills to shared SkillsScanner (Phase E) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cleanup pass: HermesFileService.loadSkills() was duplicating walk logic that the new ScarfCore SkillsScanner now owns. Replaced the ~38-line implementation with a one-line delegation. Removed: - HermesFileService.loadSkills() walk body (38 lines). - HermesFileService.parseSkillFrontmatter (24 lines, supersedes by SkillFrontmatterParser.parseV011Fields). - HermesFileService.parseSkillRequiredConfig (24 lines, superseded by SkillFrontmatterParser.parseRequiredConfig). The remaining HermesFileService surface (loadSkillContent, saveSkillContent, isValidSkillPath) is unchanged — those are Mac- target-specific guards on file paths that don't fit ScarfCore. Tab enum audit: searched for orphan `.memory` / `.more` references under Scarf iOS/. None found — the worktree refactor cleanly migrated every selectedTab assignment to the new 5-tab vocabulary. Verified: ScarfCore 197 tests + 28 catalog tests + Mac + iOS builds all green (Phase F gate). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Core/Services/HermesFileService.swift | 91 ++----------------- 1 file changed, 6 insertions(+), 85 deletions(-) diff --git a/scarf/scarf/Core/Services/HermesFileService.swift b/scarf/scarf/Core/Services/HermesFileService.swift index 3751548..d721a15 100644 --- a/scarf/scarf/Core/Services/HermesFileService.swift +++ b/scarf/scarf/Core/Services/HermesFileService.swift @@ -496,42 +496,13 @@ struct HermesFileService: Sendable { // MARK: - Skills + /// Walks `~/.hermes/skills///`. v2.5 delegates to + /// the shared ScarfCore `SkillsScanner` so iOS and Mac use byte- + /// identical scan logic — including the v0.11 frontmatter parsing + /// that populates `HermesSkill.allowedTools` / `relatedSkills` / + /// `dependencies`. nonisolated func loadSkills() -> [HermesSkillCategory] { - let dir = context.paths.skillsDir - guard let categories = try? transport.listDirectory(dir) else { return [] } - - return categories.sorted().compactMap { categoryName in - let categoryPath = dir + "/" + categoryName - guard transport.stat(categoryPath)?.isDirectory == true else { return nil } - guard let skillNames = try? transport.listDirectory(categoryPath) else { return nil } - - let skills = skillNames.sorted().compactMap { skillName -> HermesSkill? in - let skillPath = categoryPath + "/" + skillName - guard transport.stat(skillPath)?.isDirectory == true else { return nil } - let files = (try? transport.listDirectory(skillPath)) ?? [] - let requiredConfig = parseSkillRequiredConfig(skillPath + "/skill.yaml") - // v2.5: Hermes v2026.4.23 ships richer SKILL.md - // frontmatter (allowed_tools, related_skills, - // dependencies). Parse opportunistically — old skills - // without a SKILL.md file or without these fields stay - // nil and the detail view skips the chip rows. - let frontmatter = parseSkillFrontmatter(skillPath + "/SKILL.md") - return HermesSkill( - id: categoryName + "/" + skillName, - name: skillName, - category: categoryName, - path: skillPath, - files: files.sorted(), - requiredConfig: requiredConfig, - allowedTools: frontmatter.allowedTools, - relatedSkills: frontmatter.relatedSkills, - dependencies: frontmatter.dependencies - ) - } - - guard !skills.isEmpty else { return nil } - return HermesSkillCategory(id: categoryName, name: categoryName, skills: skills) - } + SkillsScanner.scan(context: context, transport: transport) } nonisolated func loadSkillContent(path: String) -> String { @@ -552,56 +523,6 @@ struct HermesFileService: Sendable { return true } - /// Parse `allowed_tools`, `related_skills`, `dependencies` from a - /// SKILL.md YAML frontmatter block (Hermes v2026.4.23+). Returns - /// nil-filled tuple when the file is absent or has no frontmatter - /// — pre-v0.11 skills behave as before. - nonisolated private func parseSkillFrontmatter( - _ path: String - ) -> (allowedTools: [String]?, relatedSkills: [String]?, dependencies: [String]?) { - guard let content = readFile(path) else { return (nil, nil, nil) } - let lines = content.components(separatedBy: "\n") - // Frontmatter must be the first line `---` followed by another - // `---` somewhere below. Anything else and we bail. - guard lines.first == "---", - let endIdx = lines.dropFirst().firstIndex(of: "---") - else { return (nil, nil, nil) } - let frontmatter = lines[1.. [String] { - guard let content = readFile(path) else { return [] } - var result: [String] = [] - var inRequiredConfig = false - for line in content.components(separatedBy: "\n") { - let trimmed = line.trimmingCharacters(in: .whitespaces) - if trimmed.isEmpty || trimmed.hasPrefix("#") { continue } - let indent = line.prefix(while: { $0 == " " }).count - if trimmed == "required_config:" || trimmed.hasPrefix("required_config:") { - inRequiredConfig = true - continue - } - if inRequiredConfig { - if indent < 2 && !trimmed.isEmpty { - break - } - if trimmed.hasPrefix("- ") { - result.append(String(trimmed.dropFirst(2))) - } - } - } - return result - } - // MARK: - MCP Servers nonisolated func loadMCPServers() -> [HermesMCPServer] {