aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJosh Rahm <rahm@josher.dev>2026-08-11 11:19:51 -0600
committerJosh Rahm <rahm@josher.dev>2026-08-11 11:19:51 -0600
commit19ca36dfe8cc99321fcbde26a9387e3fac74509d (patch)
treec650df1c655df572bfaed31a0291bf84e7a036e3
parentd7829c069b09d2115daa1aa33a729dd9ffee301e (diff)
downloadhobs.nvim-19ca36dfe8cc99321fcbde26a9387e3fac74509d.tar.gz
hobs.nvim-19ca36dfe8cc99321fcbde26a9387e3fac74509d.tar.bz2
hobs.nvim-19ca36dfe8cc99321fcbde26a9387e3fac74509d.zip
[feat] add the opencode tools directory.HEADmain
-rw-r--r--AGENTS.md376
-rwxr-xr-xopencode/scripts/test-hello-tool.sh6
-rw-r--r--opencode/tools/files_of_intrigue.ts16
-rw-r--r--opencode/tools/hello.ts15
-rw-r--r--opencode/tools/live_read.ts27
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
+ },
+})