Skip to content

fix(skills): copy skills for non-symlink agents#123

Merged
BlackHole1 merged 1 commit into
mainfrom
fix-ai-agent-skill
Apr 29, 2026
Merged

fix(skills): copy skills for non-symlink agents#123
BlackHole1 merged 1 commit into
mainfrom
fix-ai-agent-skill

Conversation

@BlackHole1
Copy link
Copy Markdown
Member

Some supported agents ignore skills when the installed target is a symlink. Centralizing the publication policy keeps bundled, registry, update, startup sync, and local init paths aligned while retaining symlinks only for agents known to support them.

Some supported agents ignore skills when the installed target is a
symlink. Centralizing the publication policy keeps bundled, registry,
update, startup sync, and local init paths aligned while retaining
symlinks only for agents known to support them.

Signed-off-by: Kevin Cui <bh@bugs.cc>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 29, 2026

Summary by CodeRabbit

Release Notes

  • Documentation

    • Clarified skill deployment behavior, specifying how skills are installed into each supported agent's directory—symlink-with-fallback for some agents, copy-only for others.
  • Tests

    • Added comprehensive test coverage validating skill installation and update behaviors across all supported agent platforms.

Walkthrough

This pull request refactors the skill publication mechanism across multiple commands and introduces documentation updates. The changes establish a centralized function resolveManagedSkillPublicationMode() to determine whether skills are published as symlinks or copies based on the target agent. Codex, Claude Code, and QoderWork agents support symlink-or-copy publication mode, while Hermes, CodeBuddy, WorkBuddy, and OpenClaw agents use copy-only mode. This logic is extracted from inline conditionals scattered across the codebase (auto-sync.ts, init.ts, registry-skill-publication.ts, shared.ts) into a dedicated resolver module. Tests are updated to reflect the new per-agent publication behavior, and documentation is revised to specify the publication strategy for each supported agent.

Possibly related PRs

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title follows the required format with type(scope): subject and clearly describes the main change of copying skills for non-symlink agents.
Description check ✅ Passed The description directly relates to the changeset by explaining the motivation for centralizing publication policy and distinguishing symlink-capable from non-symlink agents.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch fix-ai-agent-skill

Review rate limit: 4/5 reviews remaining, refill in 12 minutes.

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.

🧹 Nitpick comments (2)
src/application/commands/skills/index.test.ts (1)

1421-1436: Avoid hardcoding host-mode groups inline in this integration test.

The linked/copied host arrays duplicate the centralized publication policy. Prefer deriving expected mode per host via resolveManagedSkillPublicationMode(...) and asserting behavior accordingly.

♻️ Suggested refactor
+import { resolveManagedSkillPublicationMode } from "./managed-skill-publication.ts";
- for (const linkedSkillDirectoryPath of [
-     codexSkillDirectoryPath,
-     claudeSkillDirectoryPath,
-     qoderWorkSkillDirectoryPath,
- ]) { ... }
-
- for (const copiedSkillDirectoryPath of [
-     hermesSkillDirectoryPath,
-     codeBuddySkillDirectoryPath,
-     workBuddySkillDirectoryPath,
-     openClawSkillDirectoryPath,
- ]) { ... }
+const installedByAgent = {
+    codex: codexSkillDirectoryPath,
+    claude: claudeSkillDirectoryPath,
+    hermes: hermesSkillDirectoryPath,
+    codebuddy: codeBuddySkillDirectoryPath,
+    workbuddy: workBuddySkillDirectoryPath,
+    openclaw: openClawSkillDirectoryPath,
+    qoderwork: qoderWorkSkillDirectoryPath,
+} as const;
+
+for (const [agentName, installedPath] of Object.entries(installedByAgent)) {
+    if (resolveManagedSkillPublicationMode(agentName as keyof typeof installedByAgent) === "symlink-or-copy") {
+        expect(await realpath(installedPath)).toBe(canonicalSkillRealPath);
+        continue;
+    }
+    expect(await realpath(installedPath)).not.toBe(canonicalSkillRealPath);
+    expect((await lstat(installedPath)).isSymbolicLink()).toBeFalse();
+}

Based on learnings: Applies to **/*.test.{ts,tsx} : When extracting a shared utility from production code, also replace any test helpers or inline expressions that duplicate the same logic in test files.

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

In `@src/application/commands/skills/index.test.ts` around lines 1421 - 1436, The
test currently hardcodes which hosts are "linked" vs "copied" (the arrays
containing codexSkillDirectoryPath, claudeSkillDirectoryPath,
qoderWorkSkillDirectoryPath, etc. and hermesSkillDirectoryPath,
codeBuddySkillDirectoryPath, workBuddySkillDirectoryPath,
openClawSkillDirectoryPath) which duplicates the publication policy; instead,
import and call resolveManagedSkillPublicationMode(hostIdentifier) for each host
and derive the expected behavior (linked => expect realpath(...)
toBe(canonicalSkillRealPath), copied => expect realpath(...) not
toBe(canonicalSkillRealPath) or other assertions used), replacing the two
hardcoded for-loops so the test asserts per-host behavior based on
resolveManagedSkillPublicationMode instead of inline arrays.
src/application/commands/skills/update.test.ts (1)

277-305: Extract duplicated update fetcher/setup into a local test factory.

The new Hermes copy-mode test repeats the same package-info/tarball stubbing pattern already used in this file, which makes fixture updates error-prone.

♻️ Suggested refactor
+function createOpenAiUpdateFetcher(version: string, skillMarkdown: string) {
+    return async (input: RequestInfo | URL, init?: RequestInit) => {
+        const request = toRequest(input, init);
+
+        if (request.url.includes("/package-info/")) {
+            return new Response(JSON.stringify({
+                packageName: "openai",
+                version,
+                skills: [{ description: "Chat with a model", name: "chatgpt", title: "ChatGPT" }],
+            }));
+        }
+
+        if (request.url.endsWith(`/openai/-/meta/openai-${version}.tgz`)) {
+            return new Response(await createRegistrySkillArchiveBytes({
+                "package/package/skills/chatgpt/SKILL.md": skillMarkdown,
+            }));
+        }
+
+        throw new Error(`Unexpected request: ${request.url}`);
+    };
+}
- fetcher: async (input, init) => { ... }
+ fetcher: createOpenAiUpdateFetcher("0.0.4", "# ChatGPT fresh\n")

As per coding guidelines: **/*.test.{ts,tsx}: Extract repeated setup (mocks, stubs, setup objects) into local factory functions in test files; avoid copy-pasting test setup.

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

In `@src/application/commands/skills/update.test.ts` around lines 277 - 305, The
test duplicates the package-info/tarball fetcher setup used elsewhere; extract
that repeated logic into a local factory helper (e.g., createUpdateFetcher or
makeUpdateSandbox) inside this test file and have sandbox.run use the returned
fetcher/setup instead of inlining it. Locate where sandbox.run(["skills",
"update", "chatgpt"], { fetcher: ... }) is used and move the Response stubs (the
package-info JSON and the openai-0.0.4.tgz branch that calls
createRegistrySkillArchiveBytes with "package/package/skills/chatgpt/SKILL.md")
into the new factory function, then call that factory from each test to supply
the fetcher (and any other repeated setup) to avoid duplication.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/application/commands/skills/index.test.ts`:
- Around line 1421-1436: The test currently hardcodes which hosts are "linked"
vs "copied" (the arrays containing codexSkillDirectoryPath,
claudeSkillDirectoryPath, qoderWorkSkillDirectoryPath, etc. and
hermesSkillDirectoryPath, codeBuddySkillDirectoryPath,
workBuddySkillDirectoryPath, openClawSkillDirectoryPath) which duplicates the
publication policy; instead, import and call
resolveManagedSkillPublicationMode(hostIdentifier) for each host and derive the
expected behavior (linked => expect realpath(...) toBe(canonicalSkillRealPath),
copied => expect realpath(...) not toBe(canonicalSkillRealPath) or other
assertions used), replacing the two hardcoded for-loops so the test asserts
per-host behavior based on resolveManagedSkillPublicationMode instead of inline
arrays.

In `@src/application/commands/skills/update.test.ts`:
- Around line 277-305: The test duplicates the package-info/tarball fetcher
setup used elsewhere; extract that repeated logic into a local factory helper
(e.g., createUpdateFetcher or makeUpdateSandbox) inside this test file and have
sandbox.run use the returned fetcher/setup instead of inlining it. Locate where
sandbox.run(["skills", "update", "chatgpt"], { fetcher: ... }) is used and move
the Response stubs (the package-info JSON and the openai-0.0.4.tgz branch that
calls createRegistrySkillArchiveBytes with
"package/package/skills/chatgpt/SKILL.md") into the new factory function, then
call that factory from each test to supply the fetcher (and any other repeated
setup) to avoid duplication.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: af68e80b-1b14-43d6-af4a-4033cff9a054

📥 Commits

Reviewing files that changed from the base of the PR and between 75424ac and 7dfb2ae.

📒 Files selected for processing (11)
  • docs/commands.md
  • docs/commands.zh-CN.md
  • src/application/commands/skills/auto-sync.ts
  • src/application/commands/skills/index.test.ts
  • src/application/commands/skills/init.test.ts
  • src/application/commands/skills/init.ts
  • src/application/commands/skills/managed-skill-publication.test.ts
  • src/application/commands/skills/managed-skill-publication.ts
  • src/application/commands/skills/registry-skill-publication.ts
  • src/application/commands/skills/shared.ts
  • src/application/commands/skills/update.test.ts

@BlackHole1 BlackHole1 merged commit e5f7a0c into main Apr 29, 2026
5 checks passed
@BlackHole1 BlackHole1 deleted the fix-ai-agent-skill branch April 29, 2026 10:40
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