feat: add skill support system#12
Merged
Merged
Conversation
Skills are SKILL.md files under ~/.x-code/skills/<name>/SKILL.md (global) or .x-code/skills/<name>/SKILL.md (project). Loaded at startup; project-level overrides global on name conflict. New capabilities: - core: SkillRegistry + loadSkills loader (YAML frontmatter, zod validation) - core: activateSkill tool — AI injects skill context into conversation - core: AgentOptions.skillRegistry plumbed through to tool creation - cli: /skill list — shows all loaded skills with descriptions - cli: /skill install <url> — downloads via shell into global skills dir - cli: /<skillname> [arg] — activates skill; without arg stores as pending and injects with the user's next real message (pendingSkillRef pattern) - cli: /clear resets any pending skill Bug fixes: - Session slug no longer polluted by <activated_skill> XML as first message - /skill list hint uses platform-correct path (GLOBAL_XCODE_DIR) - Install hint forbids webFetch+write (corrupts YAML); requires shell download Tests: 15 new tests in packages/core/tests/skills.test.ts
…ender
Two threads land together — a menu-render bug surfaced when a skill
description exceeded terminal width, which uncovered the missing
disable/enable infrastructure and a YAML parser gap.
- ChatInput.tsx + chat-input/text-helpers.ts: slash menu now wraps each
description across up to 2 rows (wrapCellsToRows helper, ellipsis on
overflow). Previously a row wider than termWidth would hard-wrap at
the physical-row level — cell-diff treated it as a single grid row,
so subsequent [K clears missed the wrapped overflow, and when the
wrap spilled past the last terminal row the viewport scrolled,
drifting the frame out of sync with lastFrameTopRef and producing
phantom input boxes on every menu open/dismiss cycle.
- skills/loader.ts + sub-agents/loader.ts: YAML frontmatter parser now
folds indented continuation lines into the previous key's value
(standard folded-scalar behavior). Without this, SKILL.md with a
multi-line description: was silently truncated to the first line.
- /skill disable|enable|remove: disable/enable write
~/.x-code/settings.json (global) or .x-code/settings.local.json
(project) under {disabledSkills} — union across scopes. remove
deletes the skill directory and clears both scopes' disable
entries to avoid stale entries silencing a future re-install of
the same name. Default --scope = the skill's own source. Each
command prints "Restart the CLI to apply." SkillRegistry filters
disabled on list/names/get; listAll/getEntry expose disabled state
to the /skill list output (now annotated with [on]/[off] tags).
- loop.ts: one-line debug log agent.skills.system-prompt prints the
enabled and disabled names going into the prompt at session start,
so users can verify disable actually hides the skill from the LLM.
- Tests: registry filter, folded-scalar description, settings noop +
unrelated-field preservation.
Single end-to-end test exercising loader + settings + registry filter together: writes two SKILL.md files to a temp dir (via XC_SKILLS_DIR), writes .x-code/settings.local.json with one of them in disabledSkills, calls createSkillRegistry(), and asserts list/names/get hide the disabled skill while listAll/getEntry retain it. Guards against future refactors that decouple the layers and silently let a disabled skill reach the agent loop — the unit tests above cover each layer in isolation but wouldn't catch a wiring regression.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR introduces a skill system to x-code-cli — reusable agent personas that can be loaded on demand to give the AI specialized context for a task.
How skills work
SKILL.mdfile with YAML frontmatter (name,description) and a markdown body (the persona / instructions).~/.x-code/skills/<name>/SKILL.md.x-code/skills/<name>/SKILL.md(overrides global on name conflict)New capabilities
/skill list/skill install <url>/<skillname>/<skillname> <arg>activateSkilltool/<skillname>pending-skill patternWhen
/hello(or any skill name) is invoked without an argument, the skill is stored in apendingSkillRefand the user sees"Skill hello loaded. Type your request."— the skill XML is then prepended to the user's very next real message. This matches how Gemini CLI, OpenCode, and Claude Code handle skill/persona activation: skill context is always paired with a real user message rather than triggering a standalone AI response.Bug fixes
1. Session slug polluted by skill XML
When
/<skillname>was the first command in a session,generateTaskSlugreceived raw<activated_skill>XML as the "task text", producing garbage slugs likeactivatedskill-namehello-activatedskill-.... Fixed by stripping<activated_skill>blocks fromtaskTextbefore passing togenerateTaskSlugandappendHeaderinloop.ts.2.
/skill listno-skills hint used hardcoded Unix pathThe hint showed
~/.x-code/skills/<name>/SKILL.mdas a literal string on all platforms. Fixed to usepath.join(GLOBAL_XCODE_DIR, 'skills', '<name>', 'SKILL.md')for platform-correct output (e.g.C:\Users\bin\.x-code\skills\<name>\SKILL.mdon Windows).3. AI-guided skill install corrupts YAML frontmatter
When a user asked the AI to install a skill from a URL, the AI used
webFetch(which renders markdown and collapses---separators) +write, producing a single-line file with no valid frontmatter. Fixed by updatinginstallHintinsystem-prompt.tsto explicitly prohibitwebFetch + writeand require shell download commands (Invoke-WebRequeston Windows,curlon macOS/Linux).Files changed
packages/core/src/skills/loader.tspackages/core/src/skills/registry.tsSkillRegistryclass +createSkillRegistry()factorypackages/core/src/tools/activate-skill.tsactivateSkilltool for AI to inject skill contextpackages/core/tests/skills.test.tspackages/core/src/types/index.tsskillRegistry?: SkillRegistrytoAgentOptionspackages/core/src/index.tsSkillRegistry,createSkillRegistry,SkillDefinitionpackages/core/src/agent/loop.ts<activated_skill>XML before slug/header generationpackages/core/src/agent/system-prompt.tspackages/cli/src/index.tsskillRegistry, pass to agent optionspackages/cli/src/ui/components/App.tsx/skillcommand handler,pendingSkillRefpattern, path fixTest plan
SKILL.mdwith valid frontmatter in~/.x-code/skills/test/SKILL.md, restart CLI, run/skill list— skill should appear/test(no arg) — should see "Skill test loaded. Type your request.", then submit a message — AI should receive skill context/test hello world— AI should receive skill context + "hello world" in one shot/skill install <raw-url>— file should be downloaded to~/.x-code/skills/<name>/SKILL.mdwith intact YAMLpnpm test packages/core/tests/skills.test.ts— all 15 tests passpnpm typecheck— no errors/<skillname>— session slug should reflect real user text, not skill XML