How 99's #rules Autocomplete System Works Internally: A Deep Dive into the Completion Framework
The #rules autocomplete system in 99 uses a provider-based completion framework that triggers on the # character, discovers custom rules from SKILL.md files, and injects rule content into LLM prompts via pattern matching and resolution callbacks.
The #rules autocomplete system is a core feature of ThePrimeagen's 99 plugin that streamlines prompt engineering by allowing users to reference external rule files using the # trigger. This system is built on a modular completion framework located in lua/99/extensions/completions.lua that manages provider registration, trigger detection, and content resolution.
The Completion Provider Architecture
The autocomplete functionality is implemented as a completion provider within the agents extension. Each provider declares a trigger character, item generation logic, validation, and resolution capabilities.
Trigger Characters and Provider Structure
In lua/99/extensions/agents/init.lua, the completion_provider function returns a structured table that defines how the # trigger behaves:
function M.completion_provider(_99)
return {
trigger = "#",
name = "rules",
get_items = function() … end,
is_valid = function(token) return M.is_rule(_99.rules, token) end,
resolve = function(token) … end,
}
end
The trigger field specifies that typing # activates this provider. The get_items callback generates the completion list, while is_valid checks if a token corresponds to an actual rule, and resolve fetches the rule's full content.
Registration with the Completion Manager
The provider registers itself with the central completion system in lua/99/extensions/completions.lua:
Completions.register(Agents.completion_provider(_99))
This inserts the provider into the internal providers array, making it available for trigger detection and item generation throughout the editing session.
Rule Discovery and Indexing
Before autocomplete can suggest rules, the system must discover and index available rule files from the filesystem.
Scanning Custom Rule Directories
Rule discovery happens at startup via lua/99/init.lua. The system creates a global state that includes state.completion.custom_rules, which contains paths to user-defined rule directories.
The Agents.rules function in lua/99/extensions/agents/init.lua iterates over these paths:
self.rules = Agents.rules(self)
For each path in custom_rules, the system calls helpers.ls to scan for directories containing rule definitions.
Parsing SKILL.md Files
The helpers.ls function in lua/99/extensions/agents/helpers.lua handles the actual filesystem traversal. It expands paths and looks for SKILL.md files or directories containing them:
{
name = "my_rule",
path = "rules/my_rule",
absolute_path = "/abs/.../SKILL.md"
}
Each discovered rule becomes an object containing its directory name, relative path, and absolute path to the skill definition.
Building the Rule Index
The discovered rules are stored in state.rules.custom as a list. Additionally, a name-to-rule mapping (by_name) is constructed for O(1) lookups during validation and resolution phases.
Generating LSP-Style Completion Items
When the user types #, the completion provider generates LSP-compatible completion items that Neovim can display in the popup menu.
Constructing Completion Objects
The get_items callback in the rules provider iterates over the indexed rules and builds LSP completion items:
{
label = rule.name,
insertText = "#" .. rule.path,
filterText = "#" .. rule.name,
kind = 12,
documentation= { kind = "markdown", value = docs },
detail = "Rule: " .. rule.path,
}
The label displays the rule name, while insertText determines what gets inserted into the buffer (including the # prefix). The filterText enables searching by name, and kind categorizes the item as a value type (12).
Preview Extraction with helpers.head
To provide helpful context in the completion popup, the system uses helpers.head from lua/99/extensions/agents/helpers.lua. This function reads the first few lines of the SKILL.md file and includes them in the documentation field as markdown content.
Resolving #rules Tokens in Prompts
The autocomplete system doesn't just insert text—it ensures that when a prompt is sent to the LLM, the rule content is properly injected.
Pattern Matching and Token Extraction
When processing a prompt, Completions.parse in lua/99/extensions/completions.lua scans the text for registered trigger patterns:
local pattern = provider.trigger:gsub("([%%%^%$%(%)%.%[%]%*%+%-%?])","%%%1") .. "%S+"
for word in prompt_text:gmatch(pattern) do
local token = word:sub(#provider.trigger + 1)
if provider.is_valid(token) then
local content = provider.resolve(token)
if content then table.insert(refs, {content = content}) end
end
end
The pattern escapes special regex characters from the trigger (here #) and matches any non-whitespace sequence following it. The extracted token (the text after #) is validated against the rule index.
Content Injection and Tag Wrapping
For valid tokens, the resolver fetches the rule content:
local rule = M.get_rule_by_path(_99.rules, token)
return M.get_rule_content(rule)
The get_rule_content function reads the SKILL.md file and wraps it in XML-style tags:
"<" .. rule.name .. ">\n" .. file_content .. "\n</" .. rule.name .. ">"
This structured format helps the LLM identify and utilize the rule context within the prompt.
End-to-End Workflow
Understanding the complete flow clarifies how the components interact:
-
Startup:
lua/99/init.lualoads custom rule paths and callsAgents.rules(), which scans directories viahelpers.ls()and stores results instate.rules.custom. -
Registration: The agents extension registers its provider with
Completions.register(), enabling the#trigger. -
Completion: When the user types
#, Neovim queriesprovider.get_items(), which builds LSP items from the indexed rules using metadata extracted byhelpers.head(). -
Validation: Upon prompt submission,
Completions.parse()extracts tokens using regex pattern matching, validates them viaprovider.is_valid(), and resolves content throughprovider.resolve(). -
Injection: The resolved content, wrapped in XML-style tags, is inserted into the prompt sent to the LLM.
Practical Implementation Examples
Adding a custom rule directory:
require("99").setup({
completion = {
custom_rules = { "~/.config/nvim/99/rules" },
},
})
Triggering completion in a buffer:
- Type
#in any buffer. - Select a rule from the popup (e.g.,
#git_commit). - The buffer now contains the token
#git_commit.
Programmatically resolving a rule:
local content = require("99.extensions.agents").completion_provider(state)
.resolve("git_commit")
-- Returns: "<git_commit>\n<file contents>\n</git_commit>"
Using the parsed references programmatically:
local prompt = "Please follow #git_commit guidelines."
local refs = require("99.extensions.completions").parse(prompt)
for _, ref in ipairs(refs) do
print(ref.content) -- Full rule content with XML tags
end
Key Files and Their Responsibilities
| File | Role |
|---|---|
lua/99/extensions/completions.lua |
Core completion manager; registers providers, builds trigger patterns, parses prompts. |
lua/99/extensions/agents/init.lua |
Implements the # provider: discovers rules, builds LSP items, validates and resolves tokens. |
lua/99/extensions/agents/helpers.lua |
Helpers for path normalization, rule discovery (ls), and preview extraction (head). |
lua/99/init.lua |
Bootstrap: creates global state, loads custom rules, registers the agents provider. |
lua/99/test/completions_spec.lua |
Test suite confirming that # completions are registered and parsed correctly. |
Summary
- The
#rulesautocomplete system uses a provider-based architecture centered inlua/99/extensions/completions.lua. - Rule discovery happens at startup via
Agents.rules(), which scanscustom_rulespaths forSKILL.mdfiles usinghelpers.ls(). - The completion provider registers the
#trigger and generates LSP-compatible completion items with metadata extracted byhelpers.head(). - When prompts are processed,
Completions.parse()uses regex pattern matching to extract tokens, validates them against the rule index, and resolves content by readingSKILL.mdfiles and wrapping them in XML-style tags. - This separation of concerns between discovery, presentation, and resolution makes the system modular and extensible.
Frequently Asked Questions
How does 99 discover custom rules for the # autocomplete?
During initialization in lua/99/init.lua, the system reads paths from state.completion.custom_rules and invokes Agents.rules(). This function iterates over each path and calls helpers.ls() to scan for directories containing SKILL.md files. Each valid discovery is stored as a rule object in state.rules.custom with name, path, and absolute_path fields.
What file format does 99 expect for rule definitions?
The system expects rules to be defined in files named SKILL.md located within directories. When a rule is resolved, the provider reads the entire contents of this file and wraps it in XML-style tags using the rule's directory name (e.g., <rule_name>\n...content...\n</rule_name>) before injecting it into the LLM prompt.
How does the completion provider integrate with Neovim's LSP?
The provider generates LSP-compatible completion items in the get_items() callback, setting standard fields such as label (rule name), insertText (the # prefixed path), filterText (searchable text), kind (12 for Value), and documentation (markdown preview). Neovim's built-in completion engine consumes these items directly, displaying them when the user types the # trigger character.
Can I use the #rules system programmatically outside of autocomplete?
Yes. You can manually resolve rules or parse prompts using the extension's API. For example, call require("99.extensions.agents").completion_provider(state).resolve("rule_name") to fetch the wrapped content directly, or use require("99.extensions.completions").parse(prompt_text) to extract and resolve all # tokens from a string programmatically.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →