Skip to content

Conversation

@codewithkenzo
Copy link
Owner

Summary

Skills like superpowers:systematic-debugging contain multiple supporting files (.md docs, .sh scripts, .ts examples) but only SKILL.md content was loaded into agent context. Agents saw references like "See root-cause-tracing.md in this directory" but couldn't access that content.

Changes

  • utils.ts: Remove currentDepth > 0 guard in collectMdFilesRecursive(), add skipFiles param to exclude SKILL.md at root (avoids duplication)
  • supporting-files.ts: Add content?: string field to SupportingFile interface, read file content inline for files under 50KB
  • loader.ts: Pass new Set(['SKILL.md']) to skip duplication, format supporting files with syntax-highlighted code blocks

Guardrails

  • 50KB limit for inline content (prevents context explosion)
  • Existing 20-file and 10MB total limits unchanged
  • Language detection for 20+ file extensions

Testing

  • All 103 skill-loader tests pass
  • Typecheck clean

- Add CATEGORY_SKILLS mapping for auto-injecting skills per category
- Add getCategorySkills() with user override support via config
- Add category_skills schema for user customization
- Fix Sisyphus→Musashi branding in skill-content tests
- Fix CI workflow to use Musashi agent config
- Add session files to .gitignore
- Fix ToolContext mock in tests (metadata/ask props)
- Update to @opencode-ai/plugin@1.1.30
- Remove depth=0 guard in collectMdFilesRecursive, add skipFiles param
- Load file content inline for supporting files under 50KB
- Format supporting files with syntax-highlighted code blocks
- Add language detection for 20+ file extensions
@greptile-apps
Copy link

greptile-apps bot commented Jan 22, 2026

Greptile Summary

This PR enables skill-loader to include sibling .md files and script contents in agent context, solving the issue where agents saw references to supporting files but couldn't access them.

Key changes:

  • utils.ts: Added skipFiles param to collectMdFilesRecursive() to prevent SKILL.md duplication at root level
  • supporting-files.ts: Added inline content loading for files <50KB, increased MAX_FILES from 20 to 25, added language detection for 20+ extensions
  • loader.ts: Updated to pass skipFiles param and format supporting files with syntax-highlighted code blocks

Issues found:

  • async-loader.ts not updated: Still calls collectMdFilesRecursive() without skipFiles param (loader.ts:98), causing SKILL.md to be included twice in async loading path
  • Priority order inconsistency: Comments in discoverOpencodeGlobalSkills() (line 306) say "loaded first" but array order [...plural, ...singular] may not match intended priority behavior compared to loadOpencodeGlobalSkills() (line 228) which uses [...singular, ...plural]

Edge cases handled well:

  • 50KB inline content limit prevents context explosion
  • Binary files fail gracefully (UTF-8 read returns undefined)
  • Tests confirm MAX_FILES=25 limit and content field population

Confidence Score: 3/5

  • Safe to merge after fixing async-loader.ts duplication bug
  • Core feature works correctly in loader.ts with proper skipFiles handling and good test coverage. However, async-loader.ts was not updated, causing SKILL.md duplication in async loading path (logic bug). Priority order inconsistency in discover functions needs clarification but may not be a runtime issue.
  • src/features/opencode-skill-loader/async-loader.ts needs the same skipFiles fix applied to line 98

Important Files Changed

Filename Overview
src/features/opencode-skill-loader/supporting-files.ts Added inline content loading for files <50KB, increased MAX_FILES to 25, added language detection. Binary file handling could be improved (UTF-8 read failures are silently caught).
src/features/opencode-skill-loader/loader.ts Updated to pass skipFiles param and format supporting files with syntax highlighting. Contains priority order inconsistency in discover functions and async-loader.ts not updated.
src/features/opencode-skill-loader/async-loader.ts Not updated in this PR - still calls collectMdFilesRecursive without skipFiles param, will duplicate SKILL.md content.

Sequence Diagram

sequenceDiagram
    participant Agent as Claude Agent
    participant Loader as loadSkillFromPath()
    participant Utils as collectMdFilesRecursive()
    participant Support as discoverSupportingFiles()
    participant FS as File System

    Agent->>Loader: Load skill directory
    Loader->>FS: Read SKILL.md
    FS-->>Loader: Main skill content
    
    Loader->>Utils: collectMdFilesRecursive(dir, 0, 3, '', skipFiles={'SKILL.md'})
    Utils->>FS: Scan directory
    loop For each entry
        alt Is directory
            Utils->>Utils: Recurse (depth+1)
        else Is .md file
            alt currentDepth==0 && skipFiles.has(name)
                Utils->>Utils: Skip SKILL.md at root
            else
                Utils->>FS: Read .md file
                FS-->>Utils: File content
            end
        end
    end
    Utils-->>Loader: Sibling .md files (no SKILL.md)
    
    Loader->>Support: discoverSupportingFiles(dir)
    Support->>FS: Scan recursively for non-.md files
    loop For each file (max 25, <1MB, <10MB total)
        Support->>FS: Get file stats
        alt size <= 50KB
            Support->>FS: Read file as UTF-8
            FS-->>Support: File content (or undefined if binary)
        end
        Support->>Support: Store metadata + content
    end
    Support-->>Loader: Supporting files with inline content
    
    Loader->>Loader: Format with syntax highlighting
    Note over Loader: For each file with content:<br/>```language<br/>content<br/>```
    Loader-->>Agent: Complete skill template with supporting files
Loading

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

Additional Comments (5)

  1. src/features/opencode-skill-loader/supporting-files.ts, line 98 (link)

    syntax: comment says "max 20 files" but MAX_FILES is now 25

  2. src/features/opencode-skill-loader/supporting-files.ts, line 101 (link)

    syntax: return type comment says "no file contents" but files now include content field

  3. src/features/opencode-skill-loader/supporting-files.test.ts, line 65 (link)

    syntax: test name says "20 files" but MAX_FILES is now 25

  4. src/features/opencode-skill-loader/supporting-files.test.ts, line 73 (link)

    syntax: assertion still checks for 20 but limit is now 25

  5. src/features/opencode-skill-loader/supporting-files.test.ts, line 109 (link)

    style: test doesn't verify the new content field exists for files under 50KB. Add assertion like expect(file.content).toBeDefined() or expect(file.content).toBe('{"key":"value"}')

18 files reviewed, 7 comments

Edit Code Review Agent Settings | Greptile

}

if (stats.size <= INLINE_CONTENT_MAX_SIZE_BYTES) {
supportingFile.content = await fs.readFile(entryPath, 'utf-8').catch(() => undefined)
Copy link

Choose a reason for hiding this comment

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

logic: reading binary files (images, PDFs, executables, etc.) as UTF-8 will produce garbled text or fail silently. The .catch(() => undefined) hides errors, so binary files get content: undefined but still appear in the list. Consider checking file extension against a text-file allowlist before attempting UTF-8 read.

Suggested change
supportingFile.content = await fs.readFile(entryPath, 'utf-8').catch(() => undefined)
const isTextFile = ['.ts', '.tsx', '.js', '.jsx', '.json', '.sh', '.bash', '.zsh', '.py', '.rb', '.rs', '.go', '.yaml', '.yml', '.toml', '.xml', '.html', '.css', '.sql', '.txt', '.md'].includes(extname(entry.name).toLowerCase())
if (isTextFile) {
supportingFile.content = await fs.readFile(entryPath, 'utf-8').catch(() => undefined)
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/features/opencode-skill-loader/supporting-files.ts
Line: 83:83

Comment:
**logic:** reading binary files (images, PDFs, executables, etc.) as UTF-8 will produce garbled text or fail silently. The `.catch(() => undefined)` hides errors, so binary files get `content: undefined` but still appear in the list. Consider checking file extension against a text-file allowlist before attempting UTF-8 read.

```suggestion
          const isTextFile = ['.ts', '.tsx', '.js', '.jsx', '.json', '.sh', '.bash', '.zsh', '.py', '.rb', '.rs', '.go', '.yaml', '.yml', '.toml', '.xml', '.html', '.css', '.sql', '.txt', '.md'].includes(extname(entry.name).toLowerCase())
          if (isTextFile) {
            supportingFile.content = await fs.readFile(entryPath, 'utf-8').catch(() => undefined)
          }
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +89 to +91
if (f.content) {
const lang = getLanguageFromExtension(f.extension)
return `${header}\n\`\`\`${lang}\n${f.content}\n\`\`\``
Copy link

Choose a reason for hiding this comment

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

style: if f.content contains triple backticks (```), the markdown rendering will break. Consider escaping or warning about this edge case in large codebases. Do skills typically contain files with triple backticks in their content?

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/features/opencode-skill-loader/loader.ts
Line: 89:91

Comment:
**style:** if `f.content` contains triple backticks (\`\`\`), the markdown rendering will break. Consider escaping or warning about this edge case in large codebases. Do skills typically contain files with triple backticks in their content?

How can I resolve this? If you propose a fix, please make it concise.

…Template

- Remove console.warn logs from supporting-files.ts (file limit logs on startup)
- Fix extractSkillTemplate to use skill.lazyContent.load() instead of raw file read
- Update test expectations to match new MAX_FILES limit (25)
- Update docstring: max files 20→25, add inline content behavior
- Update test: verify content field exists for small files
@codewithkenzo codewithkenzo merged commit a9b7006 into dev Jan 22, 2026
3 of 5 checks passed
@codewithkenzo codewithkenzo deleted the feat/skill-loader-full-content branch January 22, 2026 00:57
Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

Additional Comments (1)

  1. src/features/opencode-skill-loader/loader.ts, line 98 (link)

    logic: async-loader.ts:98 still calls collectMdFilesRecursive() without the skipFiles param, meaning SKILL.md will be included twice in async loader (once as main content, once in subdirFiles). This inconsistency between loader.ts:78 and async-loader.ts:98 causes duplication in async loading path.

20 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +306 to +313
// Plural takes priority (loaded first) as it's the newer convention
const skillsSingular = join(homedir(), ".config", "opencode", "skill")
const skillsPlural = join(homedir(), ".config", "opencode", "skills")
const [singular, plural] = await Promise.all([
loadSkillsFromDir(skillsSingular, "opencode"),
loadSkillsFromDir(skillsPlural, "opencode"),
])
return [...singular, ...plural]
return [...plural, ...singular]
Copy link

Choose a reason for hiding this comment

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

logic: comment says "loaded first" but in [...plural, ...singular], plural is actually first in the array. For LoadedSkill[] arrays, order matters differently than for Record conversion. In lines 228 and 240, [...singular, ...plural] gives plural priority via skillsToRecord() (last wins). Here, [...plural, ...singular] puts plural first, which may not give it priority depending on how the array is consumed downstream.

verify the intended priority behavior matches the comment and implementation. Does the order matter for LoadedSkill[] arrays in the same way as for Record conversion, or is first-in-array actually the correct priority here?

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/features/opencode-skill-loader/loader.ts
Line: 306:313

Comment:
**logic:** comment says "loaded first" but in `[...plural, ...singular]`, plural is actually first in the array. For `LoadedSkill[]` arrays, order matters differently than for `Record` conversion. In lines 228 and 240, `[...singular, ...plural]` gives plural priority via `skillsToRecord()` (last wins). Here, `[...plural, ...singular]` puts plural first, which may not give it priority depending on how the array is consumed downstream.

verify the intended priority behavior matches the comment and implementation. Does the order matter for LoadedSkill[] arrays in the same way as for Record conversion, or is first-in-array actually the correct priority here?

How can I resolve this? If you propose a fix, please make it concise.

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