Agent micro-prompting and cowork framework that keeps agent output at arms length while still allowing for easy integration of suggestions.
Overview
Code context, templated prompts and a coding agent, inside Neovim Hive implements a few core ideas: - Separate suggestions from the code to reduce visual clutter in the IDE, but provide efficient tools for accepting suggestions, even partial suggestions. - Control a task-oriented array of smaller agentic models and access patterns to them, hence the hive name, each insect does a single, specialised job, in parallel to others. - Improve control and generation accuracy by allowing the user to fine tune the context sent to the models, while using enough generated context to give the model a good chance to produce the specific output the user wants (emphasising shape from automatically sourced code, direction from user input). - Use local resources efficiently by giving the developer the tools and knowledge to produce good llm outputs, deferring to cloud solutions only when the task is appropriately large or general.
README
💡 Mission statement
Hive implements a few core ideas:
- Separate suggestions from the code to reduce visual clutter in the IDE, but provide efficient tools for accepting suggestions, even partial suggestions.
- Control a task-oriented array of smaller agentic models and access patterns to them, hence the hive name, each insect does a single, specialised job, in parallel to others.
- Improve control and generation accuracy by allowing the user to fine tune the context sent to the models, while using enough generated context to give the model a good chance to produce the specific output the user wants (emphasising shape from automatically sourced code, direction from user input).
- Use local resources efficiently by giving the developer the tools and knowledge to produce good llm outputs, deferring to cloud solutions only when the task is appropriately large or general.
The aim of these core ideas is to provide a user with more control over code style than pure vibe coding. Allowing the user to inject their real world context into the code shape itself, providing effective abstractions instead of the common brute force and re-implementation methods typified by generated code.
🗺️ Status
Hive has three layers, each with its own command.
| Command | Layer | Modules | State |
|---|---|---|---|
:Hive |
Agent: a Neovim frontend for pi-coding-agent | hive.agent, hive.fs (planned) |
Planned, see PLAN.md |
:HiveBare |
Direct: templated prompts sent straight to an endpoint | hive.direct.* |
Fill-in-the-middle works |
:HiveContext |
Context: select, gather and copy code | hive.context.*, hive.workbench |
Works |
Agent (planned)
Neovim starts a Node service that wraps @earendil-works/pi-coding-agent. The
service owns the agent loop. Neovim is the filesystem: the agent’s read, write
and edit tools use buffers and never touch disk. An edit to an open file changes
the buffer and leaves the file on disk as it was. A write to a new file opens a
listed, modified, unsaved buffer. bash, grep, find and ls still read the
filesystem.
Lua only sends notifications to Node, and Node drives Neovim through the API. Lua never waits on Node, so a tool that reads a buffer cannot deadlock the editor.
The API key stays in the secret store that :HiveBare uses. Lua resolves it and
gives it to Node in the child’s environment, never on the command line and never in
Pi’s own credential file.
The build runs in stages: the RPC channel, a session that streams events into a
buffer, a layer that rebuilds exact file bytes from buffer lines (line endings,
BOM, trailing newline), the buffer-backed tools, open on write, then diff
rendering and approval. One decision is open: when agent-touched buffers get saved,
because bash and test runs read stale disk until they are.
PLAN.md has the full design.
Templated prompts
A templated prompt is a fixed instruction, plus the context that hive.context
gathers for it, sent as one request by hive.direct.api. The editor picks the
context from the code (shape), and you give the direction.
| Template | Context | State |
|---|---|---|
| Fill in the middle | The enclosing function, split at the cursor | :HiveBare fim, POST /v1/fim/completions |
| Summarise this diff | git diff of the file against a revision |
Context works (:HiveContext diff), template planned |
| Write a unit test for this function | The function, the file outline, and the definitions and usages of its symbols | Context works (:HiveContext outline, :HiveContext symbol), template planned |
The server builds the FIM sentinels from prompt and suffix, so Hive sends
the two sides as plain text and never writes a model’s special tokens.
:HiveBare fim appends the filled function to the project’s workbench, a Markdown
scratch file under stdpath("cache")/workbenches. The code in the buffer does not
change, so you can compare the suggestion and take all or part of it.
Context
:HiveContext finds code with treesitter and LSP. Selection uses node types for
each language (Python, C, Rust, Lua and TypeScript), and a shared fallback list
for other languages.
| Subcommand | Does |
|---|---|
select call|method|class|parent |
Select the enclosing node. With a range, select the nearest one that extends past it. |
ref |
Copy :- for a range, or [:-] for the element at the cursor |
rel |
Copy the buffer’s path relative to the working directory |
file |
Copy the buffer’s absolute path |
outline [reg] |
Copy a tree of signatures for the buffer, or for the range |
symbol [reg] |
Copy the definitions and references of the symbol at the cursor, from LSP |
diff [rev=] [reg] |
Copy the git diff of the buffer’s file against rev (default HEAD) |
fim |
Append the enclosing function, split at the cursor with FIM markers, to the workbench |
workbench open|new|next|previous |
Open this project’s workbench, start a new one, or step between them |
[reg] defaults to +.
⚡️ Requirements
- Neovim 0.12.2 or later
- curl: every
:HiveBarerequest is acurlsubprocess - A server with Mistral’s
POST /v1/fim/completions, such ashttps://api.mistral.aiwithcodestral-latest - Treesitter parsers for the languages you select in, a language server for
:HiveContext symbol, and git for:HiveContext diff - Node.js, for
:Hivewhen the agent lands
For development, also: StyLua, LuaLS, git and Make. Optionally lazydev.nvim.
📦 Installation
With lazy.nvim:
{
"brichar01/hive.nvim",
cmd = { "HiveBare", "HiveContext" },
opts = {
base_url = "https://api.mistral.ai",
model = "codestral-latest",
api_key_env = "MISTRAL_API_KEY",
},
}
setup() is optional — hive.nvim works with its hard-coded defaults. Check the connection with :checkhealth hive.
🚀 Usage
-- Async: the callback runs via vim.schedule(), so buffers and windows are safe.
require("hive").completions("def add(a, b):\n ", "\n return total", 64, function(err, out)
if err then
return vim.notify(err, vim.log.levels.ERROR)
end
vim.notify(out.text)
end)
-- Blocking: returns (err, completion). An empty suffix puts the cursor at the end.
local err, out = require("hive").completions("2 + 2 =", "", 8)
print(err or out.text)
The completion table carries text, finish_reason, usage and raw (the decoded
response body, verbatim). Exactly one of err and out is set.
From the command line:
:HiveBare complete The capital of France is
:HiveBare fim 128
:HiveBare health
:'HiveContext outline a
:HiveContext diff rev=main
Map keys to the commands in your own config. Use `` in normal mode, and : in
visual mode so the command gets the range:
vim.keymap.set("n", "vsm", "HiveContext select method")
vim.keymap.set("x", "vsm", ":HiveContext select method")
vim.keymap.set("x", "", ":HiveContext select parent")
vim.keymap.set("n", "nf", "HiveBare fim")
Configuration
require("hive").setup({
base_url = "http://localhost:8080",
model = "default",
timeout = 60, -- seconds, whole request
headers = {
["Content-Type"] = "application/json",
["Accept"] = "application/json",
},
})
The agent
:Hive uses Pi’s built-in DeepSeek provider, with its own options in agent. The
options above apply only to :HiveBare. agent.model is a model id from Pi’s
DeepSeek catalogue, and Pi sets the endpoint and token limits for it. The key comes
from agent.api_key, then $DEEPSEEK_API_KEY (renameable via agent.api_key_env).
agent.api_key takes a string or a function, the same as api_key.
agent.thinking is one of off, minimal, low, medium, high, xhigh or
max, and low by default. Pi lowers it to what the model supports, so a model
without reasoning runs at off.
agent.tools lists the active tools. By default these are read, write, edit,
bash, grep, find and ls. read, write and edit use Neovim’s buffers.
read_disk and write_disk are Pi’s own read and write, which use the file on disk
and ignore unsaved changes. They are off by default and reachable only from
codemode scripts, so they never clutter the model’s tool list.
An entry is an exact name, a * pattern (any characters), or a +name/-name
delta. A list of plain names and patterns replaces the selection; a list of only
deltas changes it, keeping the rest:
require("hive").setup({
agent = {
model = "deepseek-v4-pro",
thinking = "low",
api_key_env = "DEEPSEEK_API_KEY",
-- Keep the defaults, add the disk-aware read, and drop write.
tools = { "+read_disk", "-write" },
},
})
Plain names and deltas cannot be mixed, matching Pi’s own --tools. The same
syntax works at run time with :Hive set tools.
The conversation goes into the project’s newest workbench, the same one
:HiveContext workbench open shows. Each workbench has its own session, so
:HiveContext workbench new starts a new conversation. Neovim does not save the
workbench for you.
Each message in the workbench sits between and. The id is the message’s entry in the agent session. The
hint says what the message holds, so a closed fold shows it. For a prompt or a reply
it is the first 20 characters of its text. For a tool it is what the call acted on
and what it found, such as read lua/hive/ui.lua:20-60, grep "set_folds" lua/: 4 matches
or bash npm test (exit 1). The workbench window folds on these tags. Each tool
call is in the same fold as its result. Tool results, and agent messages with only
thinking or a tool call, start closed. To chat in the workbench itself, write below the last hive:end tag and run
:Hive chat. It sends that text as the prompt and appends the reply below it.
To change the model, thinking level or tools until Neovim exits, use :Hive set. A
new model starts a new session, and the conversation history is lost. A new thinking
level or tool list keeps the session. :Hive set tools takes the same exact names,
* patterns and +name/-name deltas as agent.tools. With no value, :Hive set
shows the current one.
:Hive set model deepseek-v4-pro
:Hive set thinking high
:Hive set thinking
:Hive set tools read_disk write_disk edit bash grep find ls
:Hive set tools +read_disk -write
:Hive set tools '*_disk'
Pointing it at another machine
base_url is the only thing that has to change. model almost certainly does
too — every server names its models differently, and :checkhealth hive will
tell you which ones yours actually serves:
require("hive").setup({
base_url = "http://gpubox.lan:8080",
model = "qwen2.5-coder:7b",
-- Optional. The token is passed to curl out of band on curl >= 8.3, so it
-- never appears in the process list where any local process could read it.
api_key_env = "HIVE_API_KEY",
})
The bearer token
api_key is unset by default, so nothing is sent unless you configure it. The
fallback order is api_key first, then $HIVE_API_KEY (renameable via
api_key_env), then no Authorization header at all.
A literal string ends up in your dotfiles. api_key also accepts a function, so
the token can come from a wallet instead — resolve_api_key calls it once and
memoises the answer, which means a blocking lookup costs one IPC round trip per
session rather than one per request:
require("hive").setup({
api_key = function()
if vim.fn.executable("secret-tool") ~= 1 then
return nil
end
-- A locked wallet prompts for a passphrase and nothing here can answer it,
-- so the lookup is killed rather than left waiting.
local out = vim.system(
{ "secret-tool", "lookup", "service", "mistral", "key", "api" },
{ text = true, timeout = 10000 }
):wait()
if out.code ~= 0 then
return nil
end
-- A trailing newline would ride into `Authorization: Bearer` and read as a
-- bad key.
return vim.trim(out.stdout or "")
end,
})
That reads the item created by:
secret-tool store --label='Mistral API key' service mistral key api
Returning nil from the function falls through to $HIVE_API_KEY, so a missing
secret-tool, a locked wallet or an absent item all degrade rather than fail.
Note that Secret Service items are addressed by their attribute pairs, not by a
folder and entry name — a secret already in KWallet under kdewallet /
Passwords / MISTRAL_API_KEY is not reachable this way and has to be stored
again with the command above.
Full documentation lives in :help hive.
🧱 Layout
| Module | Role |
|---|---|
hive |
Public entry point (setup, completions) |
hive.config |
Defaults, validation and API key resolution |
hive.endpoint |
Classifies base_url: loopback, TLS or remote plaintext |
hive.health |
:checkhealth hive |
hive.direct.api |
Endpoint bindings: builds the request, decodes the response |
hive.direct.curl |
Transport: builds a curl argv, runs it with vim.system(), returns { status, body } |
hive.context.selection |
Treesitter node types for each language, and node selection |
hive.context.format |
Splits a node’s text at the cursor |
hive.context.gather |
Outlines, and definitions and usages from LSP |
hive.context.diff |
git diff of a file |
hive.context.path |
Buffer paths and : references |
hive.workbench |
The project’s Markdown workbench files |
hive.direct.curl knows nothing about the endpoints. The request body goes to
curl’s stdin (--data-binary @-) so large prompts never reach the command line.
The HTTP status comes back from --write-out behind a marker, so the body stays
byte-exact.
🧪 Development
make test # mini.test suite via lazy.minit
make lint # StyLua
make typecheck # LuaLS
make check # all three
make dev # nvim -u repro/repro.lua
The test suite runs without a server: transport errors are exercised against a closed port and response handling against synthetic payloads. One test in tests/api_spec.lua hits the configured base_url and skips itself when nothing answers.
🤖 AI Coding Agent
This project ships with Agent Skills in .agents/skills/. Skills follow the Agent Skills specification — the same SKILL.md format works across many agents. Agents discover skills from different directories (e.g. .claude/skills/, .github/skills/); if yours doesn’t pick them up, rename .agents/ to its expected directory.
| Skill | Description |
|---|---|
nvim-init |
Initialize plugin project and verify development environment |
nvim-plugin |
Plugin development best practices and patterns |
nvim-test |
Execute tests and diagnose failures |
nvim-doc |
Write and update vimdoc help documentation |
nvim-commit |
Create conventional commits for release-please |
nvim-help |
Search Neovim’s built-in :help documentation |
🙏 Acknowledgments
- base.nvim: the template this plugin started from
- nvim-best-practices: Collection of DOs and DON’Ts for modern Neovim Lua plugin development
- LuaCATS annotations: type annotations to your Lua code
- mini.test: minimal test framework with child-process isolation
Recommended Tools
Try a different keyword or remove a filter.
Install
npx skillfish add brichar01/hive.nvim