Skip to content

Commit 843d356

Browse files
Allow dot-namespaced skill names (#92) (#93)
* Allow dot-namespaced skill names (#92) Extend the ValidateName regex from `^[a-z0-9]+(-[a-z0-9]+)*$` to `^[a-z0-9]+([.-][a-z0-9]+)*$`, accepting dots as a segment separator with the same shape rules as hyphens. Names like `myorg.bootstrap` and `codebase-intelligence.scan` are now valid; `.bootstrap`, `myorg..bootstrap`, `bootstrap.`, and `myorg.-foo` are still rejected. The new regex is a strict superset of the old one — every previously valid name remains valid. No migration required, no JSON shape change. Why: skill installers use dot-namespacing to mark ownership (e.g. `myorg.*`). Platforms that derive slash commands from directory names (GitHub Copilot maps `.copilot/skills/<dir>/` to `/<dir>`) lose the namespace if installers must strip dots before registering. The declared `name:` in `SKILL.md`, the registered directory, and the downstream slash command can now all stay in sync. - internal/skill/skill.go: regex + error message - internal/skill/skill_test.go: positive cases (dotted, mixed) and negative cases (consecutive dots, leading/trailing dot, dot adjacent to hyphen) - AGENTS.md, docs/writing-skills.md, docs/concepts/skill-format.md, docs/reference/validation.md: regex string + note on dot semantics - CHANGELOG.md: Unreleased entry Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Add e2e regression test for dot-namespaced skill name (#92) Cover the full create -> validate -> install -> uninstall -> remove lifecycle with `myorg.bootstrap` so the literal dot in the on-disk directory name is exercised across the registry and an installed platform. The CI Windows matrix picks this up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 924083c commit 843d356

8 files changed

Lines changed: 77 additions & 9 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ Key points:
226226

227227
### Skill Name Validation
228228

229-
Names must match `^[a-z0-9]+(-[a-z0-9]+)*$` and be 1-64 characters.
229+
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`).
230230

231231
### Registry Paths
232232

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- **Dot-namespaced skill names are now valid.** `ValidateName` accepts dots as
13+
a segment separator in addition to hyphens, so names like `myorg.bootstrap`
14+
and `codebase-intelligence.scan` pass validation. The hyphen-only form
15+
(`code-review`) remains valid; this is a strict superset of the previous
16+
regex, no migration required. Enables installers to preserve their
17+
namespace prefix on platforms that derive slash commands from directory
18+
names (e.g. GitHub Copilot maps `.copilot/skills/myorg.bootstrap/` to
19+
`/myorg.bootstrap`). ([#92])
20+
21+
[#92]: https://github.com/devrimcavusoglu/skern/issues/92
22+
1023
## [v0.3.0] — 2026-05-07
1124

1225
Platform registry rewrite, five new adapters, agent-instruction snippet

docs/concepts/skill-format.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ The main technique or pattern (before/after for techniques).
4646

4747
| Field | Required | Description |
4848
|-------|----------|-------------|
49-
| `name` | Yes | Skill name matching `[a-z0-9]+(-[a-z0-9]+)*`, 1-64 chars. Must equal the directory name. |
49+
| `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. |
5050
| `description` | Yes | What the skill does — start with "Use when…". Max 1024 chars. |
5151
| `tags` | No | List of classification tags |
5252
| `allowed-tools` | No | Tools the skill may use. No empty entries. |

docs/reference/validation.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ The summary line counts each separately, e.g. `Skill "x" has 1 error(s), 0 warni
1818

1919
### Name Format
2020

21-
Skill names must match `[a-z0-9]+(-[a-z0-9]+)*` and be 1–64 characters.
21+
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.
2222

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

2525
### Description
2626

docs/writing-skills.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ description: |
5656
- Prefer verb-first active voice: `creating-skills` not `skill-creation`
5757
- Be specific: `condition-based-waiting` not `async-test-helpers`
5858

59-
Names must match `^[a-z0-9]+(-[a-z0-9]+)*$` and be 1-64 characters.
59+
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`).
6060

6161
## Recommended Body Structure
6262

internal/cli/e2e_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,49 @@ func TestEndToEnd_MultiSkillWorkflow(t *testing.T) {
251251
require.NoError(t, err)
252252
}
253253

254+
// TestEndToEnd_DotNamespacedName is a regression test for #92: dot-namespaced
255+
// skill names (e.g. `myorg.bootstrap`) must survive the full create → validate
256+
// → install → uninstall → remove lifecycle, with the literal dot preserved in
257+
// the on-disk directory name in the registry and on installed platforms.
258+
func TestEndToEnd_DotNamespacedName(t *testing.T) {
259+
cc, userDir, _ := testRegistryWithDirs(t)
260+
home := t.TempDir()
261+
project := t.TempDir()
262+
withTestDetector(t, cc, home, project)
263+
264+
const skillName = "myorg.bootstrap"
265+
266+
_, err := runCmd(t, cc, "skill", "create", skillName,
267+
"--description", "Use when bootstrapping a myorg project")
268+
require.NoError(t, err)
269+
270+
registryDir := filepath.Join(userDir, skillName)
271+
_, err = os.Stat(filepath.Join(registryDir, "SKILL.md"))
272+
require.NoError(t, err, "registry directory should contain literal dot in name")
273+
274+
out, err := runCmd(t, cc, "skill", "validate", skillName, "--json")
275+
require.NoError(t, err)
276+
var validateResult output.SkillValidateResult
277+
require.NoError(t, json.Unmarshal([]byte(out), &validateResult))
278+
assert.True(t, validateResult.Valid)
279+
280+
_, err = runCmd(t, cc, "skill", "install", skillName, "--platform", "claude-code")
281+
require.NoError(t, err)
282+
installed := filepath.Join(home, ".claude", "skills", skillName, "SKILL.md")
283+
_, err = os.Stat(installed)
284+
require.NoError(t, err, "installed directory should contain literal dot in name")
285+
286+
_, err = runCmd(t, cc, "skill", "uninstall", skillName, "--platform", "claude-code")
287+
require.NoError(t, err)
288+
_, err = os.Stat(installed)
289+
assert.True(t, os.IsNotExist(err))
290+
291+
_, err = runCmd(t, cc, "skill", "remove", skillName)
292+
require.NoError(t, err)
293+
_, err = os.Stat(registryDir)
294+
assert.True(t, os.IsNotExist(err))
295+
}
296+
254297
// TestEndToEnd_OverlapAndValidation tests the overlap detection and validation
255298
// flows work correctly across the full lifecycle.
256299
func TestEndToEnd_OverlapAndValidation(t *testing.T) {

internal/skill/skill.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,11 @@ const (
1818
ScopeProject Scope = "project"
1919
)
2020

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

2427
// Author represents the creator of a skill.
2528
type Author struct {
@@ -62,7 +65,7 @@ func ValidateName(name string) error {
6265
return fmt.Errorf("skill name cannot exceed 64 characters")
6366
}
6467
if !nameRegex.MatchString(name) {
65-
return fmt.Errorf("skill name %q is invalid: must match [a-z0-9]+(-[a-z0-9]+)* (lowercase alphanumeric with hyphens)", name)
68+
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)
6669
}
6770
return nil
6871
}

internal/skill/skill_test.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ func TestValidateName(t *testing.T) {
1919
{"long hyphenated", "my-really-long-skill-name", false},
2020
{"single char", "a", false},
2121
{"max length 64", "a234567890123456789012345678901234567890123456789012345678901234", false},
22+
// Dot-namespaced names (per #92): dots are valid segment separators.
23+
{"dot-namespaced", "myorg.bootstrap", false},
24+
{"dot and hyphen mixed", "codebase-intelligence.scan", false},
25+
{"multi-segment dotted", "a.b.c", false},
2226
{"empty", "", true},
2327
{"too long 65", "a2345678901234567890123456789012345678901234567890123456789012345", true},
2428
{"uppercase", "MySkill", true},
@@ -28,7 +32,12 @@ func TestValidateName(t *testing.T) {
2832
{"trailing hyphen", "skill-", true},
2933
{"double hyphen", "my--skill", true},
3034
{"special chars", "skill@name", true},
31-
{"dot", "my.skill", true},
35+
// Dot-separator placement: same shape rules as hyphen.
36+
{"consecutive dots", "myorg..bootstrap", true},
37+
{"leading dot", ".bootstrap", true},
38+
{"trailing dot", "bootstrap.", true},
39+
{"dot then hyphen", "myorg.-foo", true},
40+
{"hyphen then dot", "myorg-.foo", true},
3241
}
3342

3443
for _, tt := range tests {

0 commit comments

Comments
 (0)