In the previous chapter, UI & Window Management, we built the dashboard that lets us talk to the plugin. Before that, in Global State & Entry Point, we built the brain to remember our settings.
Now, we need to give our AI a Personality.
By default, Large Language Models (LLMs) are generalists. They know a little bit about everything. But when you are coding, you don't want a generalist; you want a specialist.
In 99, we don't hard-code these personalities. Instead, we use Skills (also called Agents or Rules).
Think of the AI as a talented actor. A Skill is a "Job Card" or a "Script" you hand to the actor.
#backend card $\rightarrow$ They act like a Go/Java expert.#vim card $\rightarrow$ They act like a Neovim plugin developer.This chapter explains how 99 reads these cards from your hard drive and injects them into your conversation.
SKILL.md)A skill is just a Markdown file. It lives in a specific folder structure. The text inside contains instructions (System Prompts) that tell the AI how to behave.
#)
We use the hash symbol (#) to reference these skills. If you have a skill named "test", typing #test in the input window tells the plugin: "Go find the instructions for 'test' and include them in this request."
This is the code that scans your folders, creates a list of available skills, and swaps #tags for actual text content.
Let's say we want to create a personality that is really good at writing Lua code for Neovim.
1. Create the File
You create a file at custom_rules/vim/SKILL.md.
<!-- custom_rules/vim/SKILL.md -->
You are a Neovim Lua expert.
1. Always use `vim.api` functions, not standard Lua functions.
2. Be concise.
3. Do not explain standard library code.
2. Configure the Plugin We tell 99 where to look for these rules in our setup function.
require("99").setup({
completion = {
-- Look in this folder for skills
custom_rules = { "/home/user/.config/nvim/custom_rules" }
}
})
3. Use it in the UI When you open the input window (from Chapter 2), you simply type:
"Refactor this function #vim"
What happens:
The system sees #vim, reads custom_rules/vim/SKILL.md, and sends those instructions secretly to the AI. The AI now acts like the expert we defined.
How does the plugin turn #vim into text? Let's look at the lifecycle.
First, we need to find all the SKILL.md files. We use a helper to list files in the directories provided by the user.
-- lua/99/extensions/agents/init.lua
function M.rules(_99)
local custom = {}
-- Loop through user-defined paths
for _, path in ipairs(_99.completion.custom_rules or {}) do
-- 'ls' is a helper that finds SKILL.md files in subfolders
local custom_rules = helpers.ls(path)
-- Add them to our list
for _, r in ipairs(custom_rules) do
table.insert(custom, r)
end
end
-- ... code to index by name ...
end
Explanation: We iterate through the folders configured in the global state. We find every valid skill file and store it in a list called custom.
When the user types a prompt, we need to scan their text for words starting with #.
-- lua/99/extensions/agents/init.lua
function M.by_name(rules, prompt)
local names = {} -- List of skill names found
local out_rules = {} -- List of rule objects found
-- Split prompt by spaces and check every word
for word in prompt:gmatch("%S+") do
if word:sub(1, 1) == "#" then
local w = word:sub(2) -- Remove the '#'
-- Check if we have a rule with this name
if rules.by_name[w] then
table.insert(names, w)
-- Add the actual rule object to our list
-- (Logic simplified for readability)
end
end
end
return { names = names, rules = out_rules }
end
Explanation: If the user types #vim, we strip the #, getting vim. We look inside rules.by_name["vim"]. If it exists, we know the user wants to activate that agent.
Once we know the user wants #vim, we need to read the file content to send to the AI. We wrap the content in XML-style tags so the AI understands where the rules start and end.
-- lua/99/extensions/agents/init.lua
function M.get_rule_content(rule)
-- Open the file in read mode
local file = io.open(rule.path, "r")
if not file then return nil end
-- Read the whole file (*a = all)
local content = file:read("*a")
file:close()
-- Wrap in tags: <vim> ... instructions ... </vim>
return string.format(
"<%s>\n%s\n</%s>",
rule.name, content, rule.name
)
end
Explanation: This function physically opens the file on your hard drive, reads the text, and wraps it. If SKILL.md contained "Be cool", this returns <vim>Be cool</vim>.
To make this user-friendly, we want a popup menu to appear when the user types #. This is handled by the completion_provider.
-- lua/99/extensions/agents/init.lua
function M.completion_provider(_99)
return {
trigger = "#", -- Activate when user types this
get_items = function()
local items = {}
-- Convert our loaded rules into menu items
for _, rule in ipairs(_99.rules.custom) do
table.insert(items, {
label = rule.name, -- Show "vim"
insertText = "#" .. rule.name, -- Type "#vim"
detail = "Rule: " .. rule.path,
})
end
return items
end
}
end
Explanation: This allows the UI (which we will connect in Chapter 8) to show a dropdown list of all available skills immediately after the user types #.
We have built the Personality System for 99.
SKILL.md files in folders.#skillname.Now our AI has a Brain (State), a Body (UI), and a Personality (Agents). But it doesn't actually do anything yet. It needs to perform tasks.
Next Chapter: Operations (Ops)
Generated by Code IQ