Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ Key points:

### Skill Name Validation

Names must match `^[a-z0-9]+(-[a-z0-9]+)*$` and be 1-64 characters.
Names must match `^[a-z0-9]+([.-][a-z0-9]+)*$` and be 1-64 characters. Dots and hyphens are both valid segment separators; dots enable namespace-style names (e.g. `myorg.bootstrap`).

### Registry Paths

Expand Down
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- **Dot-namespaced skill names are now valid.** `ValidateName` accepts dots as
a segment separator in addition to hyphens, so names like `myorg.bootstrap`
and `codebase-intelligence.scan` pass validation. The hyphen-only form
(`code-review`) remains valid; this is a strict superset of the previous
regex, no migration required. Enables installers to preserve their
namespace prefix on platforms that derive slash commands from directory
names (e.g. GitHub Copilot maps `.copilot/skills/myorg.bootstrap/` to
`/myorg.bootstrap`). ([#92])

[#92]: https://github.com/devrimcavusoglu/skern/issues/92

## [v0.3.0] — 2026-05-07

Platform registry rewrite, five new adapters, agent-instruction snippet
Expand Down
2 changes: 1 addition & 1 deletion docs/concepts/skill-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ The main technique or pattern (before/after for techniques).

| Field | Required | Description |
|-------|----------|-------------|
| `name` | Yes | Skill name matching `[a-z0-9]+(-[a-z0-9]+)*`, 1-64 chars. Must equal the directory name. |
| `name` | Yes | Skill name matching `[a-z0-9]+([.-][a-z0-9]+)*`, 1-64 chars. Hyphens and dots are both valid separators (`code-review`, `myorg.bootstrap`). Must equal the directory name. |
| `description` | Yes | What the skill does — start with "Use when…". Max 1024 chars. |
| `tags` | No | List of classification tags |
| `allowed-tools` | No | Tools the skill may use. No empty entries. |
Expand Down
4 changes: 2 additions & 2 deletions docs/reference/validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ The summary line counts each separately, e.g. `Skill "x" has 1 error(s), 0 warni

### Name Format

Skill names must match `[a-z0-9]+(-[a-z0-9]+)*` and be 1–64 characters.
Skill names must match `[a-z0-9]+([.-][a-z0-9]+)*` and be 1–64 characters. Lowercase alphanumeric segments are joined by hyphens or dots; dots enable namespace-style names used by skill installers.

Valid: `code-review`, `lint-fix`, `deploy`. Invalid: `Code_Review`, `my skill`.
Valid: `code-review`, `lint-fix`, `deploy`, `myorg.bootstrap`, `codebase-intelligence.scan`. Invalid: `Code_Review`, `my skill`, `.bootstrap`, `myorg..bootstrap`.

### Description

Expand Down
2 changes: 1 addition & 1 deletion docs/writing-skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ description: |
- Prefer verb-first active voice: `creating-skills` not `skill-creation`
- Be specific: `condition-based-waiting` not `async-test-helpers`

Names must match `^[a-z0-9]+(-[a-z0-9]+)*$` and be 1-64 characters.
Names must match `^[a-z0-9]+([.-][a-z0-9]+)*$` and be 1-64 characters. Hyphens are the conventional separator (`code-review`); dots are also accepted for namespace-style names used by skill installers (`myorg.bootstrap`).

## Recommended Body Structure

Expand Down
43 changes: 43 additions & 0 deletions internal/cli/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,49 @@ func TestEndToEnd_MultiSkillWorkflow(t *testing.T) {
require.NoError(t, err)
}

// TestEndToEnd_DotNamespacedName is a regression test for #92: dot-namespaced
// skill names (e.g. `myorg.bootstrap`) must survive the full create → validate
// → install → uninstall → remove lifecycle, with the literal dot preserved in
// the on-disk directory name in the registry and on installed platforms.
func TestEndToEnd_DotNamespacedName(t *testing.T) {
cc, userDir, _ := testRegistryWithDirs(t)
home := t.TempDir()
project := t.TempDir()
withTestDetector(t, cc, home, project)

const skillName = "myorg.bootstrap"

_, err := runCmd(t, cc, "skill", "create", skillName,
"--description", "Use when bootstrapping a myorg project")
require.NoError(t, err)

registryDir := filepath.Join(userDir, skillName)
_, err = os.Stat(filepath.Join(registryDir, "SKILL.md"))
require.NoError(t, err, "registry directory should contain literal dot in name")

out, err := runCmd(t, cc, "skill", "validate", skillName, "--json")
require.NoError(t, err)
var validateResult output.SkillValidateResult
require.NoError(t, json.Unmarshal([]byte(out), &validateResult))
assert.True(t, validateResult.Valid)

_, err = runCmd(t, cc, "skill", "install", skillName, "--platform", "claude-code")
require.NoError(t, err)
installed := filepath.Join(home, ".claude", "skills", skillName, "SKILL.md")
_, err = os.Stat(installed)
require.NoError(t, err, "installed directory should contain literal dot in name")

_, err = runCmd(t, cc, "skill", "uninstall", skillName, "--platform", "claude-code")
require.NoError(t, err)
_, err = os.Stat(installed)
assert.True(t, os.IsNotExist(err))

_, err = runCmd(t, cc, "skill", "remove", skillName)
require.NoError(t, err)
_, err = os.Stat(registryDir)
assert.True(t, os.IsNotExist(err))
}

// TestEndToEnd_OverlapAndValidation tests the overlap detection and validation
// flows work correctly across the full lifecycle.
func TestEndToEnd_OverlapAndValidation(t *testing.T) {
Expand Down
9 changes: 6 additions & 3 deletions internal/skill/skill.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@ const (
ScopeProject Scope = "project"
)

// nameRegex validates skill names: lowercase alphanumeric with hyphens, 1-64 chars.
var nameRegex = regexp.MustCompile(`^[a-z0-9]+(-[a-z0-9]+)*$`)
// nameRegex validates skill names: lowercase alphanumeric segments joined by
// hyphens or dots as separators, 1-64 chars. Dots enable namespace-style names
// (e.g. "myorg.bootstrap") so installers can preserve their prefix across
// platforms that map directory names to slash commands.
var nameRegex = regexp.MustCompile(`^[a-z0-9]+([.-][a-z0-9]+)*$`)

// Author represents the creator of a skill.
type Author struct {
Expand Down Expand Up @@ -62,7 +65,7 @@ func ValidateName(name string) error {
return fmt.Errorf("skill name cannot exceed 64 characters")
}
if !nameRegex.MatchString(name) {
return fmt.Errorf("skill name %q is invalid: must match [a-z0-9]+(-[a-z0-9]+)* (lowercase alphanumeric with hyphens)", name)
return fmt.Errorf("skill name %q is invalid: must match [a-z0-9]+([.-][a-z0-9]+)* (lowercase alphanumeric segments joined by hyphens or dots)", name)
}
return nil
}
11 changes: 10 additions & 1 deletion internal/skill/skill_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ func TestValidateName(t *testing.T) {
{"long hyphenated", "my-really-long-skill-name", false},
{"single char", "a", false},
{"max length 64", "a234567890123456789012345678901234567890123456789012345678901234", false},
// Dot-namespaced names (per #92): dots are valid segment separators.
{"dot-namespaced", "myorg.bootstrap", false},
{"dot and hyphen mixed", "codebase-intelligence.scan", false},
{"multi-segment dotted", "a.b.c", false},
{"empty", "", true},
{"too long 65", "a2345678901234567890123456789012345678901234567890123456789012345", true},
{"uppercase", "MySkill", true},
Expand All @@ -28,7 +32,12 @@ func TestValidateName(t *testing.T) {
{"trailing hyphen", "skill-", true},
{"double hyphen", "my--skill", true},
{"special chars", "skill@name", true},
{"dot", "my.skill", true},
// Dot-separator placement: same shape rules as hyphen.
{"consecutive dots", "myorg..bootstrap", true},
{"leading dot", ".bootstrap", true},
{"trailing dot", "bootstrap.", true},
{"dot then hyphen", "myorg.-foo", true},
{"hyphen then dot", "myorg-.foo", true},
}

for _, tt := range tests {
Expand Down
Loading