Skip to content

Infer targets from host-specific skill paths#144

Merged
runkids merged 2 commits intorunkids:mainfrom
hhh2210:add-sync-target-filter
Apr 29, 2026
Merged

Infer targets from host-specific skill paths#144
runkids merged 2 commits intorunkids:mainfrom
hhh2210:add-sync-target-filter

Conversation

@hhh2210
Copy link
Copy Markdown
Contributor

@hhh2210 hhh2210 commented Apr 26, 2026

Summary

Fixes target filtering for multi-host skill repos that package host-specific skill trees such as:

  • .cursor/skills/...
  • .factory/skills/...
  • openclaw/skills/...
  • .agents/skills/...

When a nested SKILL.md omits targets: frontmatter, discovery currently leaves Targets as nil. During sync, nil means "sync to every target", so a host-specific skill can be copied into unrelated tools. This makes the same logical skill appear multiple times in tools that index nested SKILL.md files, especially with gstack-style repos.

Changes

  • Infer targets from known project skill paths during full discovery.
  • Keep generic skills unchanged: skills outside host-specific paths still keep Targets == nil and sync everywhere.
  • Keep DiscoverSourceSkillsLite behavior unchanged, because lite discovery is used by commands that do not need target filtering.
  • Addressed bot review feedback:
    • project-path matching now runs before the */skills/* fallback, so paths such as .skills/<skill> are covered;
    • project target paths are cached and sorted once instead of rebuilding the map per discovered skill;
    • shared project paths use config.GroupedProjectTargets() so inference returns the canonical grouped target, e.g. .agents/skills -> universal.

Linked Issue

Fixes #145

Tests

env GOCACHE=/Users/larry_1/Opensource/skillshare/.tmp/go-build go test ./internal/sync
env GOCACHE=/Users/larry_1/Opensource/skillshare/.tmp/go-build go test ./internal/...
env GOCACHE=/Users/larry_1/Opensource/skillshare/.tmp/go-build go build -o bin/skillshare ./cmd/skillshare
env GOCACHE=/Users/larry_1/Opensource/skillshare/.tmp/go-build SKILLSHARE_TEST_BINARY=/Users/larry_1/Opensource/skillshare/bin/skillshare go test ./tests/integration -run TestSkillTargets

Current gh pr checks output only reports the Vercel deployment authorization failure for the fork PR. The local checks above pass.

Copilot AI review requested due to automatic review settings April 26, 2026 06:36
@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces automatic target inference for skills based on their relative file paths, allowing skills located in specific directories (e.g., .cursor/skills) to be automatically associated with their respective targets. The changes include new logic in the discovery process to fallback to path-based inference when targets are not explicitly defined in frontmatter, along with caching for known target names to improve performance. Feedback focuses on making the path inference more flexible by removing restrictive directory checks and optimizing the project target lookup by caching the configuration and removing redundant uniqueness checks.

Comment thread internal/sync/discover_walk.go Outdated
Comment on lines +42 to +44
if len(parts) < 3 || parts[1] != "skills" {
return nil
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

This check is quite restrictive as it assumes host-specific skills always follow the host/skills/... pattern. If a target is configured with a custom project path that doesn't have skills as the second component (e.g., my-target/myskill), inference will fail even if it matches the configuration. Consider making this check more flexible or relying on the configuration-based matching first to support custom target paths.

Comment on lines +66 to +81
targets := make([]string, 0, 2)
seen := make(map[string]bool)

for targetName, pathCfg := range config.ProjectTargets() {
if targetName == "" || pathCfg.Path == "" {
continue
}

targetPath := filepath.ToSlash(pathCfg.Path)
if matchesHostSkillPath(relPath, targetName, targetPath) && !seen[targetName] {
seen[targetName] = true
targets = append(targets, targetName)
}
}

return targets
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The seen map is redundant here because config.ProjectTargets() returns a map[string]TargetConfig, ensuring that each targetName in the loop is already unique. Additionally, calling config.ProjectTargets() inside this function (which is called for every skill discovered) is inefficient as it reconstructs the map and iterates over all specs every time. Consider caching the project targets map using a sync.Once pattern, similar to knownTargetNameSet.

func inferTargetsFromProjectPaths(relPath string) []string {
	targets := make([]string, 0, 2)

	// Note: config.ProjectTargets() should ideally be cached to avoid repeated map allocations
	for targetName, pathCfg := range config.ProjectTargets() {
		if targetName == "" || pathCfg.Path == "" {
			continue
		}

		targetPath := filepath.ToSlash(pathCfg.Path)
		if matchesHostSkillPath(relPath, targetName, targetPath) {
			targets = append(targets, targetName)
		}
	}

	return targets
}

@hhh2210
Copy link
Copy Markdown
Contributor Author

hhh2210 commented Apr 26, 2026

Superseded by the latest update: #144 (comment)

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR updates skill discovery so that when a nested SKILL.md omits targets: frontmatter, targets can be inferred from host-specific skill directory layouts (e.g. .cursor/skills/..., .factory/skills/..., openclaw/skills/...) to prevent cross-host syncing.

Changes:

  • Infer DiscoveredSkill.Targets from known host-specific skill source paths during full discovery (while keeping Lite discovery behavior unchanged).
  • Apply inferred targets both for normal discovered skills and for ignored skills when includeIgnored is enabled.
  • Add unit + integration tests covering inferred targets and ensuring Lite mode does not infer.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
tests/integration/skill_targets_test.go Adds an integration test ensuring host-packaged skills only sync to the intended configured target.
internal/sync/discover_walk.go Implements path-based target inference during discovery and adds helper functions/caching.
internal/sync/discover_test.go Adds unit tests for inferred targets (and confirms Lite mode keeps Targets == nil).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/sync/discover_walk.go Outdated
Comment on lines +41 to +51
parts := strings.Split(relPath, "/")
if len(parts) < 3 || parts[1] != "skills" {
return nil
}

// Prefer explicit project paths from known targets (e.g. ".factory/skills").
targets := inferTargetsFromProjectPaths(relPath)
if len(targets) > 0 {
return targets
}

Comment on lines +65 to +79
func inferTargetsFromProjectPaths(relPath string) []string {
targets := make([]string, 0, 2)
seen := make(map[string]bool)

for targetName, pathCfg := range config.ProjectTargets() {
if targetName == "" || pathCfg.Path == "" {
continue
}

targetPath := filepath.ToSlash(pathCfg.Path)
if matchesHostSkillPath(relPath, targetName, targetPath) && !seen[targetName] {
seen[targetName] = true
targets = append(targets, targetName)
}
}
Comment on lines +65 to +82
func inferTargetsFromProjectPaths(relPath string) []string {
targets := make([]string, 0, 2)
seen := make(map[string]bool)

for targetName, pathCfg := range config.ProjectTargets() {
if targetName == "" || pathCfg.Path == "" {
continue
}

targetPath := filepath.ToSlash(pathCfg.Path)
if matchesHostSkillPath(relPath, targetName, targetPath) && !seen[targetName] {
seen[targetName] = true
targets = append(targets, targetName)
}
}

return targets
}
@hhh2210
Copy link
Copy Markdown
Contributor Author

hhh2210 commented Apr 26, 2026

@runkids I pushed an update after the bot review comments.

What changed:

  • moved explicit project-path matching before the */skills/* fallback, so targets like letta with project path .skills are covered;
  • cache and sort project target paths once instead of rebuilding config.ProjectTargets() per discovered skill;
  • use config.GroupedProjectTargets() for shared project paths, so .agents/skills resolves to the canonical universal target instead of a nondeterministic set;
  • added unit coverage for .skills/<skill> and .agents/skills/<skill>.

Re-ran locally:

go test ./internal/sync
go test ./internal/...
go build -o bin/skillshare ./cmd/skillshare
SKILLSHARE_TEST_BINARY=/Users/larry_1/Opensource/skillshare/bin/skillshare go test ./tests/integration -run TestSkillTargets

All passed locally. Current gh pr checks output only reports the Vercel authorization failure for the fork PR, which looks unrelated to this code change.

Please review this PR when possible.

@hhh2210
Copy link
Copy Markdown
Contributor Author

hhh2210 commented Apr 26, 2026

@codex review

@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@runkids runkids merged commit 0821cb7 into runkids:main Apr 29, 2026
7 of 8 checks passed
@runkids
Copy link
Copy Markdown
Owner

runkids commented Apr 29, 2026

Thanks! Will ship the next version.

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.

Avoid duplicate skills from multi-host skill repos

3 participants