Bounded AI code edits via Codestral fill-in-the-middle: rewrite only the region between two anchors, the rest stays byte-identical.
Project description
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.
Why this exists
Most coding agents edit by regenerating a block and pasting 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.
No agent CLI exposes a verified bounded-edit primitive. Claude Code and Codex edit by regenerating blocks; plain
vibehas no FIM patch op. Codestral's FIM is what makes a provably-bounded edit possible, andvibe-fimturns it into one an agent can call and check.
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 — the thing the FIM ecosystem (all autocomplete) does not do. 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.)
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
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file vibe_fim-0.4.0.tar.gz.
File metadata
- Download URL: vibe_fim-0.4.0.tar.gz
- Upload date:
- Size: 26.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.16
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
458a2c36fde844e33597b2d3f812d2fa1d01354329229ff2476e7649242c48d3
|
|
| MD5 |
1ea8897823fcaeac64fd78ce139e6e70
|
|
| BLAKE2b-256 |
4c1372c48e71b03ee7504f63d40fa4fcdd4f2633201f25f9d0caad41446daacf
|
File details
Details for the file vibe_fim-0.4.0-py3-none-any.whl.
File metadata
- Download URL: vibe_fim-0.4.0-py3-none-any.whl
- Upload date:
- Size: 21.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.16
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ec250cfdc21d2d604c95b24536aaf1144f15e2fa265c1e7baf1ba26ab785911b
|
|
| MD5 |
4147be79938adfe293b910d6594db735
|
|
| BLAKE2b-256 |
0908cb9a0f2d8078166494cfbb925fc388c69a6b0d093ec6cf9b6cd06b50b2f7
|