Skip to content

Migrate to SKILL.md agent skills standard#7

Merged
ClankerGuru merged 3 commits into
mainfrom
fix/skill-md-migration
Apr 16, 2026
Merged

Migrate to SKILL.md agent skills standard#7
ClankerGuru merged 3 commits into
mainfrom
fix/skill-md-migration

Conversation

@ClankerGuru
Copy link
Copy Markdown
Owner

@ClankerGuru ClankerGuru commented Apr 16, 2026

Summary

Replace flat .md file generation with the Agent Skills Standard SKILL.md format, adopted by Claude Code, Copilot, Codex, OpenCode, and 30+ other tools.

Key changes

  • Skills written as ~/.clkx/skills/{name}/SKILL.md (source of truth)
  • Agent defs written as ~/.clkx/agents/{name}.md
  • Symlinked to project .claude/skills/, .github/skills/, .codex/skills/, .opencode/skills/
  • Copilot gets file copies (JetBrains doesn't follow symlinks)
  • .agents/skills/ shared path all tools read
  • ~/.clkx/.gitignore ignores everything
  • Project skill dirs get opsx* srcx* wrkx* gitignore patterns

Test plan

  • ./gradlew build passes (465 tests, detekt, ktlint, kover)

Change proposed via opsx

opsx-propose + opsx-apply (9/12 tasks auto-completed, 3 fixed manually).

Summary by CodeRabbit

  • New Features

    • Added support for agents to receive skill and agent files via copying instead of symbolic links.
  • Refactor

    • Reorganized skill files into dedicated subdirectories per skill.
    • Centralized agent definitions with per-agent distribution options.
    • Updated skill and agent distribution and cleanup mechanisms to support new directory structure.

Replace flat .md files with proper SKILL.md format in skill directories.
Source of truth is ~/.clkx/ with skills/{name}/SKILL.md structure.

- Agent enum: skillsDir replaces skillDir/skillExtension/useSymlinks
- SkillGenerator writes SKILL.md files to ~/.clkx/skills/{name}/
- Agent defs written to ~/.clkx/agents/, symlinked to project dirs
- Copilot gets copies instead of symlinks (JetBrains limitation)
- Project-level: symlinks from .claude/skills/, .github/skills/, etc.
- .agents/skills/ shared path for cross-tool compatibility
- All gitignores use opsx* srcx* wrkx* patterns
- ~/.clkx/.gitignore ignores everything with *
- CleanTask removes from all new locations
- All 465 tests pass
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 16, 2026

Warning

Rate limit exceeded

@ClankerGuru has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 37 minutes and 59 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 37 minutes and 59 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 51d1877d-75c5-4019-a3fb-fa7b6ff1cb8c

📥 Commits

Reviewing files that changed from the base of the PR and between f869815 and 3973b13.

📒 Files selected for processing (4)
  • src/main/kotlin/zone/clanker/opsx/model/Agent.kt
  • src/main/kotlin/zone/clanker/opsx/skill/SkillGenerator.kt
  • src/test/kotlin/zone/clanker/opsx/model/AgentTest.kt
  • src/test/kotlin/zone/clanker/opsx/skill/SkillGeneratorAgentTest.kt
📝 Walkthrough

Walkthrough

This PR refactors the skill and agent distribution architecture: skills now organize in per-skill subdirectories (~/.clkx/skills/{skillName}/SKILL.md), agent definitions consolidate to a central source (~/.clkx/agents/opsx.md), and distribution uses agent-specific symlinks or copies based on a new usesCopy property in the Agent enum.

Changes

Cohort / File(s) Summary
Agent Model Updates
src/main/kotlin/zone/clanker/opsx/model/Agent.kt
Constructor parameter skillDir renamed to skillsDir; removed skillExtension parameter. All enum entries updated to point to */skills directories. Companion property allSkillDirs renamed to allSkillsDirs. Added new usesCopy computed property (true only for COPILOT).
Skill Generation Refactoring
src/main/kotlin/zone/clanker/opsx/skill/SkillGenerator.kt
Skills now stored in per-skill subdirectories (~/.clkx/skills/{skillName}/SKILL.md). Distribution logic changed from symlinking individual files to creating intermediate symlinks from rootDir/.agents/skills/{skillName}, then distributing per-agent via symlink or copy. Agent definitions moved to central source ~/.clkx/agents/opsx.md with per-agent distribution. Helper function signatures updated.
Cleanup Task Updates
src/main/kotlin/zone/clanker/opsx/task/CleanTask.kt
Cleanup logic updated to iterate allSkillsDirs and shared skills directory. Removed per-target home directory cleaning. Now cleans entire ~/.clkx/agents/ directory. Replaced unconditional file deletion with Files.deleteIfExists() for symlink-aware removal.
Sync Task Updates
src/main/kotlin/zone/clanker/opsx/task/SyncTask.kt
.gitignore written to parent directory with mkdirs(). Updated to handle shared skills directory with specific patterns (opsx*, srcx*, wrkx*). Cleanup targets computed from allSkillsDirs. Source directory cleanup now recursive rather than .md-file-only.
Test Suite Updates
src/test/kotlin/zone/clanker/opsx/OpsxPluginSyncTest.kt, src/test/kotlin/zone/clanker/opsx/model/AgentTest.kt, src/test/kotlin/zone/clanker/opsx/skill/SkillGenerator*.kt, src/test/kotlin/zone/clanker/opsx/task/Clean*.kt, src/test/kotlin/zone/clanker/opsx/task/SyncTaskTest.kt
All tests updated to expect directory-based skill layout (/SKILL.md under skill subdirectories) instead of flat markdown files. Directory symlinks replace file symlinks. Agent tests verify new allSkillsDirs property and usesCopy behavior. Agent-definition tests assert central ~/.clkx/agents/opsx.md as source-of-truth with per-agent symlink/copy distribution.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • opsx#4: Modifies SkillGenerator and task-level agent-aware skill distribution with changes to symlink vs. copy behavior and agent file production locations.
  • opsx#3: Refactors the same core files (SkillGenerator, CleanTask, SyncTask) changing skill directory paths and agent-target distribution patterns.
  • opsx#5: Updates SkillGenerator, SyncTask, and CleanTask with multi-agent distribution and cleanup changes alongside Agent-related path modifications.

Poem

🐰 Skills hop into folders, neat and tidy,
Each one named just right, not messy-like Friday,
Some agents get copies, some get symlinks sweet,
Organized magic makes the repo complete!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: migrating from flat .md files to the SKILL.md agent skills standard with organized directory structures.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/skill-md-migration

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.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
src/main/kotlin/zone/clanker/opsx/task/SyncTask.kt (1)

61-63: ⚠️ Potential issue | 🟡 Minor

Skill count logic doesn't match directory-based layout.

The count uses it.name.endsWith(".md"), but with the new directory-based skill layout (~/.clkx/skills/{name}/SKILL.md), skills are directories, not .md files at the sourceDir level. This will always report 0 skills.

Compare with CleanTask.cleanSourceSkills() which correctly counts directories: sourceDir.listFiles().orEmpty().count { it.isDirectory }.

🐛 Proposed fix
-        val fileCount = sourceDir.listFiles()?.count { it.name.endsWith(".md") } ?: 0
+        val fileCount = sourceDir.listFiles()?.count { it.isDirectory } ?: 0
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/kotlin/zone/clanker/opsx/task/SyncTask.kt` around lines 61 - 63, The
skill count in SyncTask (variable fileCount) incorrectly counts .md files in
sourceDir but skills are directories; update the logic to mirror
CleanTask.cleanSourceSkills by using sourceDir.listFiles().orEmpty().count {
it.isDirectory } (referencing SyncTask, fileCount and sourceDir) so the logged
skill count reflects directory-based skills; keep agentSummary generation
unchanged.
🧹 Nitpick comments (3)
src/main/kotlin/zone/clanker/opsx/task/SyncTask.kt (1)

88-104: Consider also cleaning broken symlinks during sync.

SyncTask.cleanAgentSymlinks() only removes symlinks that point into sourceDir, but doesn't handle broken symlinks. In contrast, CleanTask.cleanSymlinksInDir() (see src/main/kotlin/zone/clanker/opsx/task/CleanTask.kt:65-69) filters by isOpsxSymlink(it, sourceDir) || isBrokenSymlink(it).

If a skill is removed between syncs, its symlink becomes broken and would persist across subsequent sync operations.

♻️ Proposed fix to also remove broken symlinks
     private fun cleanAgentSymlinks(
         root: File,
         sourceDir: File,
     ) {
         // Clean ALL agent dirs, not just configured ones — removes stale symlinks from removed agents
         val dirs =
             (Agent.allSkillsDirs + SkillGenerator.SHARED_SKILLS_DIR)
                 .map { File(root, it) }
                 .distinct()
         dirs.filter { it.exists() }.forEach { dir ->
             dir
                 .listFiles()
                 .orEmpty()
-                .filter { isOpsxSymlink(it, sourceDir) }
+                .filter { isOpsxSymlink(it, sourceDir) || isBrokenSymlink(it) }
                 .forEach { it.delete() }
         }
     }
+
+    private fun isBrokenSymlink(file: File): Boolean {
+        val path = file.toPath()
+        return Files.isSymbolicLink(path) && !Files.exists(path)
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/kotlin/zone/clanker/opsx/task/SyncTask.kt` around lines 88 - 104,
cleanAgentSymlinks currently only deletes symlinks that point into sourceDir, so
broken symlinks persist; update SyncTask.cleanAgentSymlinks to also remove
broken symlinks by changing the filter to include isBrokenSymlink(it) in
addition to isOpsxSymlink(it, sourceDir) (use the same isBrokenSymlink helper
used by CleanTask), ensuring the loop over dirs (built from Agent.allSkillsDirs
+ SkillGenerator.SHARED_SKILLS_DIR) deletes entries that are either opsx
symlinks or broken.
src/main/kotlin/zone/clanker/opsx/skill/SkillGenerator.kt (2)

92-106: Silent failure in createSymlink may hide issues.

When Files.exists(link) is true but it's not a symlink (line 101-102), the function silently returns without creating the symlink or logging a warning. Additionally, runCatching swallows all exceptions. This could make debugging difficult if symlink creation fails for unexpected reasons.

Consider logging a warning when skipping due to existing non-symlink files, or when createSymbolicLink throws.

♻️ Optional: Add logging for transparency
 private fun createSymlink(
     target: File,
     source: File,
 ) {
     val link = target.toPath()
     val sourcePath = source.toPath()
-    runCatching {
-        if (Files.isSymbolicLink(link)) {
-            Files.delete(link)
-        } else if (Files.exists(link)) {
-            return@runCatching
-        }
-        Files.createSymbolicLink(link, sourcePath)
+    if (Files.isSymbolicLink(link)) {
+        Files.delete(link)
+    } else if (Files.exists(link)) {
+        // Non-symlink exists; skip to avoid overwriting user data
+        return
     }
+    runCatching { Files.createSymbolicLink(link, sourcePath) }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/kotlin/zone/clanker/opsx/skill/SkillGenerator.kt` around lines 92 -
106, The createSymlink function currently swallows all errors with runCatching
and silently returns when a non-symlink file exists; update createSymlink to (1)
detect the case Files.exists(link) && !Files.isSymbolicLink(link) and emit a
warning (use the class logger or suitable logger) explaining the existing path
blocks symlink creation, and (2) replace runCatching with explicit try/catch
that logs any exception from Files.createSymbolicLink (including the exception
message/stack) and rethrows or returns a failure indicator so callers can react;
reference createSymlink, Files.isSymbolicLink, Files.exists, and
Files.createSymbolicLink when making the changes.

503-507: Remove the unused agent parameter instead of deprecating.

The agent parameter is unused (and suppressed), but external non-test callers do not exist. Tests do call this method with agent arguments (lines 343, 350 in SkillGeneratorAgentTest.kt), but the implementation ignores them. Rather than deprecating, simply remove the parameter from both the method signature and the test calls, since this is a utility helper with no external dependents.

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

In `@src/main/kotlin/zone/clanker/opsx/skill/SkillGenerator.kt` around lines 503 -
507, Remove the unused agent parameter from the generatedDirs function and
update its callers in tests: change fun generatedDirs(agent: Agent? = null):
List<File> to fun generatedDirs(): List<File> (in SkillGenerator.kt) and remove
the agent argument from calls in SkillGeneratorAgentTest (the calls around the
previous lines ~343 and ~350); ensure imports and any suppress annotations are
cleaned up since the parameter and `@Suppress`("UnusedParameter") are no longer
needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/kotlin/zone/clanker/opsx/skill/SkillGenerator.kt`:
- Around line 204-208: The current primary-agent selection uses
agents.firstOrNull { it.agentDir != null } which makes frontmatter dependent on
the agents list order; instead, choose a deterministic preferred primary (e.g.,
prefer Agent.CLAUDE, then Agent.COPILOT, then any with agentDir) before calling
writeAgentDefinition/buildAgentDefinition so the YAML frontmatter is stable.
Update the selection logic around agents.firstOrNull to explicitly pick
Agent.CLAUDE if present (fallback to Agent.COPILOT, then any agent with non-null
agentDir, then return), so writeAgentDefinition(primaryAgent, homeAgentsDir) and
buildAgentDefinition use a consistent primary agent.

In `@src/test/kotlin/zone/clanker/opsx/skill/SkillGeneratorAgentTest.kt`:
- Around line 78-84: Wrap the System.setProperty("user.home", ...) manipulation
in a small reusable helper and use it in SkillGeneratorAgentTest around the
SkillGenerator(tempDir, ...).generateAgentDefinitions() calls so the original
property is always restored via try/finally and to avoid cross-test pollution;
e.g., add a withSystemProperty helper (used from SkillGeneratorAgentTest) that
accepts a key, value, and lambda, sets the property, runs the lambda, and
restores the previous value, or alternatively enable Kotest's
IsolationMode.InstancePerLeaf for the test class to ensure per-test JVM property
isolation; replace the direct System.setProperty/restore blocks around
SkillGenerator and the generateAgentDefinitions() calls with calls to this
helper.

---

Outside diff comments:
In `@src/main/kotlin/zone/clanker/opsx/task/SyncTask.kt`:
- Around line 61-63: The skill count in SyncTask (variable fileCount)
incorrectly counts .md files in sourceDir but skills are directories; update the
logic to mirror CleanTask.cleanSourceSkills by using
sourceDir.listFiles().orEmpty().count { it.isDirectory } (referencing SyncTask,
fileCount and sourceDir) so the logged skill count reflects directory-based
skills; keep agentSummary generation unchanged.

---

Nitpick comments:
In `@src/main/kotlin/zone/clanker/opsx/skill/SkillGenerator.kt`:
- Around line 92-106: The createSymlink function currently swallows all errors
with runCatching and silently returns when a non-symlink file exists; update
createSymlink to (1) detect the case Files.exists(link) &&
!Files.isSymbolicLink(link) and emit a warning (use the class logger or suitable
logger) explaining the existing path blocks symlink creation, and (2) replace
runCatching with explicit try/catch that logs any exception from
Files.createSymbolicLink (including the exception message/stack) and rethrows or
returns a failure indicator so callers can react; reference createSymlink,
Files.isSymbolicLink, Files.exists, and Files.createSymbolicLink when making the
changes.
- Around line 503-507: Remove the unused agent parameter from the generatedDirs
function and update its callers in tests: change fun generatedDirs(agent: Agent?
= null): List<File> to fun generatedDirs(): List<File> (in SkillGenerator.kt)
and remove the agent argument from calls in SkillGeneratorAgentTest (the calls
around the previous lines ~343 and ~350); ensure imports and any suppress
annotations are cleaned up since the parameter and `@Suppress`("UnusedParameter")
are no longer needed.

In `@src/main/kotlin/zone/clanker/opsx/task/SyncTask.kt`:
- Around line 88-104: cleanAgentSymlinks currently only deletes symlinks that
point into sourceDir, so broken symlinks persist; update
SyncTask.cleanAgentSymlinks to also remove broken symlinks by changing the
filter to include isBrokenSymlink(it) in addition to isOpsxSymlink(it,
sourceDir) (use the same isBrokenSymlink helper used by CleanTask), ensuring the
loop over dirs (built from Agent.allSkillsDirs +
SkillGenerator.SHARED_SKILLS_DIR) deletes entries that are either opsx symlinks
or broken.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 914ced95-c734-4b2d-a377-6119e30a6b25

📥 Commits

Reviewing files that changed from the base of the PR and between 437aec9 and f869815.

📒 Files selected for processing (10)
  • src/main/kotlin/zone/clanker/opsx/model/Agent.kt
  • src/main/kotlin/zone/clanker/opsx/skill/SkillGenerator.kt
  • src/main/kotlin/zone/clanker/opsx/task/CleanTask.kt
  • src/main/kotlin/zone/clanker/opsx/task/SyncTask.kt
  • src/test/kotlin/zone/clanker/opsx/OpsxPluginSyncTest.kt
  • src/test/kotlin/zone/clanker/opsx/model/AgentTest.kt
  • src/test/kotlin/zone/clanker/opsx/skill/SkillGeneratorAgentTest.kt
  • src/test/kotlin/zone/clanker/opsx/skill/SkillGeneratorTest.kt
  • src/test/kotlin/zone/clanker/opsx/task/CleanTaskTest.kt
  • src/test/kotlin/zone/clanker/opsx/task/SyncTaskTest.kt

Comment thread src/main/kotlin/zone/clanker/opsx/skill/SkillGenerator.kt
Comment thread src/test/kotlin/zone/clanker/opsx/skill/SkillGeneratorAgentTest.kt Outdated
No more nulls in Agent enum. Codex uses AGENTS.md + .agents/ dir.
OpenCode uses AGENTS.md + .opencode/agents/ dir. Agent definitions
now generated for all 4 platforms.
Primary agent for frontmatter prefers CLAUDE then COPILOT instead of
depending on list order. Test helper withHome() wraps System.setProperty
in try/finally to prevent cross-test pollution.
@ClankerGuru ClankerGuru merged commit 1f907cf into main Apr 16, 2026
2 checks passed
@ClankerGuru ClankerGuru deleted the fix/skill-md-migration branch April 16, 2026 02:10
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.

1 participant