Skip to main content

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.

CI Python License deps


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):

  1. byte-identical prefix — the original file is prepended verbatim, so every pre-existing test is the same source (not regenerated-and-hopefully-equal);
  2. parses — the assembled file still parses;
  3. 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);
  4. 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_region detects 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_overlap reports what was removed.
  • Truncation. A completion cut off at max_tokens is 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.parses is True/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.py replays 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

  1. Locate the unique before and after anchors.
  2. prefix = text[:end_of_before], suffix = text[start_of_after:].
  3. Send (prefix, suffix) to Codestral FIM → middle (de-duplicated, truncation-checked).
  4. new_text = prefix + middle + suffix; the bytes outside the region are the same slices, concatenated unchanged.
  5. Run the parse gate and surface the result.

License

MIT © Guillaume Vele

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

vibe_fim-0.4.1.tar.gz (27.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

vibe_fim-0.4.1-py3-none-any.whl (22.5 kB view details)

Uploaded Python 3

File details

Details for the file vibe_fim-0.4.1.tar.gz.

File metadata

  • Download URL: vibe_fim-0.4.1.tar.gz
  • Upload date:
  • Size: 27.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.15

File hashes

Hashes for vibe_fim-0.4.1.tar.gz
Algorithm Hash digest
SHA256 dede77f80ae251a87ed9213a9a45ad9c512944074404fada2585b0523e2ee396
MD5 b50cf0406bc5f3fa7e6da7194d333625
BLAKE2b-256 62432f0dedb4d8354fb9b5ad36515aeadc58e7eda699398afc54756447ec5c9e

See more details on using hashes here.

File details

Details for the file vibe_fim-0.4.1-py3-none-any.whl.

File metadata

  • Download URL: vibe_fim-0.4.1-py3-none-any.whl
  • Upload date:
  • Size: 22.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.15

File hashes

Hashes for vibe_fim-0.4.1-py3-none-any.whl
Algorithm Hash digest
SHA256 2767c55f4dd50ef447abc29320ecb55f950d5094ae9e7a77ab13c317321ad2f4
MD5 8b2b96a940190ddf30e918f1ac053668
BLAKE2b-256 08f789f6e8dd334396a08d5915c99af33e03d58bb4982c4ac479cde63910d7e6

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page