vibe-fim
Bounded AI code edits via Codestral fill-in-the-middle. Rewrite only the region between two anchors — the rest of the file comes out byte-for-byte identical, by construction, including existing LF, CRLF, or CR line endings.
Why this exists
Many AI editing workflows regenerate a block and paste it back. The model re-emits your whole function (or file) token by token, so it can silently change things you never asked it to touch — reflow a comment, drop a trailing newline, "fix" an unrelated line, normalise quotes.
Fill-in-the-middle (FIM) makes that structurally impossible. You hand the model a verbatim prefix and suffix; it only writes the middle. So the edit is provably confined to the region between two anchors. Everything outside is never sent to be rewritten — it cannot drift.
vibe-fim wraps Mistral's Codestral FIM in a one-call surgical-patch
primitive. The bytes outside the region are the original slices, concatenated
unchanged — so bounded scope is structural. On top of that it runs the checks
that can actually fail (below), so a bad completion is caught, not shipped.
vibe-fim packages Codestral FIM as an agent-callable bounded-edit primitive,
with a scope report and a parse check where the standard library supports the
language. Bounded scope does not prove semantic correctness or runtime behavior;
those remain downstream checks.
Install
pip install vibe-fim
export MISTRAL_API_KEY=... # from https://console.mistral.ai
Zero runtime dependencies (stdlib urllib only).
CLI
# Rewrite only what's between the two anchors; show the diff without writing.
vibe-fim patch --file units.py \
--before "def to_celsius(f):" \
--after $'\n\ndef to_fahrenheit' \
--hint "round the result to 1 decimal place" \
--dry-run
The scope report goes to stderr:
[BOUNDED + PARSES] edit confined between anchors.
frozen prefix: 142 chars (unchanged)
frozen suffix: 98 chars (unchanged)
region: 31 chars -> 39 chars
Drop --dry-run to write the file in place. If the patched file no longer
parses, the CLI refuses to write (exit 3) unless you pass --allow-broken.
Many edits, one transaction
patch-many applies N bounded edits to one file as a single all-or-nothing
transaction. Every
segment between and around the regions stays byte-identical, the assembled file
is parse-checked once, and the whole result is written or nothing is. A model
that drifts in one region cannot corrupt the others; a result that doesn't parse
is rejected as a unit.
vibe-fim patch-many --file app.py --json --edits '[
{"before": "def to_celsius(f):", "after": "\ndef to_fahrenheit", "hint": "round to 1dp"},
{"before": "def format_row(f):", "after": "\nTABLE_FOOTER", "hint": "pad to 6 cols"}
]'
# {"bounded": true, "parses": true, "regions": 2, ... "wrote": true}
Edits must be in file order and non-overlapping; an ambiguous/missing anchor, an
overlap, or a non-parsing result rejects the whole transaction (nothing is
applied). from vibe_fim import patch_regions for the same as a library call.
Grow tests, provably additive
grow-tests uses FIM to append new test_* functions and proves the change is
additive — your green tests stay green by execution, not by hope:
vibe-fim grow-tests --file tests/test_app.py --focus "edge cases" --json
A four-part gate, all-or-nothing (nothing is written unless all hold):
- byte-identical prefix — the original file is prepended verbatim, so every pre-existing test is the same source (not regenerated-and-hopefully-equal);
- parses — the assembled file still parses;
- node-ids monotonic — a real pytest run collects every old test id plus the new ones (nothing renamed, removed, or shadowed by a duplicate name);
- green stays green — each pre-existing test keeps the outcome it had on the original file, and each new test actually executed.
The pytest run is the proof (via the builtin --junit-xml channel — no plugin
dep). Exit 4 if the additive proof fails; exit 3 if it does not parse. Generated
tests are candidates — review them for meaning: the gate proves they are
additive and that they run, not that they are good tests. A live Codestral run,
replayable offline, is committed at examples/GROW_TESTS_RECEIPT.md.
Agent-callable
An agent shells out and consumes a stable JSON contract — no screen-scraping:
vibe-fim patch --stdin --before "…" --after "…" --name app.py --json
# {"bounded": true, "parses": true, "scope_report": "...", "unified_diff": "...",
# "frozen_prefix_chars": 142, "frozen_suffix_chars": 98, "trimmed_overlap": 0,
# "new_middle": "...", "wrote": false}
--stdin/--text pass the source inline (no disk round-trip); on error the
record is {"bounded": false, "error": "..."} with a non-zero exit, so a loop
can branch. There is also an MCP server — see Wiring into an agent below.
Library
from vibe_fim import patch_region
result = patch_region(
source_code,
before="def to_celsius(f):", # frozen prefix ends right after this
after="\n\ndef to_fahrenheit", # frozen suffix starts right at this
hint="round the result to 1 decimal place",
)
assert result.bounded # structural invariant
assert source_code.startswith(result.prefix) # prefix is byte-identical
assert result.parses is not False # patched file still parses
new_source = result.text
The completion backend is injectable, so the bounded-edit logic is fully testable with no network:
patch_region(src, before, after, fim_fn=lambda prefix, suffix: " return 42")
Anchors must be unique in the file. A missing or ambiguous anchor raises
FimError instead of guessing — surgical edits should never be applied to the
wrong place.
Verification that can fail
Bounded scope is structural (the prefix/suffix are the original slices). The value is in the checks that a bad completion can actually trip:
- Boundary echo. FIM models sometimes re-emit the start of the suffix (or end
of the prefix) inside the middle — duplicating code, on the same line, not just
across a line break.
patch_regiondetects and strips a real re-emission (a run of ≥4 non-whitespace chars) while leaving a coincidental shared}or a boundary newline alone.result.trimmed_overlapreports what was removed. - Truncation. A completion cut off at
max_tokensis an incomplete middle.finish_reason == "length"raises instead of shipping a half-written edit. - Parse gate. For languages we can parse with the stdlib (Python via
ast), the patched file is re-parsed;result.parsesisTrue/False/None, the report shows[BOUNDED + PARSES]/[BOUNDED + DOES NOT PARSE], and the CLI refuses to write a non-parsing result by default.
The guarantee, and the drift — demonstrated
Two separate claims, kept separate:
- The guarantee is structural: surgical FIM changes 0 lines outside the target, by construction, because the frozen regions are never regenerated.
- The drift is measured.
examples/bench.pyreplays a committed Codestral transcript (examples/fixtures/) and counts collateral with one newline-aware counter for both approaches:
surgical FIM : 0 lines changed outside target
block regeneration: 2 lines changed outside target # one source line: its trailing newline dropped (counted as a -/+ diff pair)
It runs offline by default (no key, same numbers for every reviewer); pass
--live to re-capture from the API. The block-regen baseline produced the
correct body and still silently altered the file outside the edit.
python examples/bench.py # offline replay of the committed fixture
MISTRAL_API_KEY=... python examples/bench.py --live
Wiring into an agent
vibe-fim patch --json is the stable contract for shelling out. For a native
tool, install the optional MCP server and point any MCP client at it:
pip install "vibe-fim[mcp]"
vibe-fim-mcp # stdio MCP server exposing one tool: surgical_patch
The tool returns the unified diff plus the scope report, so the agent sees the
bounded proof — not just a success flag. (The base package stays
zero-dependency; the mcp SDK is pulled in only by the [mcp] extra. The core
CLI supports Python 3.9+; the MCP extra requires Python 3.10+.)
How it works
- Locate the unique
beforeandafteranchors. prefix = text[:end_of_before],suffix = text[start_of_after:].- Send
(prefix, suffix)to Codestral FIM →middle(de-duplicated, truncation-checked). new_text = prefix + middle + suffix; the bytes outside the region are the same slices, concatenated unchanged.- Run the parse gate and surface the result.
License
MIT © Guillaume Vele
Metadata
Release files for vibe-fim 0.4.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| vibe_fim-0.4.1.tar.gz | 27.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| vibe_fim-0.4.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 49.8 kB
Release files / vibe_fim-0.4.1.tar.gz
| Download URL | vibe_fim-0.4.1.tar.gz |
|---|---|
| Size | 27.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
dede77f80ae251a87ed9213a9a45ad9c512944074404fada2585b0523e2ee396
|
|
BLAKE2b-256 checksum How to use checksums |
62432f0dedb4d8354fb9b5ad36515aeadc58e7eda699398afc54756447ec5c9e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.15
|
Release files / vibe_fim-0.4.1-py3-none-any.whl
| Download URL | vibe_fim-0.4.1-py3-none-any.whl |
|---|---|
| Size | 22.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
2767c55f4dd50ef447abc29320ecb55f950d5094ae9e7a77ab13c317321ad2f4
|
|
BLAKE2b-256 checksum How to use checksums |
08f789f6e8dd334396a08d5915c99af33e03d58bb4982c4ac479cde63910d7e6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.15
|