Skip to content

feat: agent skill management#1684

Merged
yottahmd merged 6 commits intomainfrom
agent-skills
Feb 16, 2026
Merged

feat: agent skill management#1684
yottahmd merged 6 commits intomainfrom
agent-skills

Conversation

@yottahmd
Copy link
Copy Markdown
Collaborator

@yottahmd yottahmd commented Feb 16, 2026

Summary by CodeRabbit

Release Notes

  • New Features
    • Added agent skills management interface for administrators with create, edit, delete, and enable/disable capabilities.
    • Added Markdown editor component for skill documentation editing.
    • Skills are now tracked in Git sync alongside DAGs and memory files.
    • New API endpoints for managing agent skills and configuring enabled skills.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 16, 2026

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This pull request introduces a comprehensive agent skills management system with CRUD operations, file-based persistence, API endpoints, and UI components. Skills are stored as SKILL.md files with YAML frontmatter metadata, discoverable via git sync, and managed through admin-only endpoints with validation and access control.

Changes

Cohort / File(s) Summary
API Specification
api/v1/api.yaml
Added 6 new REST endpoints for skill CRUD and enabling management under /settings/agent/skills and PUT /settings/agent/enabled-skills. Introduced SkillResponse, ListSkillsResponse, CreateSkillRequest, UpdateSkillRequest, SetEnabledSkillsRequest, SetEnabledSkillsResponse schemas. Extended SyncItemKind enum with skill variant.
Agent Configuration
internal/agent/model_config.go
Added EnabledSkills field to Config struct to track enabled skill IDs.
Skill Domain Model
internal/agent/skill.go, internal/agent/skill_test.go
Introduced Skill entity, SkillType enum (custom/builtin), SkillStore interface with CRUD methods, error sentinels, and ValidateSkillID function with regex pattern matching and length constraints. Comprehensive unit tests validate ID validation logic.
File-Based Skill Storage
internal/persis/fileagentskill/store.go, store_test.go
Implemented filesystem-backed SkillStore with skills stored as {id}/SKILL.md files. Features include atomic writes, in-memory indexing by ID and name, frontmatter parsing, path traversal protection, and duplicate name handling. Extensive test suite (756 lines) covers CRUD, validation, concurrency, and error cases.
Git Sync Integration
internal/gitsync/service.go, internal/gitsync/state.go
Added scanSkillFiles to detect SKILL.md entries in skills directory, DAGKindSkill constant for categorization, and KindForDAGID logic to identify skill DAGs. Updated file filtering to allow .md files from skills directory.
Frontend API Handler
internal/service/frontend/api/v1/agent_skills.go, api.go, sync.go
Implemented 6 API endpoint handlers (ListAgentSkills, CreateAgentSkill, GetAgentSkill, UpdateAgentSkill, DeleteAgentSkill, SetEnabledSkills) with admin-only access control, error mapping, and audit logging. Added agentSkillStore dependency to API and toAPISyncItemKind mapping for SyncItemKindSkill.
Server Initialization
internal/service/frontend/server.go
Wired file-based skill store initialization with fallback to nil on error, and propagated store to API via WithAgentSkillStore option.
Frontend Routes and Navigation
ui/src/App.tsx, ui/src/menu.tsx
Added /agent-skills routes (list, create, edit) wrapped with AdminElement. Added Agent Skills navigation item in Admin section with Sparkles icon when agent is enabled.
Frontend API Schema
ui/src/api/v1/schema.ts
Generated TypeScript types and operation interfaces for 6 skill endpoints and related schemas (SkillResponse, ListSkillsResponse, CreateSkillRequest, UpdateSkillRequest, SetEnabledSkillsRequest, SetEnabledSkillsResponse). Extended SyncItemKind enum with skill and memory variants.
Frontend Components and Pages
ui/src/components/editors/MarkdownEditor.tsx, ui/src/pages/agent-skills/index.tsx, ui/src/pages/agent-skills/SkillEditorPage.tsx, ui/src/pages/git-sync/index.tsx
Introduced MarkdownEditor component for knowledge field editing. Added AgentSkillsPage with search, filtering (all/enabled/disabled tabs), CRUD actions, and toggle for skill enablement. Added SkillEditorPage supporting create/edit modes with auto-generated ID slugs and debounced saves. Updated Git Sync UI to recognize and filter skill-kind items with dedicated badge and counts.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client/UI
    participant Handler as API Handler
    participant SkillStore as SkillStore
    participant FileSystem as File System
    participant ConfigStore as ConfigStore

    Client->>Handler: POST /settings/agent/skills (CreateSkillRequest)
    Handler->>Handler: Validate input & check admin permission
    Handler->>SkillStore: Create(ctx, skill)
    SkillStore->>FileSystem: Create {id}/ directory
    SkillStore->>FileSystem: Write SKILL.md (frontmatter + body)
    SkillStore->>SkillStore: Update byID & byName indices
    FileSystem-->>SkillStore: Success
    SkillStore-->>Handler: Skill created
    Handler->>Handler: Log audit event
    Handler-->>Client: 201 Created + SkillResponse

    Client->>Handler: PUT /settings/agent/enabled-skills (SetEnabledSkillsRequest)
    Handler->>Handler: Validate skill IDs exist
    Handler->>ConfigStore: Update EnabledSkills
    ConfigStore->>FileSystem: Persist config
    ConfigStore-->>Handler: Success
    Handler->>Handler: Log audit event
    Handler-->>Client: 200 OK + SetEnabledSkillsResponse
Loading
sequenceDiagram
    participant Scanner as Git Sync Scanner
    participant FileSystem as File System
    participant DAGState as DAG State
    participant DAGSync as DAG Sync Engine

    Scanner->>FileSystem: Scan skills/ directory
    Scanner->>FileSystem: Find SKILL.md files
    Scanner->>FileSystem: Read file & compute hash
    FileSystem-->>Scanner: File content
    Scanner->>DAGState: Set Kind = DAGKindSkill
    Scanner->>DAGState: Set Status = StatusUntracked
    DAGState-->>Scanner: Updated state
    Scanner->>DAGSync: Register skill DAG item
    DAGSync->>DAGSync: Include in sync operations
    DAGSync-->>Scanner: Complete
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.81% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: agent skill management' clearly and concisely summarizes the main change: adding comprehensive agent skill management functionality including CRUD operations, API endpoints, and UI components.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch agent-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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
internal/service/frontend/api/v1/sync.go (1)

398-407: ⚠️ Potential issue | 🟠 Major

syncItemFilePath does not handle DAGKindSkill — skills will get a .yaml extension.

This function only maps DAGKindMemory to .md; skill items (which are also .md files) will fall through to the default .yaml extension. Since the PR adds skill kind support in toAPISyncItemKind just above, this function should be updated too.

🐛 Proposed fix
 func syncItemFilePath(itemID string, kind gitsync.DAGKind) string {
 	if kind == "" {
 		kind = gitsync.KindForDAGID(itemID)
 	}
 	ext := ".yaml"
-	if kind == gitsync.DAGKindMemory {
+	if kind == gitsync.DAGKindMemory || kind == gitsync.DAGKindSkill {
 		ext = ".md"
 	}
 	return itemID + ext
 }
ui/src/pages/git-sync/index.tsx (1)

703-721: ⚠️ Potential issue | 🟠 Major

Skill and memory items link to /dags/... which is incorrect for non-DAG items.

Line 706 links all items to /dags/${encodeURIComponent(itemId)}, but skill items should link to /agent-skills/${skillId} and memory items shouldn't link to a DAG page at all. Clicking a skill row's name will navigate to a non-existent DAG page.

🔧 Suggested approach
                    <div className="flex items-center gap-1.5">
-                      <a
-                        href={`/dags/${encodeURIComponent(itemId)}`}
-                        className="font-mono hover:underline"
-                      >
-                        {item.displayName}
-                      </a>
+                      {kind === 'skill' ? (
+                        <a
+                          href={`/agent-skills/${encodeURIComponent(itemId)}`}
+                          className="font-mono hover:underline"
+                        >
+                          {item.displayName}
+                        </a>
+                      ) : kind === 'memory' ? (
+                        <span className="font-mono">{item.displayName}</span>
+                      ) : (
+                        <a
+                          href={`/dags/${encodeURIComponent(itemId)}`}
+                          className="font-mono hover:underline"
+                        >
+                          {item.displayName}
+                        </a>
+                      )}
🤖 Fix all issues with AI agents
In `@api/v1/api.yaml`:
- Around line 4068-4255: The OpenAPI operations getAgentSkill, updateAgentSkill,
deleteAgentSkill and setEnabledSkills are missing 400 response definitions for
invalid/unknown skill IDs; add a "400" response to each operation (GET
/settings/agent/skills/{skillId}, PATCH /settings/agent/skills/{skillId}, DELETE
/settings/agent/skills/{skillId}, and PUT /settings/agent/enabled-skills)
mirroring the existing Error schema entries (application/json -> $ref:
"#/components/schemas/Error") so generated clients can handle bad-request errors
properly.

In `@internal/agent/skill.go`:
- Around line 28-38: The Skill struct's SchemaVersion field isn't persisted
because the on-disk representation (skillFrontmatter) lacks that field; update
the persistence layer by adding a SchemaVersion int `yaml:"schema_version"
json:"schemaVersion"` to the skillFrontmatter type in the fileagent skill store
so it round-trips when reading/writing, and ensure the code that maps between
skillFrontmatter and Skill copies SchemaVersion; alternatively, if SchemaVersion
is unused, remove SchemaVersion from the Skill domain struct to keep models
consistent.

In `@internal/persis/fileagentskill/store_test.go`:
- Around line 333-349: The test "name conflict returns error" in store_test.go
currently asserts agent.ErrSkillAlreadyExists but after fixing store.Update to
return the proper sentinel for a name collision it should assert
agent.ErrSkillNameAlreadyExists instead; update the assertion in that test (the
require/assert around the call to store.Update in the t.Run block) to use
agent.ErrSkillNameAlreadyExists so it matches the corrected behavior of
store.Update.

In `@internal/persis/fileagentskill/store.go`:
- Around line 357-361: The update logic in the method that checks nameChanged
(variables: nameChanged, existing, s.byName, takenByID, taken, skill.ID)
currently returns agent.ErrSkillAlreadyExists on a name uniqueness conflict;
change the returned sentinel to agent.ErrSkillNameAlreadyExists so name
conflicts use the same error as Create for consistency.

In `@internal/service/frontend/api/v1/agent_skills.go`:
- Around line 283-291: The code assumes cfg is non-nil after calling
a.agentConfigStore.Load; add a nil-check after Load to guard against a (nil,
nil) return and return ErrFailedToLoadAgentConfig (or an appropriate error)
instead of dereferencing cfg; update the block around agentConfigStore.Load /
cfg.EnabledSkills / a.agentConfigStore.Save to handle cfg == nil before
assigning cfg.EnabledSkills and calling Save, and apply the same nil-guard
pattern to the similar load/save sequence used elsewhere (the other
agentConfigStore.Load/Save block).
- Around line 324-342: applySkillUpdates currently allows update.Knowledge to be
set to an empty string which can clear a required field; change the logic in
applySkillUpdates (working with agent.Skill and api.UpdateSkillRequest) so when
update.Knowledge != nil you trim whitespace (strings.TrimSpace) and only assign
to skill.Knowledge if the trimmed value is non-empty — otherwise ignore the
update (do not set an empty value); this preserves existing knowledge when
clients send empty/whitespace updates and prevents invalid skills.
- Around line 25-42: Rename the package-level error variables to follow the
Err... convention: change errAgentSkillStoreNotAvailable to
ErrAgentSkillStoreNotAvailable, errSkillNotFound to ErrSkillNotFound, and
errSkillAlreadyExists to ErrSkillAlreadyExists; update all references/usages
across this file and any callers to use the new names (search for the original
symbols errAgentSkillStoreNotAvailable, errSkillNotFound, errSkillAlreadyExists
and replace them), keeping the error values and types unchanged.
- Around line 54-64: The code currently ignores errors from
a.agentConfigStore.Load(ctx) in the handler that lists skills; change this to
check and handle the error instead of discarding it—log the failure (using
logger.Error with tag.Error(err)) and return an appropriate Error (e.g.,
ErrFailedToLoadAgentConfig or an Error with Code api.ErrorCodeInternalError and
HTTP 500) so callers know the config load failed; apply the same pattern in the
related methods GetAgentSkill and UpdateAgentSkill where a.agentConfigStore.Load
is called to ensure config load errors aren’t silently treated as “no enabled
skills.”

In `@ui/src/pages/agent-skills/index.tsx`:
- Around line 69-92: handleToggleEnabled currently reads the outer-scope skills
array (line using skills.filter) which can be stale during rapid toggles; change
the logic to derive the updated enabled list from a functional state updater so
you always use the latest state before calling client.PUT. Specifically, compute
newEnabled inside a setSkills(prev => { const currentEnabled = prev.filter(s =>
s.enabled).map(s => s.id); const updatedEnabled = skill.enabled ?
currentEnabled.filter(id => id !== skill.id) : [...currentEnabled, skill.id];
return prev.map(s => s.id === skill.id ? { ...s, enabled: !skill.enabled } : s);
}) and then use that updatedEnabled value for the client.PUT call (ensure the
API call uses the enabled IDs produced from the same functional update rather
than the closure-captured skills). This ensures handleToggleEnabled, setSkills
and client.PUT all operate on the same, latest enabled list and avoids race
conditions.
- Around line 157-161: The success banner JSX that renders when the `success`
variable is truthy uses `text-green-600` which lacks a dark-mode variant; update
the class list on that element (the JSX block rendering `{success}`) to include
a dark-mode text color (e.g., replace or augment `text-green-600` with a
matching dark variant such as `text-green-600 dark:text-green-400`) so the text
respects dark mode per the styling guidelines.

In `@ui/src/pages/git-sync/index.tsx`:
- Around line 611-615: The display is inconsistent: selectedCounts.memory is
always shown while selectedCounts.skill is conditional; update the JSX that
renders the summary (the span around "Selected: ...") to treat memory the same
as skill by only rendering the memory count when selectedCounts.memory > 0
(mirror the existing conditional used for selectedCounts.skill) and adjust comma
punctuation so commas are only inserted between visible segments; locate the
rendering using the selectedCounts identifier in ui/src/pages/git-sync/index.tsx
and modify the conditional expressions to ensure both memory and skill follow
the same visibility rule.
🧹 Nitpick comments (9)
internal/agent/skill.go (1)

50-68: Consider extracting a shared slug-validation helper.

ValidateSkillID is structurally identical to ValidateModelID in model_config.go (same regex, same max length, same error wrapping). A shared validateSlugID(id string, sentinel error) error helper could reduce duplication.

ui/src/components/editors/MarkdownEditor.tsx (3)

24-31: Redundant theme-sync effect — the editor hasn't mounted yet when this runs.

This useEffect fires after the first render, but the Monaco editor mounts asynchronously via onMount, so editorRef.current will almost always be null here. The initial theme is already correctly set via the theme prop on the <MonacoEditor> component (line 76), making this effect a no-op in practice.

🔧 Suggested cleanup
-  useEffect(() => {
-    if (editorRef.current) {
-      const newTheme = document.documentElement.classList.contains('dark')
-        ? 'vs-dark'
-        : 'vs';
-      monaco.editor.setTheme(newTheme);
-    }
-  }, []);
-

60-64: F-key handler only blocks the letter 'f', not function keys (F1–F12).

e.code === 'KeyF' matches the physical 'F' key, not function keys like F1, F5, etc. (those have codes like 'F1', 'F5'). If this is intended to prevent a parent hotkey (e.g., a global search triggered by 'f') from capturing keystrokes while the editor is focused, this works. But the comment says "F-key" which is ambiguous — consider clarifying the intent with a brief inline comment.


18-22: Remove manual dispose() call as @monaco-editor/react handles cleanup automatically.

The @monaco-editor/react wrapper automatically disposes the editor instance on unmount. Calling dispose() manually can interfere with the wrapper's own cleanup logic. Remove this unless you've observed a specific memory leak that requires it.

Note: The same pattern exists in ui/src/features/dags/components/dag-editor/DAGEditor.tsx and should also be removed.

ui/src/pages/agent-skills/SkillEditorPage.tsx (2)

146-148: Loading state returns null, blanking the page briefly.

Per coding guidelines, full-page loading overlays should be avoided. Returning null here is acceptable for an initial load (no stale data exists), but it causes a brief flash of empty content. Consider rendering the page shell (header, empty form skeleton) immediately and only guarding the data-dependent fields, so the user sees the layout instantly.


50-71: Fetch effect lacks an abort mechanism and runs as a fire-and-forget IIFE.

If the user navigates away or remoteNode changes before the fetch completes, the stale response still calls setName, setDescription, etc. In React 19 this is silently ignored on unmounted components, but a stale response from a previous remoteNode could still incorrectly overwrite state if the component remains mounted and remoteNode changes. Consider using an AbortController or a stale-closure guard:

🛡️ Suggested improvement with cleanup flag
   useEffect(() => {
     if (isCreating || !skillId) return;
+    let cancelled = false;
 
     (async () => {
       const { data, error } = await client.GET('/settings/agent/skills/{skillId}', {
         params: { path: { skillId }, query: { remoteNode } },
       });
+      if (cancelled) return;
       if (error) {
         showError(error.message || 'Failed to load skill');
         navigate('/agent-skills');
         return;
       }
       setName(data.name);
       setIdField(data.id);
       setDescription(data.description ?? '');
       setKnowledge(data.knowledge);
       setVersion(data.version ?? '');
       setAuthor(data.author ?? '');
       setTagsInput(data.tags?.join(', ') ?? '');
       setIsLoading(false);
     })();
+
+    return () => { cancelled = true; };
   }, [skillId, isCreating, client, remoteNode, showError, navigate]);
ui/src/pages/agent-skills/index.tsx (1)

35-41: Consider using the toast system for transient success/error messages instead of inline banners.

The success and error state variables persist until the next action. The SkillEditorPage already uses showToast for success feedback. Using the same pattern here would be more consistent and avoid stale messages.

ui/src/pages/git-sync/index.tsx (1)

607-607: Nested ternary is getting complex — consider a lookup map.

With four type values, the inline ternary chain is becoming hard to read. A simple map would be cleaner:

♻️ Suggested refactor
-              {f === 'all' ? 'All' : f === 'dag' ? 'DAGs' : f === 'memory' ? 'Memory' : 'Skills'} ({typeCounts[f]})
+              {({ all: 'All', dag: 'DAGs', memory: 'Memory', skill: 'Skills' })[f]} ({typeCounts[f]})
api/v1/api.yaml (1)

7179-7287: Add schema-level constraints to mirror server validation.
The server enforces non-empty name/knowledge and a skill ID regex; encoding those constraints in the schema improves client-side validation and error messaging.

✏️ Suggested schema constraints
     CreateSkillRequest:
       type: object
       required:
         - name
         - knowledge
       properties:
         id:
           type: string
+          pattern: "^[a-z0-9]+(-[a-z0-9]+)*$"
         name:
           type: string
+          minLength: 1
         knowledge:
           type: string
+          minLength: 1

     SetEnabledSkillsRequest:
       type: object
       required:
         - skillIds
       properties:
         skillIds:
           type: array
+          uniqueItems: true
           items:
             type: string
+            pattern: "^[a-z0-9]+(-[a-z0-9]+)*$"

Comment thread api/v1/api.yaml
Comment thread internal/agent/skill.go
Comment thread internal/persis/fileagentskill/store_test.go
Comment thread internal/persis/fileagentskill/store.go
Comment thread internal/service/frontend/api/v1/agent_skills.go
Comment thread internal/service/frontend/api/v1/agent_skills.go
Comment thread internal/service/frontend/api/v1/agent_skills.go
Comment thread ui/src/pages/agent-skills/index.tsx
Comment thread ui/src/pages/agent-skills/index.tsx
Comment thread ui/src/pages/git-sync/index.tsx
@yottahmd yottahmd merged commit ae13102 into main Feb 16, 2026
6 checks passed
@yottahmd yottahmd deleted the agent-skills branch February 16, 2026 14:55
@codecov
Copy link
Copy Markdown

codecov Bot commented Feb 16, 2026

Codecov Report

❌ Patch coverage is 68.01619% with 79 lines in your changes missing coverage. Please review.
✅ Project coverage is 69.70%. Comparing base (7bf67e4) to head (e17ba7b).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
internal/persis/fileagentskill/store.go 72.98% 28 Missing and 29 partials ⚠️
internal/gitsync/service.go 29.16% 17 Missing ⚠️
internal/agent/skill.go 62.50% 0 Missing and 3 partials ⚠️
internal/gitsync/state.go 50.00% 1 Missing and 1 partial ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #1684      +/-   ##
==========================================
- Coverage   69.80%   69.70%   -0.11%     
==========================================
  Files         357      359       +2     
  Lines       39696    39941     +245     
==========================================
+ Hits        27710    27841     +131     
- Misses       9779     9856      +77     
- Partials     2207     2244      +37     
Files with missing lines Coverage Δ
internal/agent/model_config.go 96.22% <ø> (ø)
internal/gitsync/state.go 67.79% <50.00%> (-1.30%) ⬇️
internal/agent/skill.go 62.50% <62.50%> (ø)
internal/gitsync/service.go 26.61% <29.16%> (+0.02%) ⬆️
internal/persis/fileagentskill/store.go 72.98% <72.98%> (ø)

... and 12 files with indirect coverage changes


Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 7bf67e4...e17ba7b. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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