This is an automated email from the ASF dual-hosted git repository.

luigidemasi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel-kit.git

commit 342d2eaf582891b4f338c299d32bcfac109dc268
Author: Jinal Sarvaiya <[email protected]>
AuthorDate: Tue Oct 20 16:50:48 2026 +0100

    fix #243: address camel-kit review
---
 CHANGELOG.md                                       |  5 ++++
 README.md                                          |  2 +-
 .../camelkit/generator/ClaudeGenerator.java        | 28 ++++++++++++++++++++--
 .../camelkit/service/DoctorService.java            |  3 ++-
 .../camelkit/generator/ClaudeGeneratorTest.java    |  5 +++-
 .../camelkit/service/InitServiceTest.java          | 24 ++++++++++++++++++-
 6 files changed, 61 insertions(+), 6 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index f66b806e..dfdac36d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,6 +19,11 @@ and this project adheres to [Semantic 
Versioning](https://semver.org/spec/v2.0.0
 
 ### Changed
 
+- **Claude Code native skill discovery (#243)** — skills are now installed to 
`.claude/skills/` with
+  `user-invocable` and `description` frontmatter so Claude Code discovers them 
natively without command stubs.
+  Legacy `.claude/commands/camel-*.md` stubs are cleaned up on regeneration; 
user-created commands are preserved.
+  Internal skills (`camel-design`, `camel-implement`, `camel-test`, 
`camel-verify`) are hidden from both user
+  invocation and model auto-selection. Doctor accepts legacy 
`.claude/commands` workspaces.
 - **`camel-migrate` direct mode (#245)** — add a third invocation mode that 
implements routes
   directly under Iron Laws 1 and 2 when the user's request names concrete 
deliverables. Scope
   Iron Law 3 and the HARD-RULE block to chained/standalone modes. Add 
direct-mode implementation
diff --git a/README.md b/README.md
index 12c17911..888f72ad 100644
--- a/README.md
+++ b/README.md
@@ -219,7 +219,7 @@ cd my-integration
 
 | Agent | Init Flag | Primary Generated Assets | MCP Config |
 |-------|-----------|--------------------------|------------|
-| Anthropic Claude Code | `--ai claude` | `CLAUDE.md` + `.claude/commands/` + 
`.claude/skills/` | `.mcp.json` |
+| Anthropic Claude Code | `--ai claude` | `CLAUDE.md` + `.claude/skills/` + 
`.claude/agents/` + `.claude/camel-kit-personas/` | `.mcp.json` |
 | IBM Bob 2 (default) | `--ai bob2` | `.bob/custom_modes.yaml` + rules + 
shared `.bob/skills/` + scoped `.bob/agents/` + `.bob/personas/` | 
`.bob/mcp.json` |
 | Google Antigravity | `--ai antigravity` | `AGENTS.md` + `.agents/skills/` + 
`.agents/agents/` + `.agents/camel-kit-personas/` | `.agents/mcp_config.json` |
 | OpenAI Codex CLI | `--ai codex` | `AGENTS.md` + `.agents/skills/` + 
`.codex/agents/` | `.codex/config.toml` |
diff --git 
a/camel-kit-core/src/main/java/io/github/luigidemasi/camelkit/generator/ClaudeGenerator.java
 
b/camel-kit-core/src/main/java/io/github/luigidemasi/camelkit/generator/ClaudeGenerator.java
index 22cdd921..0d83c6fd 100644
--- 
a/camel-kit-core/src/main/java/io/github/luigidemasi/camelkit/generator/ClaudeGenerator.java
+++ 
b/camel-kit-core/src/main/java/io/github/luigidemasi/camelkit/generator/ClaudeGenerator.java
@@ -2,13 +2,17 @@ package io.github.luigidemasi.camelkit.generator;
 
 import java.io.IOException;
 import java.nio.file.Files;
+import java.nio.file.LinkOption;
 import java.nio.file.Path;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
 
 import io.github.luigidemasi.camelkit.config.AgentDescriptor;
 import io.github.luigidemasi.camelkit.config.AgentRegistry;
 import io.github.luigidemasi.camelkit.util.AnsiColors;
+import io.github.luigidemasi.camelkit.workflow.WorkflowManifestLoader;
 
 public class ClaudeGenerator extends DefaultGenerator {
 
@@ -16,6 +20,24 @@ public class ClaudeGenerator extends DefaultGenerator {
 
     private static final String LEGACY_COMMAND_DIR = ".claude/commands";
 
+    @Override
+    public void preflight(InitContext ctx) throws Exception {
+        Path current = ctx.projectDir();
+        for (Path component : 
ctx.projectDir().relativize(ctx.projectDir().resolve(LEGACY_COMMAND_DIR))) {
+            current = current.resolve(component);
+            if (Files.isSymbolicLink(current)) {
+                throw new IOException(
+                        "Initialization aborted: " + current + " is a symbolic 
link. Camel-Kit generates "
+                                      + "and removes files under the agent 
directories and refuses to operate "
+                                      + "through symbolic links; replace the 
link with a real directory and "
+                                      + "re-run init.");
+            }
+            if (!Files.isDirectory(current, LinkOption.NOFOLLOW_LINKS)) {
+                break;
+            }
+        }
+    }
+
     @Override
     public void generate(InitContext ctx) throws Exception {
         super.generate(ctx);
@@ -30,9 +52,11 @@ public class ClaudeGenerator extends DefaultGenerator {
         if (!Files.isDirectory(legacyDir)) {
             return;
         }
+        Set<String> ownedStubs = 
WorkflowManifestLoader.loadDefault().generatedCommandStubs().stream()
+                .map(c -> c.name() + ".md")
+                .collect(Collectors.toSet());
         try (var files = Files.list(legacyDir)) {
-            for (Path file : files.filter(f -> 
f.getFileName().toString().startsWith("camel-")
-                    && f.getFileName().toString().endsWith(".md")).toList()) {
+            for (Path file : files.filter(f -> 
ownedStubs.contains(f.getFileName().toString())).toList()) {
                 GeneratedAssetCleaner.deleteRegularFile(ctx, file);
             }
         }
diff --git 
a/camel-kit-core/src/main/java/io/github/luigidemasi/camelkit/service/DoctorService.java
 
b/camel-kit-core/src/main/java/io/github/luigidemasi/camelkit/service/DoctorService.java
index a76807c0..88ba10c1 100644
--- 
a/camel-kit-core/src/main/java/io/github/luigidemasi/camelkit/service/DoctorService.java
+++ 
b/camel-kit-core/src/main/java/io/github/luigidemasi/camelkit/service/DoctorService.java
@@ -141,7 +141,8 @@ public class DoctorService {
 
         AgentConfig agent = AgentRegistry.get(agentName);
         String folder = config.getProperty("agent.folder", "").trim();
-        if (agent.folder().equals(folder) || 
agent.commandDirectory().equals(folder)) {
+        if (agent.folder().equals(folder)
+                || (agent.commandDirectory() != null && 
agent.commandDirectory().equals(folder))) {
             findings.add(DoctorFinding.pass("config", 
".camel-kit/config.properties",
                     "Agent configuration selects " + agent.name(),
                     "No action required."));
diff --git 
a/camel-kit-core/src/test/java/io/github/luigidemasi/camelkit/generator/ClaudeGeneratorTest.java
 
b/camel-kit-core/src/test/java/io/github/luigidemasi/camelkit/generator/ClaudeGeneratorTest.java
index 7a3a718d..06e175e1 100644
--- 
a/camel-kit-core/src/test/java/io/github/luigidemasi/camelkit/generator/ClaudeGeneratorTest.java
+++ 
b/camel-kit-core/src/test/java/io/github/luigidemasi/camelkit/generator/ClaudeGeneratorTest.java
@@ -151,7 +151,7 @@ class ClaudeGeneratorTest {
     }
 
     @Test
-    void skillDescriptionsAreClaudeCodeDiscoverable() throws Exception {
+    void skillDescriptionsFollowUseWhenConvention() throws Exception {
         InitContext ctx = createContext();
         new ClaudeGenerator().generate(ctx);
 
@@ -172,6 +172,7 @@ class ClaudeGeneratorTest {
         Files.writeString(legacyDir.resolve("camel-start.md"), "legacy stub");
         Files.writeString(legacyDir.resolve("camel-migrate.md"), "legacy 
stub");
         Files.writeString(legacyDir.resolve("user-custom.md"), "user file");
+        Files.writeString(legacyDir.resolve("camel-company-deploy.md"), "user 
command");
 
         new ClaudeGenerator().generate(ctx);
 
@@ -181,5 +182,7 @@ class ClaudeGeneratorTest {
                 "Legacy camel-migrate command stub must be removed");
         assertTrue(Files.exists(legacyDir.resolve("user-custom.md")),
                 "Non-camel user files must be preserved");
+        assertTrue(Files.exists(legacyDir.resolve("camel-company-deploy.md")),
+                "User-created camel-*.md commands not owned by the workflow 
must be preserved");
     }
 }
diff --git 
a/camel-kit-core/src/test/java/io/github/luigidemasi/camelkit/service/InitServiceTest.java
 
b/camel-kit-core/src/test/java/io/github/luigidemasi/camelkit/service/InitServiceTest.java
index dfd1bcb7..c9f75073 100644
--- 
a/camel-kit-core/src/test/java/io/github/luigidemasi/camelkit/service/InitServiceTest.java
+++ 
b/camel-kit-core/src/test/java/io/github/luigidemasi/camelkit/service/InitServiceTest.java
@@ -205,6 +205,28 @@ class InitServiceTest {
         }
     }
 
+    @Test
+    @EnabledOnOs(OS.LINUX)
+    void rejectsSymlinkedLegacyCommandDirectoryBeforeWriting() throws 
Exception {
+        Path targetDir = 
Files.createDirectory(tempDir.resolve("claude-legacy"));
+        Path outside = 
Files.createDirectory(tempDir.resolve("claude-legacy-outside"));
+        Files.writeString(outside.resolve("camel-start.md"), "legacy stub");
+        Files.createDirectories(targetDir.resolve(".claude"));
+        Files.createSymbolicLink(targetDir.resolve(".claude/commands"), 
outside);
+        List<String> before = snapshot(outside);
+
+        IOException failure = assertThrows(
+                IOException.class,
+                () -> new InitService().initialize(
+                        request(targetDir, "claude", InitProgress.noop(), 
InitReporter.noop())));
+
+        assertTrue(failure.getMessage().contains("symbolic link"));
+        assertTrue(failure.getMessage().contains("Initialization aborted"));
+        assertEquals(before, snapshot(outside), "Must not write through the 
legacy command directory link");
+        assertFalse(Files.exists(targetDir.resolve(".camel-kit")),
+                "Must fail before writing any project files");
+    }
+
     @Test
     @EnabledOnOs(OS.LINUX)
     void normalizesTheTargetBeforeCleanupAndGeneration() throws Exception {
@@ -219,7 +241,7 @@ class InitServiceTest {
                 request(targetDir.resolve("link/.."), "claude", 
InitProgress.noop(), InitReporter.noop()));
 
         assertEquals(targetDir.toAbsolutePath().normalize(), 
result.targetDir());
-        
assertTrue(Files.isRegularFile(targetDir.resolve(".claude/commands/camel-ship.md")));
+        
assertTrue(Files.isRegularFile(targetDir.resolve(".claude/skills/camel-ship/SKILL.md")));
         assertEquals(before, snapshot(outside));
     }
 

Reply via email to