Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
cf90ae1
fix: trigger push CI on master, not nonexistent main
DorianDevp Jul 16, 2026
21d533f
chore: expand gitignore, drop generated doc/tags
DorianDevp Jul 16, 2026
bde371b
docs: move ml.md under doc/
DorianDevp Jul 16, 2026
6a7afd8
chore: assert CHANGELOG entry for the release version
DorianDevp Jul 16, 2026
fb75860
chore: drop unused thiserror, dev-scope async-trait, delete dead acti…
DorianDevp Jul 16, 2026
da07249
refactor: split context crate into index and rank modules
DorianDevp Jul 16, 2026
f83b5be
fix: fail pending RPC callbacks on stop and guard channel sends
DorianDevp Jul 16, 2026
b7296a6
fix: bound installer downloads and extract only the daemon binary
DorianDevp Jul 16, 2026
12f296b
refactor: centralize turn results, state reset, and shared Lua helpers
DorianDevp Jul 16, 2026
8b16f4e
chore: add stylua, selene, and LuaLS configs; format Lua
DorianDevp Jul 16, 2026
589a85f
test: add headless Lua unit tests and real daemon smoke round-trip
DorianDevp Jul 16, 2026
e035c2f
feat: reject mismatched client protocol versions at initialize
DorianDevp Jul 16, 2026
af72f34
test: drive loopbioticd JSON-RPC over stdio with the mock backend
DorianDevp Jul 16, 2026
470313a
refactor: split engine into focused modules, degrade reachable panics…
DorianDevp Jul 16, 2026
fe82b4e
test: cover session state transition tables
DorianDevp Jul 16, 2026
a7ff061
fix: time out wedged backend turns, dedupe backend helpers into support
DorianDevp Jul 16, 2026
484458b
refactor: split codex_app into transport, parse, and schema modules
DorianDevp Jul 16, 2026
bc265d7
fix: bound Ollama HTTP requests with the shared turn deadline
DorianDevp Jul 16, 2026
9f2d6d2
ci: lint Lua, run unit tests and a real daemon round-trip
DorianDevp Jul 16, 2026
904cdd5
docs: changelog for the desloppification round
DorianDevp Jul 16, 2026
ab8f537
fix: clamp cursor targets to the buffer to survive append-at-EOF drafts
DorianDevp Jul 16, 2026
f413634
feat: report backend identity and resolved model at warmup (protocol 9)
DorianDevp Jul 16, 2026
87eec5a
feat: show the resolved model and pick models from the prompt window
DorianDevp Jul 16, 2026
f3cf1e0
docs: changelog for identity handshake and model picker
DorianDevp Jul 16, 2026
3631381
fix: report phase-aware claude identity and offer CLI alias models
DorianDevp Jul 16, 2026
35a9e95
feat: backend preflight, repeated-error escalation, and client error …
DorianDevp Jul 16, 2026
8446b8d
feat: sliced goal turns with continuation speculation
DorianDevp Jul 16, 2026
3ecb50a
docs: describe the sliced goal flow
DorianDevp Jul 16, 2026
99af227
feat: one-keypress slice retry when a queued draft no longer applies
DorianDevp Jul 16, 2026
7745ef6
refactor: stabilize backend prompt cache prefixes
DorianDevp Jul 16, 2026
76e81f0
feat: normalize and classify patch violations
DorianDevp Jul 16, 2026
e80c53f
docs: record cache and contract hardening
DorianDevp Jul 16, 2026
cba8881
test: add real Codex proposal latency gate
DorianDevp Jul 16, 2026
f18acd1
fix: restore patch review controls
DorianDevp Jul 16, 2026
8bdcfa9
feat: make pairing conversational and interruptible
DorianDevp Jul 16, 2026
191bdcc
fix: make draft transitions tab-safe
DorianDevp Jul 16, 2026
477fc2d
fix: prioritize cursor diagnostics
DorianDevp Jul 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: CI

on:
push:
branches: [main]
branches: [master]
pull_request:

permissions:
Expand All @@ -22,6 +22,16 @@ jobs:
- run: cargo clippy --workspace --all-targets -- -D warnings
- run: cargo test --workspace --no-fail-fast

lua-lint:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: taiki-e/install-action@v2
with:
tool: stylua,selene
- run: stylua --check lua/ tests/ scripts/headless-smoke.lua
- run: selene lua/

neovim:
runs-on: ubuntu-24.04
steps:
Expand All @@ -30,9 +40,22 @@ jobs:
with:
version: stable
neovim: true
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Run Lua unit tests
run: nvim --headless -u NONE -i NONE -l tests/lua/run.lua
- name: Run headless smoke test
env:
XDG_STATE_HOME: ${{ runner.temp }}/state
XDG_DATA_HOME: ${{ runner.temp }}/data
XDG_CACHE_HOME: ${{ runner.temp }}/cache
run: nvim --headless -u NONE -i NONE -l scripts/headless-smoke.lua
- name: Build loopbioticd
run: cargo build -p loopbioticd
- name: Run real daemon round-trip
env:
XDG_STATE_HOME: ${{ runner.temp }}/state
XDG_DATA_HOME: ${{ runner.temp }}/data
XDG_CACHE_HOME: ${{ runner.temp }}/cache
LOOPBIOTIC_SMOKE_BIN: target/debug/loopbioticd
run: nvim --headless -u NONE -i NONE -l scripts/headless-smoke.lua
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
/target
/doc/tags
loopbioticd-v*.tar.gz
loopbioticd-v*.tar.gz.sha256
.luarc.local.json
13 changes: 13 additions & 0 deletions .luarc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"$schema": "https://raw.githubusercontent.com/LuaLS/vscode-lua/master/setting/schema.json",
"runtime": {
"version": "LuaJIT"
},
"workspace": {
"library": ["$VIMRUNTIME/lua", "${3rd}/luv/library"],
"checkThirdParty": false
},
"diagnostics": {
"globals": ["vim"]
}
}
6 changes: 6 additions & 0 deletions .stylua.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
column_width = 120
line_endings = "Unix"
indent_type = "Spaces"
indent_width = 2
quote_style = "AutoPreferDouble"
call_parentheses = "Always"
102 changes: 102 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,108 @@ The project follows [Semantic Versioning](https://semver.org/).

## [Unreleased]

### Fixed

- Navigating to a card or draft no longer throws "Cursor position outside
buffer" when the target line does not exist yet — for example a patch hunk
that appends to the end of a short file, or an agent-supplied location past
the end of the buffer. Cursor targets are now clamped to the real buffer.
- Backend turns now run under a configurable deadline
(`LOOPBIOTIC_TURN_TIMEOUT_SECS`, default 600, `0` disables). A wedged agent
CLI is killed and respawned on the next turn instead of hanging the session
forever; the Ollama HTTP client uses the same deadline.
- Stopping or switching the backend now fails in-flight requests instead of
dropping their callbacks, so the thinking spinner can no longer get stuck.
- RPC sends to an already-exited backend are dropped and logged instead of
throwing inside scheduled callbacks.
- Managed `loopbioticd` downloads now have connect and transfer time limits,
and only the expected binary is extracted from the release archive.
- Retry exhaustion and goal-batch shape mismatches in the session harness
degrade to error cards instead of panicking the daemon.
- Goal hunks that no longer apply after the buffer changes now offer a
one-keypress retry that regenerates only the stale local step.
- Rejecting a draft now stops locally with explicit Retry/Edit/Stop actions
instead of immediately spending another model turn on a replacement.
- Resuming an already-visible action card now moves focus into its window,
and patch action cards register every configured shortcut they display
(accept, reject, retry, why, and go-to) in addition to their single-key aliases.
- Working cards tolerate protocol `null` metadata instead of failing while
rendering their token footer. Sending a message while a draft is open now
restores the source and abandons the preview before conversation starts;
tab navigation never starts from a float, and background-tab action floats
are closed only after their tab becomes current, avoiding a Neovim tabline
use-after-free.
- Mechanical model diff wrappers are normalized before validation: CRLF line
endings, markdown fences, matching git headers, and unambiguous `./`, `a/`,
or `b/` path prefixes. Prose, rename/copy metadata, unmatched fences, and
headers naming another file are rejected instead of being silently dropped.
- Cursor-local editor errors remain explicit model context even when their
source line is already in the primary excerpt. Codex receives the diagnostic
text on discovery and patch turns, so a distant warning or deprecation no
longer displaces the error beside the cursor.
- Push CI runs again: the workflow triggered on a nonexistent `main` branch.

### Changed

- Auto sessions are conversational-first. Their first response and normal
replies cannot return patches or completion summaries; persistent goal
execution starts only from the explicit `Goal` action. Sending a message
pauses an active goal and answers conversationally.
- Goal work is limited to one file, one coherent hunk, and 32 changed lines
per turn. Only explicit goals may speculate on the next patch; ordinary
speculation is read-only post-accept conversation.
- `initialize` validates the client protocol version when one is supplied and
returns a structured error (`-32001`) on mismatch instead of failing later
with cryptic errors.
- The project context scan no longer blocks async worker threads.
- Backend prompts put static contracts and append-only session history before
volatile action and editor context data, preserving the longest reusable
provider-cache prefix across turns and sliced goal continuations.
- Internal restructuring: `engine.rs`, `codex_app.rs`, and the context crate
are split into focused modules; duplicated backend and Lua helpers are
shared; Lua state reset is defined next to the state it resets.

### Added

- Conversation turns have a 10-second visible-response budget and work turns
a 20-second budget. Slow turns yield a focusable `Working` card, continue in
the background, and can be interrupted through the real Codex
`turn/interrupt` API or by terminating persistent CLI processes. Completion
arrives through `agent/turn_ready`; slow-turn timing is logged locally and
injected once as compact feedback on the next model turn.
- Accepting a non-goal patch automatically surfaces a read-only conversational
next card, with no intermediate “local step applied” summary. That card is
prefetched during review by default; rejecting remains a local decision and
never regenerates code.
- Backend preflight in the prompt window (failures surface before typing;
composed prompts survive failed starts), repeated-error escalation with
actionable guidance, and a client-side error boundary that preserves the
session when a UI callback fails.
- The `backend/warmup` handshake now reports an explicit identity: the active
backend, the concrete model the next turn will use (configured, or resolved
from the backend — the Claude CLI announces it at process start, Ollama
always knows it), and the models the backend can enumerate (Ollama's local
tags). Protocol version is now 10.
- The prompt window title names the active agent and resolved model (never
"default"), refreshing as soon as warmup resolves it, and `Ctrl-l`
(`keymaps.models`) opens a model picker fed by the backend-enumerated
models, an optional per-agent `models` list, and the last reported model.
Selections persist per agent exactly like `:LoopbioticModel`.
- Identity is phase-aware: the reported model is always the patch-drafting
one, and a differing discovery model is shown separately instead of
masquerading as the active model. The shipped Codex agent uses
`gpt-5.4-mini` at low effort for conversation; Claude uses
`discovery_model = "haiku"`. The claude picker offers the CLI aliases
`sonnet`, `opus`, and `haiku` since the CLI has no model-listing API.
- Lua tooling (`stylua`, `selene`, LuaLS config) enforced in CI, headless Lua
unit tests for the patch engine and session state, `loopbioticd` JSON-RPC
integration tests, session state-machine transition tests, and a real
daemon round-trip smoke test.
- Agent-attempt telemetry includes a closed `violation_class` for contract
retries and rejected cards, allowing context mismatches, malformed diffs,
wrong files, missing fields, kind mismatches, duplicate steps, and
incoherent goal batches to be aggregated without logging patch content.

## [0.3.2] - 2026-07-14

### Added
Expand Down
22 changes: 0 additions & 22 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,5 @@ reqwest = { version = "0.12", default-features = false, features = ["json"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tempfile = "3"
thiserror = "2"
tokio = { version = "1", features = ["io-util", "macros", "process", "rt-multi-thread", "sync", "time"] }
uuid = { version = "1", features = ["v4"] }
102 changes: 67 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Implemented capabilities include:
- raw, cached, and non-cached session token usage plus a local error log
- JSON-RPC over stdio
- Rust session harness
- continuous goal state machine with local hunk-by-hunk review
- conversational-first state machine with explicit goals and local hunk review
- patch gate
- mock backend
- generic CLI backend
Expand Down Expand Up @@ -159,22 +159,51 @@ restores it on the next Neovim start. A model explicitly configured in
`setup()` always takes precedence. `:LoopbioticModel default` clears the stored model
and returns that agent to its own default.

The prompt window title always names the active agent and the concrete model
the next turn will use, e.g. `codex / gpt-5.4-mini`. Without a configured
model it shows the model the backend announces during warmup (or reported
after the last turn), and `model?` until one is known — it never shows
`default`. The title always names the patch-drafting model; when an agent
runs discovery on a different model (the shipped Codex agent uses
`gpt-5.4-mini` at low effort; Claude pins `discovery_model = "haiku"`), that
is shown separately, e.g.
`claude-fable-5 · discovery haiku`. Press `<C-l>` (`keymaps.models`) inside
the prompt to pick a model from every known candidate: the configured model,
the models the backend enumerates (Ollama's local tags; claude offers its
stable CLI aliases `sonnet`, `opus`, `haiku`), an optional `models` list on
the agent definition, and the model reported by the last turn. Picking sets
the patch model; `discovery_model` stays as configured. The picked model
persists per agent exactly like `:LoopbioticModel`; the prompt window and its
typed text stay open.

```lua
require("loopbiotic").setup({
agents = {
["local"] = {
kind = "ollama",
host = "http://127.0.0.1:11434",
models = { "qwen2.5-coder:7b", "llama3.1:8b" }, -- extra picker candidates
},
},
keymaps = {
models = "<C-l>", -- model picker inside the prompt window
},
})
```

## Flow

```text
<leader>a
Prompt
Persistent agent goal
Agent inspects the project and prepares the complete multi-file change
One local editable hunk
Edit the inline draft
Accept, Reject, edit, message, or ask Why
Why → explanation → Back to the same pending draft
Accept → advance to the next queued hunk without calling the agent
Reject → agent automatically reworks the hunk
Next file opens automatically inside the workspace without calling the agent
Next editable hunk
Repeat until completed-goal summary and local diagnostics check
Conversational answer, finding, or question — never an implicit patch
Follow up or reply back-to-back
Draft → one local editable hunk
Accept → automatic read-only next card, without an intermediate summary
Reject → stop locally; Retry only when explicitly requested
Goal → explicitly authorize a sequence of small reviewed hunks
While a goal hunk is reviewed, only its next small hunk may be prepared
Repeat until one final goal summary and local diagnostics check
```

Cards stay anchored clear of the source line and do not take focus. Use `<leader>pg`
Expand All @@ -185,13 +214,19 @@ and draft explanations stay compact by default; press `z` while the card is
focused to expand or collapse their full text (`keymaps.details` changes this
key).

For a goal patch, Loopbiotic validates every returned file against its live editor
buffer, queues the complete batch, and opens each location only when its hunk is
ready for review. Navigation and acceptance are local operations and do not
start another model turn. Automatic navigation is restricted to the current
workspace; edits are still inert drafts until the user accepts each hunk. If
the agent could not inspect a required file, `open_location` remains a fallback
that supplies its buffer in a subsequent turn.
Conversational turns have a 10-second interaction deadline and work turns have
a 20-second deadline. Crossing it yields a focusable `Working` card instead of
holding Neovim; the agent continues in the background, and `Cancel` interrupts
the real backend turn. Slow-turn timing is recorded in the local trace and a
compact, content-free instruction is injected into the next turn so the agent
prioritizes an earlier useful response.

For an explicit goal, each backend turn may return at most one file, one
coherent hunk, and 32 changed lines, plus a plan of the remaining coherent
steps. The next hunk may be prepared while the current one is reviewed.
Accepting normally surfaces it immediately; rejecting cancels it and never
generates a replacement without `Retry`. Automatic navigation stays inside the
workspace, and every edit remains an inert draft until accepted.

Loopbiotic moves directly to the evidence for a location-bearing card and to the
first non-blank character of the first added line for a draft, including drafts
Expand All @@ -207,27 +242,22 @@ with `/{kind}` to demand a specific card instead — `/hypothesis`, `/finding`,
Unknown words after `/` are treated as normal prompt text, so paths like
`/tmp/project` are safe.

The goal and accepted-step count stay visible on cards and editable drafts. In
the default `auto` mode the agent owns the complete process: it inspects the
project and prepares the complete change across the required files in one turn.
Loopbiotic then presents that batch hunk by hunk; `Accept` advances locally without
another model call, and accepting the final complete hunk closes the goal
locally. `Reject`, `Retry`, a file the agent could not inspect, or an explicit
question can return control to the agent. User control is the hunk gate, not a
repeated discovery/assess/draft ceremony. Asking `Why`
opens a side conversation about the pending hunk and then returns to that exact
draft without advancing or replacing it. Explicit
`/{kind}` prompts and investigate/explain/review modes remain available for
one-card workflows.
The goal and accepted-step count stay visible on cards and editable drafts, but
`auto` is conversational: it never starts goal execution or returns a patch on
the first turn. Use `Goal` (`<leader>pG` or `:LoopbioticGoal`) to authorize
persistent execution, and send a normal message at any time to pause it and get
a conversational answer. Asking `Why` explains the pending hunk and returns to
that exact draft without advancing or replacing it.

When the goal completes, Loopbiotic automatically checks error-level diagnostics in
the changed, loaded buffers after a short delay. `Check` repeats the same local
operation without spending model tokens, saving buffers, or running shell test
commands.

Speculative patch prefetch is off by default because an unused draft still costs
a full model turn. Set `backend.prefetch = "fix"` only when that latency/cost
tradeoff is intentional.
Ordinary speculation is read-only: by default
`backend.prefetch = "read_only"` prepares only the conversational card that
follows an accepted non-goal draft. Set it to `"off"` to disable this. Patch
speculation is allowed only inside an explicitly active goal.

Cards show raw, cached, and non-cached turn and session usage against
`backend.token_budget` (50,000 raw tokens by default).
Expand Down Expand Up @@ -327,7 +357,7 @@ Choosing `Fix` on a card first moves the source context to the card's
The patch agent therefore receives the recommended consumer or template instead
of the file where discovery happened.

The future optional classical-ML ranking design is documented in [`ml.md`](ml.md).
The future optional classical-ML ranking design is documented in [`doc/ml.md`](doc/ml.md).
The current implementation does not train or run an ML model.

## Commands
Expand All @@ -336,6 +366,8 @@ The current implementation does not train or run an ML model.
:Loopbiotic
:LoopbioticReply
:LoopbioticFix
:LoopbioticGoal
:LoopbioticCancel
:LoopbioticWhy
:LoopbioticFollow
:LoopbioticOther
Expand Down
Loading