diff options
| author | Josh Rahm <rahm@josher.dev> | 2026-08-11 11:19:51 -0600 |
|---|---|---|
| committer | Josh Rahm <rahm@josher.dev> | 2026-08-11 11:19:51 -0600 |
| commit | 19ca36dfe8cc99321fcbde26a9387e3fac74509d (patch) | |
| tree | c650df1c655df572bfaed31a0291bf84e7a036e3 | |
| parent | d7829c069b09d2115daa1aa33a729dd9ffee301e (diff) | |
| download | hobs.nvim-19ca36dfe8cc99321fcbde26a9387e3fac74509d.tar.gz hobs.nvim-19ca36dfe8cc99321fcbde26a9387e3fac74509d.tar.bz2 hobs.nvim-19ca36dfe8cc99321fcbde26a9387e3fac74509d.zip | |
| -rw-r--r-- | AGENTS.md | 376 | ||||
| -rwxr-xr-x | opencode/scripts/test-hello-tool.sh | 6 | ||||
| -rw-r--r-- | opencode/tools/files_of_intrigue.ts | 16 | ||||
| -rw-r--r-- | opencode/tools/hello.ts | 15 | ||||
| -rw-r--r-- | opencode/tools/live_read.ts | 27 |
5 files changed, 440 insertions, 0 deletions
diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..c798db1 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,376 @@ +# AGENTS.md + +## Project Overview + +`hobs.nvim` is a Neovim plugin for concurrent AI-assisted programming. + +The core idea is that AI agents should work **alongside** the human rather than forcing a blocking request/response workflow. + +A user can select some code and run: + +```vim +:Hob <prompt> +``` + +The plugin launches an OpenCode agent for that task while the user continues editing normally. + +The long-term goal is to let multiple AI "hobs" work concurrently against Neovim's live buffers, with human edits always taking precedence. + +## Design Philosophy + +The most important rule is: + +> Agents must never require the editor to stop moving. + +Human and AI work should be concurrent. + +The intended priority model is: + +```text +human edits authoritative +agent edits conditional +AI suggestions speculative +``` + +Agent behavior should be designed around optimistic concurrency rather than exclusive ownership of files or buffers. + +Do not introduce workflows that require the user to avoid editing while an agent is running. + +## Current MVP + +The MVP is intentionally simple. + +When the user visually selects code and runs: + +```vim +:Hob <prompt> +``` + +the plugin: + +1. Captures the current filename. +2. Captures the visual selection range. +3. Captures the raw selected text. +4. Creates an extmark associated with the task. +5. Displays agent status near the selected code. +6. Runs: + +```sh +opencode run <prompt> +``` + +7. Instructs OpenCode to return a `sed` script. +8. Applies that script against the current live buffer when the agent completes. + +The `sed` approach is temporary. It exists only to validate the concurrent editing interaction model. + +Do not over-engineer the sed layer. It is expected to be replaced. + +## Future Architecture + +The likely long-term architecture is: + +```text + Neovim + | + live buffer state + | + overridden OpenCode tools + | + OpenCode + | + LLM +``` + +OpenCode's built-in filesystem tools will eventually be overridden so operations against files loaded in Neovim use live buffer contents instead of stale on-disk contents. + +Likely overridden tools include: + +```text +read +edit +write +grep +apply_patch +``` + +Files not loaded into Neovim may continue to use ordinary filesystem operations. + +Dynamic connection state should be passed through environment variables rather than generated OpenCode configuration. + +The plugin may ship its own OpenCode configuration directory and launch OpenCode with: + +```sh +OPENCODE_CONFIG_DIR=<plugin-root>/opencode +``` + +## Concurrency Model + +Never rely on absolute line numbers for delayed edits. + +Line and column information may be supplied to an agent as initial context, but it is not authoritative once the user continues editing. + +Prefer content-addressed edits. + +A future edit operation should work conceptually like: + +```text +read current text + | + v +identify expected old text + | + v +attempt replacement + | + +-- still matches --> apply + | + +-- changed -------> conflict / reread +``` + +If the human modifies code that an agent previously read, the agent must not silently overwrite the human's changes. + +The human always wins conflicts. + +## Extmarks + +Use Neovim extmarks for persistent task anchors. + +Extmarks are appropriate for: + +* tracking the original task region; +* anchoring agent status UI; +* tracking suggestions; +* following code as lines are inserted or deleted around it. + +Extmarks are primarily a UI/location mechanism. + +Do not assume an extmark alone guarantees that the contents under it are still semantically valid for an agent edit. + +## UI + +Agent status should remain attached to the code that spawned the task. + +A likely UI is a small floating window anchored near an extmark. + +Example: + +```text +parseThing x = ... + ... + +-- Hob ----------------------+ + | Reading related code... | + | Updating parser... | + | Running tests... | + +-----------------------------+ +``` + +The UI must not block normal editing. + +Prefer non-focusable status windows unless explicit interaction is required. + +Avoid intrusive notifications for routine agent progress. + +## Suggestion Mode + +A future mode will periodically inspect code while the user is typing and offer edits. + +Suggestion agents must not modify the buffer directly. + +They should produce proposals such as: + +```text +old text +new text +explanation +``` + +The proposal is only committed when the user explicitly accepts it. + +Suggestions should be conservative. + +It is preferable to produce no suggestion rather than constantly surface low-value cosmetic changes. + +Potential triggers should use debouncing rather than running an agent on every keystroke. + +## OpenCode + +OpenCode is the initial agent runtime. + +Support for both of these may eventually exist: + +```sh +opencode run +``` + +and a persistent OpenCode server. + +The MVP should prefer simple `opencode run` invocation unless a feature clearly requires persistent server state. + +Avoid coupling core plugin abstractions too tightly to OpenCode internals. OpenCode should eventually be one agent backend rather than the definition of the plugin architecture. + +## Lua Style + +Keep the Lua implementation straightforward and idiomatic. + +Prefer: + +```lua +vim.api.* +vim.system(...) +vim.schedule(...) +vim.uv.* +``` + +over shelling out to Neovim commands when a clean Lua API exists. + +Use local variables aggressively. + +Keep modules small and cohesive. + +Avoid global state where possible. Maintain plugin state inside Lua modules. + +Suggested conceptual separation: + +```text +lua/hobs/ + init.lua + task.lua + ui.lua + opencode.lua + buffer.lua +``` + +Do not split files prematurely if the implementation is still small. + +## Async Behavior + +Agent execution must be asynchronous. + +Never block Neovim while waiting for OpenCode. + +Use callbacks or scheduled handlers for process output. + +Remember that process callbacks may execute in contexts where direct Neovim API calls are unsafe. Use: + +```lua +vim.schedule(...) +``` + +when necessary before touching editor state. + +Each running Hob should have its own task identity and state. + +Eventually multiple Hobs must be able to run simultaneously. + +Avoid global "current agent" assumptions. + +## Buffer Safety + +Before modifying a buffer: + +* verify it still exists; +* verify it is modifiable; +* verify the task still refers to the intended buffer; +* avoid replacing human edits based on stale assumptions. + +The MVP's generated `sed` script should be validated before replacing the buffer. + +Do not allow failed external commands to destroy or blank the user's current buffer. + +## Error Handling + +Errors should be visible but unobtrusive. + +Examples: + +* OpenCode executable not found; +* agent process exits non-zero; +* malformed sed output; +* sed application fails; +* source buffer was deleted; +* task was cancelled. + +Include enough stderr/output to diagnose failures. + +Do not silently ignore errors that may leave the user thinking an edit succeeded. + +## Commands + +The primary user-facing command is: + +```vim +:Hob <prompt> +``` + +When invoked from a visual selection, the selected source is part of the task context. + +Future commands may include things such as: + +```vim +:Hobs +:HobStop +:HobSuggest +``` + +Do not add commands merely for internal plumbing. + +## Naming + +A "Hob" is one AI worker. + +The project is: + +```text +hobs.nvim +``` + +Use "Hob" in user-facing terminology where natural. + +Avoid generic branding such as "AI Agent Manager" when the Hob terminology communicates the concept more clearly. + +## Testing + +Where practical, keep core functionality separable from UI so it can be tested headlessly. + +Useful test targets include: + +* visual selection extraction; +* prompt construction; +* task lifecycle state; +* extmark movement; +* stale-task handling; +* subprocess success/failure; +* generated edit validation; +* multiple concurrent tasks. + +For integration testing, prefer headless Neovim where possible: + +```sh +nvim --headless ... +``` + +Do not require a real LLM invocation for every test. + +Agent execution should eventually be abstracted enough that tests can substitute a fake process/backend. + +## Scope Discipline + +The MVP exists to prove one thing: + +> Can a human keep editing normally while an AI agent independently works against the same live source state? + +Prioritize proving that interaction. + +Do not spend substantial effort yet on: + +* complex configuration systems; +* elaborate status dashboards; +* multiple model providers; +* persistent chat history; +* generalized agent orchestration; +* elaborate diff UIs; +* plugin-manager-specific behavior. + +Build the smallest system that validates concurrent human/agent editing, then evolve from evidence. + diff --git a/opencode/scripts/test-hello-tool.sh b/opencode/scripts/test-hello-tool.sh new file mode 100755 index 0000000..d51c621 --- /dev/null +++ b/opencode/scripts/test-hello-tool.sh @@ -0,0 +1,6 @@ +#!/bin/bash +export OPENCODE_CONFIG_DIR="/home/rahm/Projects/hobs.nvim/opencode" + +opencode run 'You are testing a custom OpenCode tool. Use the live_read tool to retrieve and print the contents of the current neovim buffer.' +# opencode run 'You are testing a custom OpenCode tool. Look for interesting files in the repo and call the files_of_intrigue tool to document them.' +# opencode run 'You are testing a custom OpenCode tool. You MUST call the hello tool exactly once with the name "Josh". After the tool returns, reply with exactly the tool result and nothing else.' diff --git a/opencode/tools/files_of_intrigue.ts b/opencode/tools/files_of_intrigue.ts new file mode 100644 index 0000000..43bae61 --- /dev/null +++ b/opencode/tools/files_of_intrigue.ts @@ -0,0 +1,16 @@ +import { tool } from "@opencode-ai/plugin" + +export default tool({ + description: "Use this tool when you found an interesting file", + + args: { + filepath: tool.schema.string().describe("The file path of an interesting file."), + description: tool.schema.string().describe("Why is this file intriguing"), + }, + + async execute(args) { + const message = `FILE OF INTRIGUE ${Date.now()}: ${args.filepath}, because ${args.description}!` + console.error(`[hobs.nvim] intrigue files =${JSON.stringify(args.filepath)}`) + return message + }, +}) diff --git a/opencode/tools/hello.ts b/opencode/tools/hello.ts new file mode 100644 index 0000000..0b49316 --- /dev/null +++ b/opencode/tools/hello.ts @@ -0,0 +1,15 @@ +import { tool } from "@opencode-ai/plugin" + +export default tool({ + description: "Say hello to a person. Use this tool when explicitly asked to call the hello tool.", + + args: { + name: tool.schema.string().describe("The name of the person to greet"), + }, + + async execute(args) { + const message = `HELLO_TOOL_CALLED ${Date.now()}: Hello, ${args.name}!` + console.error(`[hobs.nvim] hello tool called with name=${JSON.stringify(args.name)}`) + return message + }, +}) diff --git a/opencode/tools/live_read.ts b/opencode/tools/live_read.ts new file mode 100644 index 0000000..2174015 --- /dev/null +++ b/opencode/tools/live_read.ts @@ -0,0 +1,27 @@ +import { tool } from "@opencode-ai/plugin" + +export default tool({ + description: "Read the current live Neovim buffer. Use this instead of read when you need the current unsaved contents of the user's active buffer.", + + args: {}, + + async execute() { + const nvim = process.env.NVIM + + if (!nvim) { + throw new Error("$NVIM is not set; live_read must be run from a Neovim-spawned OpenCode process") + } + + const expr = `join(getline(1, "$"), "\n")` + console.error(`[hobs.nvim] live_read tool called =${expr} =${nvim}`) + + // const result = await Bun.$`echo "hello, there"`.text() + const result = + await Bun.$`nvim --headless --server ${nvim} --remote-expr ${expr}`.text() + + console.error(`live tool returned ${result}`) + console.error(`done`) + + return result + }, +}) |