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)); }
