Skip to content

feat: support grouped skill directories - #270

Merged
pedronauck merged 2 commits into
mainfrom
nested-skills
Jul 29, 2026
Merged

pedronauck merged 2 commits into
mainfrom
nested-skills

Conversation

@pedronauck

@pedronauck pedronauck commented Jul 29, 2026 •

Copy link
Copy Markdown
Member

Summary

  • Discover SKILL.md definitions at arbitrary depth below every supported skill root, allowing transparent organizational groups such as marketing/content/campaign-brief/.
  • Preserve frontmatter name as the skill identity across precedence, workspace payloads, HTTP/UDS, native tools, watcher refreshes, and resource publication.
  • Add compozy skill create <name> --group <relative/path> with transactional scaffolding and path/symlink safety checks.

What changed

Unified discovery

  • Added internal/skillscan as the canonical scanner for filesystem and bundled fs.FS sources.
  • Removed the duplicated fixed-depth scanners and the former depth-four limit.
  • Kept deterministic lexical traversal, the 300-definition cap, hidden/reserved directory filtering, directory-symlink avoidance, and in-root file-symlink containment.
  • Routed global, bundled, workspace, additional-root, user, and agent-local discovery through the shared policy.

Identity and runtime behavior

  • Workspace skill paths now carry the parsed frontmatter name and collapse duplicate names with registry-equivalent ordering.
  • Workspace detail payloads use the resolved identity instead of deriving it from the leaf directory.
  • Nested add, edit, and remove operations invalidate snapshots and refresh the live catalog without a daemon restart.
  • Skill leaf directories remain the ownership root for SKILL.md, mcp.json, hooks, references, scripts, and provenance sidecars.

CLI authoring

  • Added the --group flag to compozy skill create and its generated CLI reference.
  • Rejects absolute paths, traversal, empty/hidden/reserved segments, backslash separators, invalid names, existing targets, and symlinked group/skill roots.
  • Creates and validates the skill in a hidden staging directory before the atomic rename, with cleanup errors preserved.
  • Reports the optional group in JSON, human, and TOON output without changing the generated frontmatter identity.

Documentation and release artifacts

  • Updated runtime docs for grouped layouts, unlimited nesting, transparent identity, source precedence, file locations, overlays, sidecars, and multi-root behavior.
  • Updated the official bundled Compozy skill while keeping the prompt reference smaller than HEAD; the startup prompt budget regression remains green.
  • Added the release highlight in .release-notes/grouped-skill-directories-1785293468.md.

Validation

  • make verify — full monorepo gate passed: codegen, installer, source-size, Bun lint/typecheck/test, web build, Go fmt/lint/test/build, and boundaries.
  • Race-enabled scanner, skills, workspace, CLI, API, and daemon integration suites.
  • HTTP and UDS workspace detail preserve a nested skill's frontmatter identity.
  • compozy__workspace_describe exposes the resolved nested identity.
  • A grouped scaffold resolves through the real workspace resolver and skills registry, then succeeds through CLI list, view, and where.
  • Site Turbo lint, typecheck, test, and content generation.

QA tracker

Final status: untested for manual scenario QA. Automated integration coverage is green, but no real-user QA session was run in this implementation slice.

  • Added docs/qa/scenarios/ET-nested-skill-groups.md for empty groups, nested discovery, live add/edit/remove, shadow precedence, management surfaces, and cross-workspace isolation.
  • The authorized worktree also contained a pre-existing pass → untested reset in docs/qa/scenarios/REL-release-candidate-plan.md; this commit includes it as requested, without claiming a new release-candidate QA run.

Compozy Impact Audit

  • Native tools: compozy__workspace_describe now reports resolved nested skill identities; compozy__skill_list and compozy__skill_view consume the unified catalog. No tool IDs, toolsets, descriptors, schemas, digests, risk flags, availability diagnostics, or capability gates changed.
  • Extensibility and hooks: filesystem and bundled sources share one discovery policy. MCP sidecars, hooks, resources, bundles, registries, and agent-local overlays remain rooted at each skill leaf. Extension-owned runtime skills retain their existing contract. No config.toml key, default, validation rule, or lifecycle changed.
  • Workspace data isolation: definitions remain workspace-scoped. Primary and additional roots are scanned independently, and daemon publication/rebuild coverage verifies that grouped definitions do not leak across workspace IDs. No persisted datum or migration was added.
  • Official Compozy skill: updated skills/compozy/references/tools-and-skills.md with grouped discovery and skill create --group, compacted to remain within the startup prompt budget.
  • Web/Docs impact: no web/ route, component, hook, cache key, or UI control changed. Existing consumers receive the same workspace skill payload shape with corrected names; runtime docs and generated CLI docs document the new behavior.

Compatibility

This is additive public behavior with an intentional greenfield deletion of the fixed-depth implementation. Existing flat skill directories, precedence, disabled-skill handling, shadows, list/view/where operations, and sidecars continue to work unchanged. No migration or compatibility bridge is required.

Summary by CodeRabbit

  • New Features

    • Added skill create --group <path> to scaffold workspace skills into nested group directories.
    • CLI output now includes group information for created skills.
  • Bug Fixes

    • Skill names now consistently use the validated frontmatter name (when present), improving correct identity and resolution across workspace/bundled discovery.
    • Improved handling of nested skills, duplicates, updates, and removals with correct precedence.
  • Security & Reliability

    • Strengthened validation for unsafe/hidden names and --group values, including whitespace normalization and protections against path traversal and symlink escapes.

@vercel

vercel Bot commented Jul 29, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
compozy-site Ready Ready Preview, Comment Jul 29, 2026 4:07am

Request Review

@coderabbitai

coderabbitai Bot commented Jul 29, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 16aade4f-d5c5-4010-8d5e-0c35673800da

📥 Commits

Reviewing files that changed from the base of the PR and between d23029b and 0d01b1a.

⛔ Files ignored due to path filters (11)
  • .release-notes/grouped-skill-directories-1785293468.md is excluded by !**/*.md
  • docs/qa/scenarios/ET-nested-skill-groups.md is excluded by !**/*.md
  • packages/site/content/runtime/cli-reference/skill/create.mdx is excluded by !**/*.mdx
  • packages/site/content/runtime/core/configuration/file-locations.mdx is excluded by !**/*.mdx
  • packages/site/content/runtime/core/configuration/skill-md.mdx is excluded by !**/*.mdx
  • packages/site/content/runtime/core/skills/index.mdx is excluded by !**/*.mdx
  • packages/site/content/runtime/core/skills/skill-md.mdx is excluded by !**/*.mdx
  • packages/site/content/runtime/core/workspaces/config-overlays.mdx is excluded by !**/*.mdx
  • packages/site/content/runtime/core/workspaces/multi-root.mdx is excluded by !**/*.mdx
  • packages/site/content/runtime/core/workspaces/resolver.mdx is excluded by !**/*.mdx
  • skills/compozy/references/tools-and-skills.md is excluded by !**/*.md
📒 Files selected for processing (27)
  • internal/api/core/conversions_workspace_provider.go
  • internal/api/core/session_workspace_internal_test.go
  • internal/api/httpapi/handlers_test.go
  • internal/api/udsapi/handlers_test.go
  • internal/cli/skill.go
  • internal/cli/skill_commands_mutation.go
  • internal/cli/skill_create.go
  • internal/cli/skill_output.go
  • internal/cli/skill_test.go
  • internal/cli/skill_workspace_metadata.go
  • internal/daemon/agent_skill_resources_integration_test.go
  • internal/daemon/daemon_integration_test.go
  • internal/daemon/native_tools_test.go
  • internal/skills/loader.go
  • internal/skills/loader_test.go
  • internal/skills/loader_values.go
  • internal/skills/registry_snapshot.go
  • internal/skills/registry_test.go
  • internal/skillscan/scan.go
  • internal/skillscan/scan_test.go
  • internal/workspace/resolver_integration_test.go
  • internal/workspace/resolver_test.go
  • internal/workspace/scanner.go
  • internal/workspace/skill_identity.go
  • internal/workspace/workspace.go
  • internal/workspace/workspace_test.go
  • skills.test

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

🔥 Files not summarized due to errors (1)
  • skills.test: Server error: no LLM provider could handle the message

Walkthrough

Changes

The PR centralizes SKILL.md discovery, preserves frontmatter-defined skill names through workspace resolution and API payloads, adds grouped skill creation with path-safety checks and atomic staging, and updates resolver, daemon, CLI, and API tests for nested layouts.

Skill workspace lifecycle

Layer / File(s) Summary
Centralized skill scanning
internal/skillscan/*, internal/skills/*
Filesystem and fs.FS scanning now share bounded candidate discovery, filtering, symlink containment, sorting, and snapshot behavior.
Workspace identity and precedence
internal/workspace/*, internal/api/core/*, internal/api/httpapi/handlers_test.go, internal/api/udsapi/handlers_test.go, internal/daemon/native_tools_test.go
Workspace skills carry frontmatter names, discovery-root order determines duplicate resolution, and API and tool projections preserve resolved names.
Grouped skill creation
internal/cli/*
skill create accepts validated group paths, creates skills through staged atomic commits, cleans up failed attempts, and includes group data in output.
Watcher and resource validation
internal/daemon/*
Daemon fixtures use nested skill paths, and watcher tests cover skill metadata updates and deletion after synchronization.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI as skill create command
  participant Metadata as normalizeSkillGroup
  participant Scaffolder as createWorkspaceSkill
  participant Filesystem
  CLI->>Metadata: normalize --group path
  Metadata-->>CLI: canonical group path
  CLI->>Scaffolder: create grouped skill
  Scaffolder->>Filesystem: stage and validate SKILL.md
  Filesystem-->>Scaffolder: committed skill paths
  Scaffolder-->>CLI: output skill and group
Loading

Possibly related issues

  • compozy/agh#3 — Covers daemon-managed skill discovery, workspace precedence, lifecycle refresh, and CLI skill creation addressed by this PR.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding support for grouped skill directories.
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.
✨ Finishing Touches 💡 1
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch nested-skills
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch nested-skills

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread internal/skillscan/scan.go
Comment thread internal/workspace/scanner.go
@greptile-apps

greptile-apps Bot commented Jul 29, 2026 •

Copy link
Copy Markdown

Greptile Summary

Adds grouped skill-directory support across discovery, workspace identity, runtime publication, and CLI scaffolding.

  • Introduces a shared depth-independent scanner with deterministic traversal and filesystem-entry and definition caps.
  • Preserves frontmatter names and shadow precedence across workspace, registry, API, native-tool, and watcher paths.
  • Adds transactional skill create --group scaffolding with path and symlink validation.
  • Updates automated coverage, runtime documentation, QA tracking, and release notes.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains; the traversal is now bounded and duplicate definitions are retained through registry root loading for shadow diagnostics.

Important Files Changed

Filename Overview
internal/skillscan/scan.go Adds canonical deterministic recursive discovery with explicit entry and definition limits, addressing the previously unbounded traversal.
internal/workspace/scanner.go Discovers nested workspace definitions by frontmatter identity while preserving registry-compatible ordering for duplicate handling.
internal/skills/registry_workspace_cache.go Loads workspace roots in precedence order so lower-priority definitions remain available to shadow diagnostics.
internal/cli/skill_create.go Implements staged grouped-skill creation with containment checks, symlink rejection, atomic rename, and cleanup handling.
internal/api/core/conversions_workspace_provider.go Publishes resolved frontmatter identity through workspace API payloads.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  Roots[Skill roots] --> Scanner[Shared skill scanner]
  Scanner --> Identity[Frontmatter identity]
  Identity --> Workspace[Workspace resolver]
  Workspace --> Registry[Skills registry and precedence]
  Registry --> Surfaces[CLI / HTTP / UDS / native tools]
  Create[skill create --group] --> Stage[Validate staging directory]
  Stage --> Roots
Loading

Reviews (2): Last reviewed commit: "fix: review round" | Re-trigger Greptile

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
internal/skillscan/scan.go (1)

184-201: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Resolve the root once instead of per candidate.

pathWithinRoot calls filepath.EvalSymlinks(root) for every discovered SKILL.md, so a full scan performs up to MaxCandidates redundant root resolutions on a path that runs on every workspace resolve/watcher refresh. Resolve the root once before the walk and pass it into the containment check.

♻️ Proposed refactor
-func pathWithinRoot(root string, candidate string) error {
-	resolvedRoot, err := filepath.EvalSymlinks(root)
-	if err != nil {
-		return fmt.Errorf("resolve root %q: %w", root, err)
-	}
-	resolvedCandidate, err := filepath.EvalSymlinks(candidate)
+func pathWithinRoot(resolvedRoot string, candidate string) error {
+	resolvedCandidate, err := filepath.EvalSymlinks(candidate)
 	if err != nil {
 		return fmt.Errorf("resolve definition %q: %w", candidate, err)
 	}

Then in ScanDirectory, compute it once before filepath.WalkDir:

resolvedRoot, err := filepath.EvalSymlinks(absRoot)
if err != nil {
    return DirectoryResult{}, fmt.Errorf("skillscan: resolve root %q: %w", absRoot, err)
}

and call pathWithinRoot(resolvedRoot, candidate) inside the walk.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/skillscan/scan.go` around lines 184 - 201, Update ScanDirectory to
resolve absRoot once with filepath.EvalSymlinks before filepath.WalkDir,
returning the existing contextual error on failure. Change pathWithinRoot to
accept the already-resolved root and remove its per-candidate root resolution,
while preserving candidate resolution and containment validation for each
discovered SKILL.md.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/cli/skill_create.go`:
- Around line 61-77: Update the staging-directory flow after validation and
before os.Rename in the skill creation function to explicitly chmod stagingDir
to 0755. Handle any chmod error with the same contextual error-wrapping style,
then preserve the existing commit behavior so the renamed skill directory has
the intended permissions.

In `@internal/cli/skill_workspace_metadata.go`:
- Around line 50-59: Update the skill-group segment loop to retain each
normalized value returned by normalizeSkillName, collect those normalized
segments, and join the collection for the returned path. Preserve empty-segment
validation and error propagation while ensuring inputs with surrounding
whitespace produce trimmed directory names.

---

Nitpick comments:
In `@internal/skillscan/scan.go`:
- Around line 184-201: Update ScanDirectory to resolve absRoot once with
filepath.EvalSymlinks before filepath.WalkDir, returning the existing contextual
error on failure. Change pathWithinRoot to accept the already-resolved root and
remove its per-candidate root resolution, while preserving candidate resolution
and containment validation for each discovered SKILL.md.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 947dcd0a-6978-4956-aa99-e9e83f2faf77

📥 Commits

Reviewing files that changed from the base of the PR and between 9f985ce and d23029b.

⛔ Files ignored due to path filters (12)
  • .release-notes/grouped-skill-directories-1785293468.md is excluded by !**/*.md
  • docs/qa/scenarios/ET-nested-skill-groups.md is excluded by !**/*.md
  • docs/qa/scenarios/REL-release-candidate-plan.md is excluded by !**/*.md
  • packages/site/content/runtime/cli-reference/skill/create.mdx is excluded by !**/*.mdx
  • packages/site/content/runtime/core/configuration/file-locations.mdx is excluded by !**/*.mdx
  • packages/site/content/runtime/core/configuration/skill-md.mdx is excluded by !**/*.mdx
  • packages/site/content/runtime/core/skills/index.mdx is excluded by !**/*.mdx
  • packages/site/content/runtime/core/skills/skill-md.mdx is excluded by !**/*.mdx
  • packages/site/content/runtime/core/workspaces/config-overlays.mdx is excluded by !**/*.mdx
  • packages/site/content/runtime/core/workspaces/multi-root.mdx is excluded by !**/*.mdx
  • packages/site/content/runtime/core/workspaces/resolver.mdx is excluded by !**/*.mdx
  • skills/compozy/references/tools-and-skills.md is excluded by !**/*.md
📒 Files selected for processing (26)
  • internal/api/core/conversions_workspace_provider.go
  • internal/api/core/session_workspace_internal_test.go
  • internal/api/httpapi/handlers_test.go
  • internal/api/udsapi/handlers_test.go
  • internal/cli/skill.go
  • internal/cli/skill_commands_mutation.go
  • internal/cli/skill_create.go
  • internal/cli/skill_output.go
  • internal/cli/skill_test.go
  • internal/cli/skill_workspace_metadata.go
  • internal/daemon/agent_skill_resources_integration_test.go
  • internal/daemon/daemon_integration_test.go
  • internal/daemon/native_tools_test.go
  • internal/skills/loader.go
  • internal/skills/loader_test.go
  • internal/skills/loader_values.go
  • internal/skills/registry_snapshot.go
  • internal/skills/registry_test.go
  • internal/skillscan/scan.go
  • internal/skillscan/scan_test.go
  • internal/workspace/resolver_integration_test.go
  • internal/workspace/resolver_test.go
  • internal/workspace/scanner.go
  • internal/workspace/skill_identity.go
  • internal/workspace/workspace.go
  • internal/workspace/workspace_test.go
💤 Files with no reviewable changes (2)
  • internal/skills/loader_values.go
  • internal/skills/loader_test.go

Comment thread internal/cli/skill_create.go
Comment on lines +50 to +59
segments := strings.Split(trimmed, "/")
for _, segment := range segments {
if segment == "" {
return "", errors.New("skill group must not contain empty segments")
}
if _, err := normalizeSkillName(segment); err != nil {
return "", fmt.Errorf("skill group segment %q: %w", segment, err)
}
}
return strings.Join(segments, "/"), nil

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use the normalized segment instead of discarding it.

normalizeSkillName trims each segment but the result is dropped (_), so --group "marketing / campaigns" passes validation and then creates directories literally named "marketing " and " campaigns". Rebuild the joined path from normalized segments.

🛠️ Proposed fix
 	segments := strings.Split(trimmed, "/")
-	for _, segment := range segments {
+	for i, segment := range segments {
 		if segment == "" {
 			return "", errors.New("skill group must not contain empty segments")
 		}
-		if _, err := normalizeSkillName(segment); err != nil {
+		normalized, err := normalizeSkillName(segment)
+		if err != nil {
 			return "", fmt.Errorf("skill group segment %q: %w", segment, err)
 		}
+		segments[i] = normalized
 	}
 	return strings.Join(segments, "/"), nil
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
segments := strings.Split(trimmed, "/")
for _, segment := range segments {
if segment == "" {
return "", errors.New("skill group must not contain empty segments")
}
if _, err := normalizeSkillName(segment); err != nil {
return "", fmt.Errorf("skill group segment %q: %w", segment, err)
}
}
return strings.Join(segments, "/"), nil
segments := strings.Split(trimmed, "/")
for i, segment := range segments {
if segment == "" {
return "", errors.New("skill group must not contain empty segments")
}
normalized, err := normalizeSkillName(segment)
if err != nil {
return "", fmt.Errorf("skill group segment %q: %w", segment, err)
}
segments[i] = normalized
}
return strings.Join(segments, "/"), nil
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cli/skill_workspace_metadata.go` around lines 50 - 59, Update the
skill-group segment loop to retain each normalized value returned by
normalizeSkillName, collect those normalized segments, and join the collection
for the returned path. Preserve empty-segment validation and error propagation
while ensuring inputs with surrounding whitespace produce trimmed directory
names.

Add unlimited-depth skill discovery across bundled and filesystem sources, preserve frontmatter identity through workspace surfaces, and support transactional workspace scaffolding with --group.

Document grouped layouts, add release notes and QA coverage, and keep the official skill within its startup prompt budget.

This branch was successfully deployed

1 active deployment
Preview — 0d01b1ab Deployed Jul 29, 2026 by vercel[bot]
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