Skip to content

feat: add skill support system#12

Merged
woai3c merged 5 commits into
mainfrom
feat/skill-support
May 23, 2026
Merged

feat: add skill support system#12
woai3c merged 5 commits into
mainfrom
feat/skill-support

Conversation

@woai3c
Copy link
Copy Markdown
Owner

@woai3c woai3c commented May 22, 2026

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

  • A skill is a SKILL.md file with YAML frontmatter (name, description) and a markdown body (the persona / instructions).
  • Global skills: ~/.x-code/skills/<name>/SKILL.md
  • Project skills: .x-code/skills/<name>/SKILL.md (overrides global on name conflict)
  • Skills are scanned and loaded at CLI startup; a restart is required after installing new ones.

New capabilities

Feature Details
/skill list Lists all loaded skills with their descriptions
/skill install <url> Downloads a skill from a URL into the global skills dir via shell (curl/Invoke-WebRequest)
/<skillname> Activates a skill by name
/<skillname> <arg> Activates skill and immediately submits the argument as the first message
activateSkill tool AI-callable tool that injects skill context into the conversation

/<skillname> pending-skill pattern

When /hello (or any skill name) is invoked without an argument, the skill is stored in a pendingSkillRef and 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, generateTaskSlug received raw <activated_skill> XML as the "task text", producing garbage slugs like activatedskill-namehello-activatedskill-.... Fixed by stripping <activated_skill> blocks from taskText before passing to generateTaskSlug and appendHeader in loop.ts.

2. /skill list no-skills hint used hardcoded Unix path

The hint showed ~/.x-code/skills/<name>/SKILL.md as a literal string on all platforms. Fixed to use path.join(GLOBAL_XCODE_DIR, 'skills', '<name>', 'SKILL.md') for platform-correct output (e.g. C:\Users\bin\.x-code\skills\<name>\SKILL.md on 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 updating installHint in system-prompt.ts to explicitly prohibit webFetch + write and require shell download commands (Invoke-WebRequest on Windows, curl on macOS/Linux).

Files changed

File Change
packages/core/src/skills/loader.ts NEW — scans skill dirs, parses YAML frontmatter, validates with zod
packages/core/src/skills/registry.ts NEW — SkillRegistry class + createSkillRegistry() factory
packages/core/src/tools/activate-skill.ts NEW — activateSkill tool for AI to inject skill context
packages/core/tests/skills.test.ts NEW — 15 tests covering loader and registry
packages/core/src/types/index.ts Added skillRegistry?: SkillRegistry to AgentOptions
packages/core/src/index.ts Exports SkillRegistry, createSkillRegistry, SkillDefinition
packages/core/src/agent/loop.ts Strip <activated_skill> XML before slug/header generation
packages/core/src/agent/system-prompt.ts Add skill context + install hint to system prompt
packages/cli/src/index.ts Create skillRegistry, pass to agent options
packages/cli/src/ui/components/App.tsx /skill command handler, pendingSkillRef pattern, path fix

Test plan

  • Place a SKILL.md with valid frontmatter in ~/.x-code/skills/test/SKILL.md, restart CLI, run /skill list — skill should appear
  • Run /test (no arg) — should see "Skill test loaded. Type your request.", then submit a message — AI should receive skill context
  • Run /test hello world — AI should receive skill context + "hello world" in one shot
  • Run /skill install <raw-url> — file should be downloaded to ~/.x-code/skills/<name>/SKILL.md with intact YAML
  • Ask AI to install a skill from a URL — AI should use shell commands, not webFetch
  • Run pnpm test packages/core/tests/skills.test.ts — all 15 tests pass
  • Run pnpm typecheck — no errors
  • First message after /<skillname> — session slug should reflect real user text, not skill XML

woai3c added 2 commits May 22, 2026 18:20
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
@woai3c woai3c changed the title feat(core/cli): add skill support system feat: add skill support system May 22, 2026
woai3c added 3 commits May 23, 2026 12:17
…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.
@woai3c woai3c merged commit f605eb9 into main May 23, 2026
5 checks passed
@woai3c woai3c deleted the feat/skill-support branch May 23, 2026 08:07
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