From 8d4db8779cab2b99823e441d40eb282b12a1c3de Mon Sep 17 00:00:00 2001 From: devrimcavusoglu Date: Wed, 13 May 2026 13:11:28 +0300 Subject: [PATCH 1/2] Allow dot-namespaced skill names (#92) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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//` to `/`) 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) --- AGENTS.md | 2 +- CHANGELOG.md | 13 +++++++++++++ docs/concepts/skill-format.md | 2 +- docs/reference/validation.md | 4 ++-- docs/writing-skills.md | 2 +- internal/skill/skill.go | 9 ++++++--- internal/skill/skill_test.go | 11 ++++++++++- 7 files changed, 34 insertions(+), 9 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index f7abf04..9ea9122 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 53789a3..e0a46aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/concepts/skill-format.md b/docs/concepts/skill-format.md index 30e42c3..ce0c2d0 100644 --- a/docs/concepts/skill-format.md +++ b/docs/concepts/skill-format.md @@ -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. | diff --git a/docs/reference/validation.md b/docs/reference/validation.md index 5f2446b..9d9546f 100644 --- a/docs/reference/validation.md +++ b/docs/reference/validation.md @@ -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 diff --git a/docs/writing-skills.md b/docs/writing-skills.md index 23d9651..09ec265 100644 --- a/docs/writing-skills.md +++ b/docs/writing-skills.md @@ -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 diff --git a/internal/skill/skill.go b/internal/skill/skill.go index 7606605..12a373a 100644 --- a/internal/skill/skill.go +++ b/internal/skill/skill.go @@ -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 { @@ -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 } diff --git a/internal/skill/skill_test.go b/internal/skill/skill_test.go index 08bf2a5..ed080d8 100644 --- a/internal/skill/skill_test.go +++ b/internal/skill/skill_test.go @@ -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}, @@ -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 { From 315ba6114c82f024d2e5713ec0295bd1ffa8bac1 Mon Sep 17 00:00:00 2001 From: devrimcavusoglu Date: Wed, 13 May 2026 17:08:56 +0300 Subject: [PATCH 2/2] 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) --- internal/cli/e2e_test.go | 43 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/internal/cli/e2e_test.go b/internal/cli/e2e_test.go index 1f39b02..5233206 100644 --- a/internal/cli/e2e_test.go +++ b/internal/cli/e2e_test.go @@ -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) {