AI assistance
FoBiS knows every source file of the project, the module graph, the build modes with their compiler, flags and -D defines, and the git history. The AI commands put that knowledge in front of a language model run on your machine: why a build failed, why a program crashed, what a procedure does, a test for a module, a second reading of lint findings or of a change, a commit message, a resolution of a merge conflict. The model is always the last step, and nothing it says is written anywhere without you.
How it works:
- Facts first. A deterministic step does what it can before any model runs: a missing module, an unlinked object, a located crash, the findings of the rules, the scopes the history uses, the merge hunks that need no judgement. Often that is the whole answer, and no model is started.
- A context you can read. What the model sees is assembled by FoBiS, not searched for by the model: the error and the code around it, the declarations and interfaces it names, the build mode and its flags, a few sections of the knowledge packs. The same inputs give the same context, and every command prints it without running a model.
- Cited claims, checked. The answer is a JSON object of claims, each citing the numbered sources (
S1,S2, ...) of the context. The schema lists the labels as an enumeration, so a model run with a grammar cannot invent one; FoBiS still checks every citation and drops the claims citing nothing it gave. - Outputs that can be checked are checked. A generated test is kept only when it builds, runs and checks something; a merge proposal is compiled; a lint verdict without a citation becomes
uncertain; a commit scope must be one the facts allow. The exit status oflintandreviewnever depends on a model. - Local, explicit, bounded. The default model runs on your machine (a pinned llama.cpp build and pinned models); nothing is downloaded unless you ask. Every answer has bounded lengths and a token cap. Nothing is written to your files or to git without
--apply, and merges and commits ask first.
Tutorial: AI-assisted development. Reference: build, run, test, ask, testgen, lint, review, commit, merge-assist, ecosystem.
Set up a local model
fobis llm runtime install # once: the pinned llama.cpp build (CUDA, Metal or CPU, detected)
fobis llm pull coder-7b # once: 4.7 GB, the default modelEvery command of this page then starts the model when it needs it and stops it at the end. Other models, Ollama or an OpenAI-compatible server, a persistent server for many commands, a cluster without internet, the knowledge packs, the MCP server for agents: Setting up the AI.
Why the build failed
fobis build --explain # facts first, then the model for what they do not cover
fobis build --mode debug --explain
fobis build --explain-context # print what the model would receive; no modelThe compiler output is printed as usual, then the explanation; the exit status is still 1. Most failures need no model. A misspelt module name:
$ fobis build --mode release --quiet --explain
warning: the file 'src/integrate.F90' depends on 'quad_rule' that is unreachable
Building src/romberg.f
Nothing to compile, all objects are up-to-date
Building src/integrate.F90
Compiling src/integrate.F90 serially
src/integrate.F90:4:5:
4 | use quad_rule, only : R8P, trapezoid, simpson
| 1
Fatal Error: Cannot open module file 'quad_rule.mod' for reading at (1): No such file or directory
compilation terminated.
Explanation (fobis build --explain)
src/integrate.F90:4: module quad_rule is not available when src/integrate.F90 is compiled
- no source defines module quad_rule; modules with a close name: quad_rules (src/quad_rules.F90).
fix: if the name is misspelt, use quad_rules
[exit status 1]FoBiS parses the messages of gfortran, ifx/ifort, nvfortran, LFortran, GNU ld and lld, scans the project, and looks at what the build compiled and linked:
- A missing module: which file defines it and why this build does not compile it (outside
src, listed inexcludeorexclude_dirs), a definition hidden by a preprocessor branch the mode's-Dflags disable, a fetched dependency defining it, a declared dependency not fetched yet, a library module (mpi,mpi_f08,omp_lib,openacc,cudafor,hdf5,netcdf) with the flag it needs, or a close name of an existing module. - An undefined reference: the symbol demangled (gfortran
__mod_MOD_proc, ifxmod_mp_proc_, nvfortranmod_proc_), where it is defined, and why its object is not linked: not compiled, in a file the program never reaches, a dependency not linked, a library (MPI, OpenMP, OpenACC, HDF5, netCDF, BLAS/LAPACK) missing from the link flags, no main program. - Objects of another compiler: runtime symbols (
_gfortran_*,for_*,pgf90_*) of a compiler other than the mode's, left by another mode. - A multiple definition (the files defining the symbol, or two main programs), an internal compiler error (a compiler defect, with the usual workarounds).
Errors that follow from an explained one are counted, not explained again. Two more real outputs:
Explanation (fobis build --explain)
src/linkmain.f90:2: module geometry is not available when src/linkmain.f90 is compiled
- geometry is defined in extra/geometry.f90, which this build does not compile: `exclude_dirs` lists extra.
fix: add the file to the build (fobos mode `src`, `exclude`, `exclude_dirs`)Explanation (fobis build --explain)
link: external_thing is referenced (referenced from MAIN__) but not linked
- external_thing is defined in src/things.f90:6.
- the target src/main.f90 does not link that file: no file it links uses the module(s) things.
fix: FoBiS links the files a program reaches through `use`, plus the files without modules: put external_thing in a module that the caller uses, or in a file without modulesFor the first error no fact covers (a type mismatch, a syntax error, a constraint), the model gets the compiler's message, the build (compiler, mode, flags), the lines around the error with the error line marked >, the declarations of the enclosing procedure, the interfaces of the project procedures named on the line, and at most two knowledge-pack sections that match the message well enough. It answers claims of two kinds, cause and fix:
The model's input (fobis build --explain-context)
$ fobis build --mode release --quiet --explain-context
Building src/romberg.f
Nothing to compile, all objects are up-to-date
Building src/integrate.F90
Compiling src/integrate.F90 serially
src/integrate.F90:14:33:
14 | print '(I6, 2ES18.4)', n, abs(trapezoid(sine, 0, pi, n) - 2._R8P), abs(simpson(sine, 0._R8P, pi, n) - 2._R8P)
| 1
Error: Type mismatch in argument 'a' at (1); passed INTEGER(4) to REAL(8)
Explanation (fobis build --explain)
=== system prompt ===
You explain why a Fortran compile step failed and how to fix it. You receive the compiler's message, the
build settings, and numbered sources: the source lines around the error (the error line is marked `>`), the
declarations of the procedure that contains it, the interfaces of project procedures named on that line, and
sections of the Fortran standard and practice notes. You may use ONLY the message and the numbered sources.
Answer with one JSON object:
- `claims`: short, self-contained sentences, each with a `kind` and the labels of the sources that support
it (`"citations": ["S1", "S3"]`).
- `kind: "cause"`: what in the code makes the compiler reject it. Name the line, the entity and the rule
it breaks, as the sources show them.
- `kind: "fix"`: a concrete change to the code or to the build that removes the error. Quote the code to
write when the change is small.
- `insufficient`: true when the sources do not show the cause; then return no claims rather than guessing.
Rules:
- Never state something that no source supports, even if you believe it is true: such statements are
discarded.
- Explain the error of the message, not other problems you notice.
- Give at least one cause and, when the sources allow it, one fix.
- Quote identifiers, types and constraint numbers exactly as the sources write them.
- Do not mention the labels or this format inside the claim text.
=== user message ===
## Compiler message
src/integrate.F90:14:33:
14 | print '(I6, 2ES18.4)', n, abs(trapezoid(sine, 0, pi, n) - 2._R8P), abs(simpson(sine, 0._R8P, pi, n) - 2._R8P)
| 1
Error: Type mismatch in argument 'a' at (1); passed INTEGER(4) to REAL(8)
## Build
compiler: gfortran (gnu)
fobos mode: release
compile flags: -O3 -funroll-loops -c
## Sources
[S1] src/integrate.F90 lines 8-18 (code:src/integrate.F90:8-18)
8 | real(R8P), parameter :: pi = acos(-1._R8P) !< Pi.
9 | integer :: n !< Number of intervals.
10 |
11 | print '(A6, 2A18)', 'n', 'trapezoid error', 'simpson error'
12 | n = 4
13 | do while (n <= 64)
14 > print '(I6, 2ES18.4)', n, abs(trapezoid(sine, 0, pi, n) - 2._R8P), abs(simpson(sine, 0._R8P, pi, n) - 2._R8P)
15 | n = 2 * n
16 | enddo
17 | print '(A)', 'Simpson, 64 intervals: '//trim(str(simpson(sine, 0._R8P, pi, 64), no_sign=.true., compact=.true.))
18 | endprogram integrate
[S2] declarations of program integrate (src/integrate.F90:2) (fact:src/integrate.F90#integrate)
program integrate
use quad_rules, only : R8P, trapezoid, simpson
use integrands, only : sine
use penf, only : str
real(R8P), parameter :: pi = acos(-1._R8P) !< Pi.
integer :: n !< Number of intervals.
[S3] interface of function trapezoid (module quad_rules), src/quad_rules.F90:38 (fact:src/quad_rules.F90#trapezoid)
pure function trapezoid(f, a, b, n) result(s)
procedure(integrand) :: f !< Integrand.
real(R8P), intent(in) :: a !< Lower bound.
real(R8P), intent(in) :: b !< Upper bound.
integer, intent(in) :: n !< Number of intervals.
real(R8P) :: s !< Integral estimate.
real(R8P) :: h !< Interval width.
integer :: i !< Counter.
[S4] interface of function sine (module integrands), src/integrands.F90:10 (fact:src/integrands.F90#sine)
pure function sine(x) result(y)
real(R8P), intent(in) :: x !< Abscissa.
real(R8P) :: y !< sin(x).
[S5] interface of function simpson (module quad_rules), src/quad_rules.F90:61 (fact:src/quad_rules.F90#simpson)
pure function simpson(f, a, b, n) result(s)
procedure(integrand) :: f !< Integrand.
real(R8P), intent(in) :: a !< Lower bound.
real(R8P), intent(in) :: b !< Upper bound.
integer, intent(in) :: n !< Number of intervals, even.
real(R8P) :: s !< Integral estimate.
real(R8P) :: h !< Interval width.
real(R8P) :: odd !< Sum at the odd nodes.
real(R8P) :: even !< Sum at the even inner nodes.
integer :: i !< Counter.
[S6] Glossary — Fortran 2023 Standard - Fortran 2023 standard: distilled reference (fortran-std:glossary#glossary-fortran-2023-standard-2)
**actual argument** — an expression, variable, or procedure passed in a procedure reference; associated with a dummy argument (15).
**allocatable** — attribute for an object whose allocation status is dynamic; auto-deallocated at scope exit (8).
**array section** — a designator selecting a subset of an array via triplets or vector subscripts (9).
**assumed-rank** — dummy `a(..)` whose rank is taken from the actual argument; used with SELECT RANK (8, 11).
**assumed-shape** — dummy `a(:)` whose extents come from the actual; **lower bound defaults to 1** (8).
**assumed-size** — dummy `a(*)`; last dimension size unknown; legacy/interop (8).
**assumed-type** — `TYPE(*)` dummy; unlimited polymorphic, opaque pass-through (7).
**association** — a relationship by which an entity is reachable by another name/storage; 9 kinds (3, 19).
**attribute** — a property of an entity (ALLOCATABLE, POINTER, INTENT, …) (8).
**BIND(C)** — gives an entity C linkage and an optional binding label (18).
**BLOCK construct** — a nested scope permitting local declarations within the execution part (11).
**characteristics** — the fixed properties of a procedure/dummy used in interface matching (15).
**CLASS(*)** — unlimited polymorphic declaration; dynamic type may be anything (7).
**coarray** — an object with codimensions, accessible across images (8, 9).
**collating sequence** — processor-dependent ordering of a character set (3, 20).
[...]
[S7] Fortran Conventions > Kind Specifications (Portability) > Forbidden kind-violating patterns - Fortran and GPU/HPC practice: conventions, pitfalls, measured lessons (hpc-practice:fortran-practice#forbidden-kind-violating-patterns)
These are the LLM's default Fortran sins — the forms generated when no kind discipline
is enforced. Reject every one of them on sight; they bypass the kind system, hardcode
precision, and silently break single/mixed-precision builds.
- `dsqrt`, `dexp`, `dlog`, `dabs`, `dcos`, `dsin`, `dtan`, `datan`, `datan2`, `dble`,
`dmin1`, `dmax1`, etc. → use the **generic intrinsics** `sqrt`, `exp`, `log`, `abs`,
`cos`, ...; let the compiler resolve to the right kind from the argument.
- `1.0d0`, `2.5d-3`, `1.d-12` (the `d` exponent literal) → use kind-suffixed literals:
`1.0_R8P`, `2.5e-3_R8P`. The `d` form silently locks to double precision.
- `real(8)`, `real(4)`, `real(16)` → bare numeric kinds are non-portable and
meaningless across compilers. Use `real(R8P)` / `real(real64)` via your kind module.
- `double precision` keyword → use `real(R8P)` / `real(real64)`.
- `dble(x)` for type conversion → use `real(x, R8P)` / `real(x, real64)`.
- Hardcoded byte sizes like `int(8._R8P, 8)` → use `storage_size(0._R8P)/8` or
`c_sizeof` for portability across precision modes.
- Bare integer-kinded "real" literal like `2_R8P` → spelled `2.0_R8P`; `2_R8P` is an
*integer* literal of kind R8P, not a real, and silently wrong in expressions expecting a real.
The single test: every floating-point literal and every intrinsic call must compile
identically when `R8P` is redefined from `real64` to `real32` (single precision) or to
[...]
[exit status 1]$ fobis build --mode release --quiet --explain
The compiler changed since the last build (path '~/.local/bin/gfortran' -> '/opt/rh/gcc-toolset-12/root/usr/bin/gfortran'): rebuilding everything
Building src/romberg.f
Compiling src/romberg.f serially
Building src/integrate.F90
Compiling .fobis_deps/penf/src/lib/penf_global_parameters_variables.F90 serially
Compiling .fobis_deps/penf/src/lib/penf_b_size.F90 serially
Compiling .fobis_deps/penf/src/lib/penf_stringify.F90 serially
Compiling src/quad_rules.F90 .fobis_deps/penf/src/lib/penf_allocatable_memory.F90 serially
Compiling src/integrands.F90 .fobis_deps/penf/src/lib/penf.F90 serially
Compiling src/integrate.F90 serially
src/integrate.F90:14:33:
14 | print '(I6, 2ES18.4)', n, abs(trapezoid(sine, 0, pi, n) - 2._R8P), abs(simpson(sine, 0._R8P, pi, n) - 2._R8P)
| 1
Error: Type mismatch in argument 'a' at (1); passed INTEGER(4) to REAL(8)
Explanation (fobis build --explain)
src/integrate.F90:14: Type mismatch in argument 'a' at (1); passed INTEGER(4) to REAL(8)
Using the running llama-server at http://127.0.0.1:41675
Cause:
- The argument 'a' in the call to 'trapezoid' is of type INTEGER(4), but it should be of type REAL(8). [1][2]
Fix:
- Change the argument 'a' to '0._R8P' to match the expected type REAL(8). [1][2]
Sources:
[1] src/integrate.F90 lines 8-18 (code:src/integrate.F90:8-18)
[2] interface of function trapezoid (module quad_rules), src/quad_rules.F90:38 (fact:src/quad_rules.F90#trapezoid)
[exit status 1]
coder-7b on 6 CPU cores and no GPU; each answer takes about 45 s there, cut from the recording.
- Only the first unexplained error goes to the model, then
(N more error(s) not explained: fix this one first, then rebuild): later errors often follow from it. - The fix is a suggestion, not compiled. Read it before applying it: on the
fobis newdemo, coder-7b on the CPU proposed1.0for areal64argument. - A C source's error is not explained: FoBiS says so and stops (the prompt and the packs are Fortran's).
- Pack retrieval matches the words of the message, which share few with the standard: often no section qualifies, and the answer rests on the code facts alone.
- The scan runs only after a failure and takes a few seconds on large trees (about 5 s for 850 files).
--jsonwins: with it the output is the JSON report, which carries the same diagnoses;--explainis ignored.- No model configured: the facts are still printed, and FoBiS says why no model explanation follows.
Tutorial: build failures, explained.
Why the program crashed
fobis run --explain # build (with --explain), run, explain a failure
fobis run --mode debug --explain
fobis test --explain # every failed testThe program's standard error is printed as it runs and recorded; the exit status is the program's. On a failure, FoBiS reads the runtime message (gfortran, Intel, nvfortran), the backtrace and the exit status:
$ fobis run --no-build --explain
n trapezoid error simpson error
ERROR STOP simpson: the number of intervals must be even
Error termination. Backtrace:
#0 0x7852f5428d14 in ???
#1 0x7852f5429ab1 in ???
#2 0x7852f542b1c8 in ???
#3 0x6405a48d640b in __quad_rules_MOD_simpson
at src/quad_rules.F90:79
#4 0x6405a48f0204 in integrate
at src/integrate.F90:14
#5 0x6405a48f044e in main
at src/integrate.F90:4
Explanation (fobis run --explain)
the program failed at src/quad_rules.F90:79
- runtime message: simpson: the number of intervals must be even
- statement: if (mod(n, 2) /= 0) error stop 'simpson: the number of intervals must be even'
[exit status 1]- An out-of-bounds access: the array, the index and the bound, the statement, the declaration; for a dummy argument, the caller that passes it.
- An unassociated pointer: Intel's message, or the bounds of an empty pointer (
1:0,0:-1) that gfortran and nvfortran print. error stop: the statement that printed the message, found by its text (Intel prints only the message).- Known compiler bugs: gfortran
-O2+ EFAULT (errno=14) on pointer sections passed to explicit-lower-bound dummies; nvfortran CUDA error 719 on an OpenACC reduction into adeclare createmodule allocatable, with the matching reductions of the project. - No location at all: in a debug build, how to get one with a debugger.
An optimised build's backtrace is not trusted. The frames of an -O2/-O3 build are often mis-resolved, so FoBiS uses a location only when the runtime message gives it (gfortran and nvfortran print the failing line even at -O3) or when the build is unoptimised with -g. Otherwise it names the mode to rerun in (unoptimised, -g, the same compiler preferred), or the flags to add when there is none:
Explanation (fobis run --explain)
segmentation fault (invalid memory access)
- runtime message: SIGSEGV: Segmentation fault - invalid memory reference.
mode opt is optimised (-O3), without debug information (-g), without runtime checks
- the backtrace of an optimised build is often wrong: the reported routine or line may not be where the fault is, so FoBiS does not explain the crash from it.
- with runtime checks a segmentation fault often becomes a located message (an index out of bounds, an unassociated pointer).
fix: reproduce it in a debug build first: fobis run --mode debug --explain
see: fobis kb show hpc-practice:gpu-practice#correctness-and-debugging-toolsIn a debug build the same defect is located:
Explanation (fobis run --explain)
index 11 of array a is above its upper bound 10 at src/kernels.f90:18
- runtime message: Index '11' of dimension 1 of array 'a' above upper bound of 10
- statement: a(i) = 0.5 * (a(i) + a(i + 1))
- declaration: real, intent(inout) :: a(:) (src/kernels.f90:14)
- a is a dummy argument of smooth: the caller passes it, called from src/bounds.f90:6.The model is asked only when the location is trusted and the facts leave something to explain. It gets the runtime output, the build (flags, optimisation, debug information, checks), the code around the failing line and around its caller, and their declarations, and answers cited cause and fix claims.
test --explainasks the model once: every failed test gets its facts, the model only the first failure the facts leave open. A test whose build failed is explained by the build, not here.- Rebuild under checks without a debug mode:
fobis run --shadow checks --explain(see Testing).
Tutorial: crashes, located.
Ask about the code and the standards
fobis ask "why does my MPI code hang when every rank sends first?"
fobis ask -p src -p test "which rule integrates cubic polynomials exactly?" # the project's units too
fobis ask --pack openmp "is a loop variable private in a parallel do?" # one pack only
fobis ask --show-context -p src "..." # the model's input; no model
echo "..." | fobis ask - # the question from standard inputThe answer is drawn from the knowledge packs shipped with FoBiS (the Fortran standard, OpenMP, OpenACC, MPI, HPC practice, compilers, libraries, numerics) and, with -p DIR, from the project's own units: each module, procedure and program described by its header, its documentation comment, its arguments, the modules it uses, its calls, its MPI calls and directives, its location:
The model's input (fobis ask --show-context)
$ fobis ask --show-context -p src -p test "Which rule integrates cubic polynomials exactly?"
=== system prompt ===
You answer technical questions about Fortran, OpenMP, OpenACC, MPI, HPC practice and the user's Fortran
project. You may use ONLY the numbered sources given with the question: the knowledge-pack sections and,
when present, entries describing the project's units (modules, procedures, programs). A `doc:` line in such
an entry is the authors' documentation of that unit: what it states may be cited.
Answer with one JSON object:
- `claims`: the statements that answer the question, each a short, self-contained sentence, each with the
labels of the sources that state it (`"citations": ["S2"]`). Cite only sources that really contain the
statement. Prefer several precise claims to one long one.
- `insufficient`: true when the sources do not answer the question; then return no claims rather than
answering from memory.
Rules:
- Never state something that no source supports, even if you believe it is true: such statements are
discarded.
- For the project's procedures, say where things are (procedure, file, line) as the sources give them;
do not describe code you were not shown.
- Quote identifiers, directives and constraint numbers exactly as the sources write them.
- Do not mention the labels or this format inside the claim text.
=== user message ===
## Question
Which rule integrates cubic polynomials exactly?
## Sources
[S1] program test_exactness (test/test_exactness.F90:2) - this project's sources (project:test/test_exactness.F90#test_exactness)
program test_exactness in test/test_exactness.F90, lines 2-12
header: program test_exactness
doc: Each rule integrates exactly the polynomials of its degree: trapezoid lines, Simpson cubics.
uses: polynomials, quad_rules
[S2] function cubic (test/polynomials.F90:18) - this project's sources (project:test/polynomials.F90#cubic)
function cubic in test/polynomials.F90, lines 18-24
header: pure function cubic(x) result(y)
doc: x**3, integral on [0, 2] = 4.
arguments: x
[S3] module polynomials (test/polynomials.F90:2) - this project's sources (project:test/polynomials.F90#polynomials)
module polynomials in test/polynomials.F90, lines 2-25
header: module polynomials
doc: Polynomials whose integrals are known exactly.
uses: quad_rules
[S4] program test_symmetry (test/test_symmetry.F90:2) - this project's sources (project:test/test_symmetry.F90#test_symmetry)
program test_symmetry in test/test_symmetry.F90, lines 2-23
header: program test_symmetry
doc: Every rule integrates an odd function over a symmetric interval to zero.
uses: polynomials, quad_rules
[S5] program integrate (src/integrate.F90:2) - this project's sources (project:src/integrate.F90#integrate)
program integrate in src/integrate.F90, lines 2-18
header: program integrate
doc: Integrate sin(x) over [0, pi], whose exact value is 2, with more and more intervals.
uses: integrands, penf, quad_rules
[S6] program integrate_mpi (src/integrate_mpi.F90:2) - this project's sources (project:src/integrate_mpi.F90#integrate_mpi)
program integrate_mpi in src/integrate_mpi.F90, lines 2-24
header: program integrate_mpi
doc: Simpson's rule over [0, pi] split among the MPI processes: each integrates its piece, rank 0 adds them up.
uses: integrands, mpi, quad_rules
MPI calls: mpi_init, mpi_comm_rank, mpi_comm_size, mpi_reduce, mpi_finalize$ fobis ask -p src -p test "Which rule integrates cubic polynomials exactly?"
Using the running llama-server at http://127.0.0.1:41675
- Simpson's rule integrates exactly cubic polynomials. [1]
Sources:
[1] program test_exactness (test/test_exactness.F90:2) - this project's sources (project:test/test_exactness.F90#test_exactness)A real answer of coder-7b on an NVIDIA A30, from the MPI pack, in 3.4 s with the server running:
- MPI code hangs when every rank sends first because mutual send-before-recv in standard mode can deadlock (buffering not guaranteed). Use `MPI_Sendrecv` or nonblocking communication instead. [1][2]
- Relying on standard-mode buffering for correctness is not guaranteed; a program that 'works' may deadlock at larger message sizes. [3]
Sources:
[1] Cheatsheet — MPI 5.0 > Decision rules > Avoid deadlock - MPI 5.0: distilled reference (mpi:cheatsheet#avoid-deadlock)
[2] Chapter 3: Point-to-Point Communication > Code Examples - MPI 5.0: distilled reference (mpi:ch03-point-to-point#code-examples)
[3] Chapter 3: Point-to-Point Communication > Anti-patterns - MPI 5.0: distilled reference (mpi:ch03-point-to-point#anti-patterns)Read a cited section in full with fobis kb show <id>.
- Routing. A question naming a programming model (
omp,!$acc,MPI_Isend), a compiler (ifx,nvfortran), a library or launcher (hdf5,mpirun) or a numerical-assessment topic (significant digits, Richardson, tolerance, benchmark) searches that pack plus the Fortran standard, HPC practice and the project, so OpenMP and OpenACC sections do not compete on the words they share.--packreplaces the routing. - Retrieval is lexical: words, not meanings. The OpenMP pack says "loop iteration variables are private"; a question about "the index of a sequential loop" does not reach it among the first sections. Rephrase with the standard's terms, raise
-k(6 sections by default), or look first withfobis kb search. - Document your procedures. A FORD
!<comment after the header, or a Doxygen!>before it, becomes the unit'sdoc:line, which the model may cite. The answer above comes from one. - No match, no model: when retrieval finds nothing,
asksays so and exits 1. When the model's claims do not survive the check, the answer is "The sources found do not answer this question."--verboseprints the dropped statements. - A citation proves the source exists, not that it says what the claim says. Asked how long
simpsontakes on a GPU, coder-7b answered three statements, each citing something real, none answering the question:
$ fobis ask -p src -p test "How long does simpson take on a GPU?"
Using the running llama-server at http://127.0.0.1:41675
- The runtime for simpson on a GPU depends on the specific GPU and the number of intervals (n). [1]
- To run simpson on a GPU, you need to enable OpenACC with the `-acc` option and specify the GPU compute capability with `-gpu=cc80` or `-gpu=ccnative`. [2]
- For optimal performance, minimize host-to-device transfers and maximize data reuse on the device. [3]
Sources:
[1] function simpson (src/quad_rules.F90:61) - this project's sources (project:src/quad_rules.F90#simpson)
[2] NVIDIA Fortran (nvfortran) > GPU options - Fortran compilers in practice: gfortran, ifx, nvfortran defaults, checks, floating point, modules (compilers:nvfortran#gpu-options)
[3] GPU / OpenACC / CUDA Conventions > Data Movement - Fortran and GPU/HPC practice: conventions, pitfalls, measured lessons (hpc-practice:gpu-practice#data-movement)Tutorial: questions about the code.
Generate tests
fobis testgen gauss_legendre --skeleton # no model: the compile-checked skeleton
fobis testgen gauss_legendre # a model fills it; printed if accepted
fobis testgen gauss_legendre --apply # ... and written to test/test_gauss_legendre.f90
fobis testgen penf_b_size -p byte_size --max 4 # one generic, at most 4 procedures
fobis testgen gauss_legendre --context # the model's input; no modelThe skeleton comes from facts: one test subroutine per public procedure (each specific of a public generic, called through the generic; each public binding of a public non-abstract type, called as obj%binding(...)), a local variable for each dummy argument, and a call with keyword arguments. FoBiS builds it with your mode in a scratch copy of the project and drops a subroutine that does not compile, with the compiler's message as the reason:
$ fobis testgen gauss_legendre --skeleton
Module gauss_legendre (src/gauss_legendre.F90): building the skeleton in a scratch copy...
Test of module gauss_legendre:
test gauss_nodes
Skeleton (builds; give the inputs values and add the checks):
! Test of module gauss_legendre: generated by fobis testgen; review it before committing.
program test_gauss_legendre
use gauss_legendre
use quad_rules, only : R8P
implicit none
integer :: failures = 0
call test_gauss_nodes()
if (failures > 0) then
print '(a,i0,a)', 'FAILED: ', failures, ' check(s)'
error stop 1
end if
print '(a)', 'test_gauss_legendre: all checks passed'
contains
subroutine test_gauss_nodes()
integer :: n
real(r8p), allocatable :: x(:)
real(r8p), allocatable :: w(:)
! TODO: give the inputs values, then check the outputs against expected values
call gauss_nodes(n=n, x=x, w=w)
end subroutine test_gauss_nodes
end program test_gauss_legendre
(fobis testgen gauss_legendre --apply writes it to test/)With the model, the skeleton, the public API, the code of the tested procedures (80 lines each at most) and an existing test of the project (for its style) go to the model. The program it returns is built and run in the scratch copy, and accepted only when it exits 0 within the timeout (60 s, --timeout), uses the module, calls one of its public procedures, and holds a check that can fail: error stop, a non-zero stop, or a failure counter incremented. Otherwise the compiler's errors on the test's lines, the exit status and the output, or what is missing, go back to the model, which is told never to remove a check to make a test pass: up to three more attempts (--retries).
$ fobis testgen gauss_legendre --max 2
Module gauss_legendre (src/gauss_legendre.F90): building the skeleton in a scratch copy...
[llm] gauss_legendre: attempt 1/4
Using the running llama-server at http://127.0.0.1:41675
Test of module gauss_legendre:
test gauss_nodes
Accepted after 1 attempt(s): builds, runs (exit 0), calls gauss_nodes; 6 check(s). Review it before committing.
Model notes: The checks verify that the nodes and weights returned by the `gauss_nodes` procedure are correct for a 3-point rule. The expected values were obtained by analyzing the implementation of the procedure and understanding the properties of the Legendre polynomial.
program test_gauss_legendre
use gauss_legendre
use quad_rules, only : R8P
implicit none
integer :: failures = 0
call test_gauss_nodes()
if (failures > 0) then
print '(a,i0,a)', 'FAILED: ', failures, ' check(s)'
error stop 1
end if
print '(a)', 'test_gauss_legendre: all checks passed'
contains
subroutine test_gauss_nodes()
integer :: n
real(r8p), allocatable :: x(:)
real(r8p), allocatable :: w(:)
! TODO: give the inputs values, then check the outputs against expected values
n = 3
allocate(x(n), w(n))
call gauss_nodes(n=n, x=x, w=w)
if (abs(x(1) + 0.774596669241483_R8P) > 1.e-15_R8P) then
print '(a, ES10.3, a, ES10.3)', 'gauss_nodes: x(1) = ', x(1), ', expected = ', -0.774596669241483_R8P
failures = failures + 1
end if
if (abs(x(2) - 0.000000000000000_R8P) > 1.e-15_R8P) then
print '(a, ES10.3, a, ES10.3)', 'gauss_nodes: x(2) = ', x(2), ', expected = ', 0.000000000000000_R8P
failures = failures + 1
end if
if (abs(x(3) - 0.774596669241483_R8P) > 1.e-15_R8P) then
print '(a, ES10.3, a, ES10.3)', 'gauss_nodes: x(3) = ', x(3), ', expected = ', 0.774596669241483_R8P
failures = failures + 1
end if
if (abs(w(1) - 0.555555555555556_R8P) > 1.e-15_R8P) then
print '(a, ES10.3, a, ES10.3)', 'gauss_nodes: w(1) = ', w(1), ', expected = ', 0.555555555555556_R8P
failures = failures + 1
end if
if (abs(w(2) - 0.888888888888889_R8P) > 1.e-15_R8P) then
print '(a, ES10.3, a, ES10.3)', 'gauss_nodes: w(2) = ', w(2), ', expected = ', 0.888888888888889_R8P
failures = failures + 1
end if
if (abs(w(3) - 0.555555555555556_R8P) > 1.e-15_R8P) then
print '(a, ES10.3, a, ES10.3)', 'gauss_nodes: w(3) = ', w(3), ', expected = ', 0.555555555555556_R8P
failures = failures + 1
end if
end subroutine test_gauss_nodes
end program test_gauss_legendre
(fobis testgen gauss_legendre --apply writes it to test/)
coder-7b on an NVIDIA A30 of a cluster node, reached through ssh; recorded in real time. The test was accepted at the first attempt.
The values above are those of the 3-point Gauss-Legendre rule, checkable against any table. On PENF, with the -Werror mode tests-gnu, coder-7b got penf_b_size accepted at the first attempt (20 s, 8 checks) and penf_stringify at the second (53 s, through the generics bcton and bstr).
- A passing test proves the code does what the model read in it, not that it is right. For a procedure without a known answer the checks can only repeat the code. Review the test before committing it.
- Your tree is never written but by
--apply(or-o PATH, which implies it), and never over an existing file. The scratch copy holds the working copy (ignored files excluded) and the fetched dependencies, and is deleted at the end. - The test directory must be among the mode's sources for
fobis testto build the written test. - FoBiS fixes two mistakes itself: lines longer than 132 columns are continued before building, and a compile failure's feedback names the public generic of each private specific the program calls.
- Not tested: procedures in submodules, operators and assignments,
class(*)and assumed-rank arguments, procedure arguments.--max(8 with the model, all with--skeleton) keeps the input small: test a large module a few procedures at a time with-p. - Small models fail here. coder-0.5b: four attempts, nothing accepted (it broke a string literal and could not fix it from the message); coder-1.5b on the CPU: three attempts in 450 s, nothing accepted.
- Exit status: 0 a program accepted (or the skeleton built), 1 none accepted, the project or the skeleton does not build, no model, or the output file exists; 2 module not found or nothing public to test.
Tutorial: generated tests.
A second reader for lint findings
The rules say "this pattern is here", not "this is a bug in your code". --deep asks the model to judge each finding in the code it is in, with citations, and to point out related problems the rules missed:
fobis lint src/solver --select OMP,ACC,MPI --deep # the parallel findings
fobis lint --deep --deep-max 25 # more groups (default 10)
fobis lint --deep-context # the model's input; no model
fobis lint src/flux.F90:120 --ask "is this region race-free?"The findings are grouped by the innermost OpenMP/OpenACC region, else by the enclosing procedure (a window around the findings when it is long); regions go first, then the most severe. The mechanical families (OBS, MDN, PRT, STY, LNT) are not sent. One request per group carries the code with the finding lines marked, the declarations, each rule's documentation and the pack sections it references, the compiler feedback for those lines (with --compiler-feedback), the interfaces of the procedures called, and pack sections matching the directive's clauses. A real answer of coder-7b on an NVIDIA A30, for a loop that lost its private(flux) reduction(+:total):
src/heat.f90:14: OMP001 [warning] 'flux' is shared and written by every thread (missing private?)
[llm] confirmed: The variable 'flux' is shared and written by every thread, leading to a data race. [1][2]
src/heat.f90:16: OMP002 [warning] 'total' is accumulated by every thread: use reduction(+:total)
[llm] confirmed: The variable 'total' is accumulated by every thread without using a reduction clause, leading to incorrect results. [1][3]
Sources cited by the review:
[1] src/heat.f90 lines 12-17 (!$omp parallel do (line 12)) (code:src/heat.f90:12-17)
[2] rule OMP001 shared-scalar-written-in-parallel-region (rule:OMP001)
[3] rule OMP002 accumulation-without-reduction (rule:OMP002)
Review [llm]: 1 of 1 group(s); 2 confirmed, 0 dismissed, 0 uncertain. Dismissed findings are still reported and still count.- Nothing is hidden. A dismissed finding is printed with the model's reason and still counts in the exit status. To silence a false positive, suppress it in the code with a reason (
! fobis: ignore[OMP001] ...) or record it in the baseline: see Code quality. - Uncited verdicts are downgraded. A confirmed or dismissed verdict citing nothing becomes
uncertain, a finding the model skipped staysuncertain, an uncited concern is dropped. - A 7B model's verdict carries no information. On a labelled set of 10 findings (7 real defects, 3 false alarms of the linter), coder-7b confirmed all 10, the false alarms included, restating the finding's message where the code contradicted it. Its value is the reasoning attached to the real ones; use a larger model when the verdicts matter.
- One request per group, and groups are many: 850 files of an HPC solver gave 580. Narrow with paths and
--select, and raise--deep-maxdeliberately. --ask "QUESTION" FILE:LINEbuilds the same input for the region or procedure around that line, findings included, and answers cited claims asfobis askdoes, without the lint report.- Formats: text and json are annotated (
--format jsonadds areviewto each reviewed finding and a top-levelreview); the other formats are not, with a warning.
Tutorial: a second reader.
A risk summary of a change
fobis review reports the lint findings on the changed lines (see Code quality); --deep adds a model's risk summary of the change:
fobis review --deep # every uncommitted change
fobis review --base origin/master --deep # the commits of a branch
fobis review --deep-context # the model's input; no modelThe model gets the diff of each changed Fortran file (what was removed matters as much as what was added), the changed code in context with the changed lines marked, the OpenMP/OpenACC regions the change touches, the files that use a module whose procedure headers changed, and the findings on the changed lines with their rules' documentation. It answers cited claims of two kinds, risks and things to check:
The model's input (fobis review --deep-context)
$ fobis review --deep-context
=== system prompt ===
You summarise the risks of a change to a Fortran code (HPC: OpenMP, OpenACC, MPI). You receive the changed
files, the findings a rule-based linter reported on the changed lines, and numbered sources: the changed code
(changed lines marked `>`), the OpenMP/OpenACC regions the change touches, the files that use a module whose
procedure headers changed, and the documentation of the rules. You may use ONLY these sources.
Answer with one JSON object:
- `claims`: short, self-contained sentences, each with a `kind` and the labels of the sources that support it
(`"citations": ["S1", "S4"]`).
- `kind: "risk"`: what this change could break, and where: a changed interface and the files using it, a data
race or data movement in a touched parallel region, a changed numerical path, a finding of the linter.
- `kind: "check"`: what to verify before merging: a test to run, a caller to update, a result to compare.
- `insufficient`: true when the sources show no risk worth reporting; then return no claims.
Rules:
- Never state something that no source supports: such statements are discarded.
- Be specific: name files, procedures, lines and variables as the sources show them. No generic advice.
- Prefer a few precise claims to many vague ones.
- Do not mention the labels or this format inside the claim text.
=== user message ===
## Changed files
M src/sweep.F90
## Findings on the changed lines
OMP001 [warning] src/sweep.F90:16: 'n' is shared and written by every thread (missing private?)
## Sources
[S1] diff of src/sweep.F90 (- removed, + added) (diff:src/sweep.F90)
@@ -12,5 +12,5 @@ integer :: k !< Counter.
integer :: n !< Number of intervals.
-!$omp parallel do private(n)
+!$omp parallel do
do k = 1, cases
n = 2 ** k
[S2] src/sweep.F90 lines 11-17 (changed lines marked >) (code:src/sweep.F90:11-17)
11 | integer :: k !< Counter.
12 | integer :: n !< Number of intervals.
13 |
14 > !$omp parallel do
15 | do k = 1, cases
16 | n = 2 ** k
17 | error(1, k) = abs(trapezoid(sine, 0._R8P, pi, n) - 2._R8P)
[S3] !$omp parallel do region, src/sweep.F90 lines 14-19 (region:src/sweep.F90:14-19)
14 > !$omp parallel do
15 | do k = 1, cases
16 | n = 2 ** k
17 | error(1, k) = abs(trapezoid(sine, 0._R8P, pi, n) - 2._R8P)
18 | error(2, k) = abs(simpson(sine, 0._R8P, pi, n) - 2._R8P)
19 | enddo
[S4] rule OMP001 shared-scalar-written-in-parallel-region (rule:OMP001)
OMP001 shared-scalar-written-in-parallel-region [warning]: A shared scalar is assigned inside an OpenMP parallel region: a data race.
Inside a parallel region every variable of the enclosing procedure is shared unless a clause
says otherwise, so a scalar temporary assigned in the loop body is written by all threads at
once: each thread may read another thread's value. The loop variables of the region's DO loops
are private automatically (Fortran rule), nothing else is. List the variable in `private` (or use
`default(none)` to make the compiler ask for every variable), or protect the write with
`critical`/`atomic` when the sharing is intended. Offload regions are no exception: in
`target teams distribute parallel do`, an unmapped scalar is firstprivate to the target region,
and that single copy is shared by all the threads of its teams. Compilers often privatise such
scalars as an optimisation, which hides the race until a compiler or a flag changes. Writing the
same value from every thread (`n = omp_get_num_threads()`) is a race too: use `single`.
Reported:
!$omp parallel do
do i = 1, n
tmp = a(i) * 2 ! tmp is shared: race
b(i) = tmp + 1
end do
Preferred:
!$omp parallel$ fobis review --deep
Using the running llama-server at http://127.0.0.1:41675
src/sweep.F90:16: OMP001 [warning] 'n' is shared and written by every thread (missing private?)
1 finding(s) in 13 file(s): OMP001 x1
Explain a rule with: fobis lint --explain <ID>
Reviewed 1 changed line(s) in 1 Fortran file(s).
Risk summary [llm]:
Risks:
- The variable 'n' is shared and written by every thread in the OpenMP parallel region, leading to a data race. [1][2]
Sources:
[1] diff of src/sweep.F90 (- removed, + added) (diff:src/sweep.F90)
[2] rule OMP001 shared-scalar-written-in-parallel-region (rule:OMP001)
[exit status 1]- The findings and the exit status are the linter's. The summary never removes a finding and never changes the exit status.
- Formats: the summary is added to text, markdown (a pull-request comment) and json (
risk); other formats warn. - A 7B model repeats itself and skips the checks. On a heat-equation change coder-7b gave three right risks, one of them twice, and no "to check" item.
Commit messages
git add -p # one logical change staged
fobis commit --show-facts # what FoBiS knows of the change; no model
fobis commit # print a message; nothing is committed
fobis commit --apply # print it, ask, then git commitBefore the model, FoBiS computes facts of the staged change:
- Scope candidates from the history: for each staged file, the scopes of the last 300 commits that touched it; with fewer than 3, those of its nearest directory that has them (stopping below
src/and the package), the file's own commits breaking the ties. A path new to the whole history is named after its directory undersrc/. - Fortran line kinds: each removed or added line is code, an OpenMP or OpenACC directive, or a comment or blank, from the staged and the HEAD versions lexed by FoBiS; the procedures whose code changed and the changed procedure headers.
- Type hints: only documentation, tests, CI or build files; only directives (
perf(acc), and the model becomes the first scope candidate); only comments; a changed procedure header (breaking, if that API is published).
$ fobis commit --show-facts
## Facts of the change (computed by FoBiS from git and the sources)
Scope candidates (use one of these, or an empty scope for a cross-cutting change):
- area: src/area.F90 is new to the history: named after its directory
Fortran sources (changed lines classified by the lexer; comment = comment or blank):
- src/area.F90: changed lines (removed + added): 3 code; procedures: areaA directive-only change in an HPC code (a real output, names changed: OpenACC async added to one loop):
## Facts of the change (computed by FoBiS from git and the sources)
Scope candidates (use one of these, or an empty scope for a cross-cutting change):
- acc: only these directives changed
- lib: 9 of 24 past commits touching src/lib/gpu/ used it
- gpu: 1 of 24 past commits touching src/lib/gpu/ used it; 1 of 2 past commits touching src/lib/gpu/gpu_ib_kernels.F90 used it
- lib/ib: 1 of 24 past commits touching src/lib/gpu/ used it; 1 of 2 past commits touching src/lib/gpu/gpu_ib_kernels.F90 used it
Type hint: perf(acc) (only OpenACC directive lines changed, no code)
Fortran sources (changed lines classified by the lexer; comment = comment or blank):
- src/lib/gpu/gpu_ib_kernels.F90: changed lines (removed + added): 2 accThe model receives the complete file list (git diff --cached --name-status, never truncated), the stat, the staged diff, the last 5 commits as a style reference, the branch and the facts. It answers a JSON object (type, scope, breaking, description, body, footers) and FoBiS writes the type(scope)!: description header itself, with the scope restricted to the candidates (kept as written, solver/gpu included) or empty:
$ fobis commit
[managed:coder-7b] Generating commit message…
Using the running llama-server at http://127.0.0.1:41675
refactor(area): add midpoint integration method
Add the midpoint integration method to the area program. This method provides an
additional way to compute the integral of sin(x) over [0, pi].
The scope is right, the type is wrong: a program printing a new result is a feat. That is why fobis commit commits nothing, and --apply asks Commit with this message? [y/N] before running git commit.
; the user configuration, ~/.config/fobis/config.ini
[llm]
; staged-diff characters sent to the model (default 12000)
max_diff_chars = 30000
; critique-and-rewrite passes after the draft (default 0)
refine_passes = 1- A long diff is cut at file boundaries: every file keeps its header, a cut hunk is marked
@@ [+N chars not shown] @@, files with no room are listed by name; the file list still names them all.--max-diff Nfor one run. --refine-passes N(1 to 3) gives a small model a critique-and-rewrite pass on a shallow first draft.- A server that rejects the schema (HTTP 400 or 422) gets the free-text request; the answer is cleaned (
<think>blocks removed, a repeated message dropped) and wrapped at 72 columns. --no-factsleaves the scope free.--show-facts --jsonprints the facts as JSON and exits 1 when nothing is staged.- Another server for one run:
--provider ollama|openai,--url,--model,--api-key-env NAME; the key is read only from the variable you name, and a hosted endpoint receives your staged diff. More in Setting up the AI.
Tutorial: commit messages.
Merge conflicts
git merge feature # stops on conflicts
fobis merge-assist --no-model # deterministic resolutions only
fobis merge-assist # a proposal per conflict, compiled before it is shown
fobis merge-assist --apply # hunk by hunk: accept, ours, theirs, both, edit, skip, quit
fobis merge-assist --apply --yes # only the safe ones; the rest stays in conflict
fobis merge-assist src/flux.F90 # only these conflicted files
fobis merge-assist --context # the model's input; no modelThe hunks come from the index, not from the file: base, ours and theirs are merged again with git merge-file --diff3, whatever your merge.conflictStyle and even after you edited the file. The operation is read from MERGE_HEAD, REBASE_HEAD, CHERRY_PICK_HEAD or REVERT_HEAD, and the sides are named as they are: during a rebase "ours" is the upstream and "theirs" your commit being replayed. Deterministic rules come first:
- High confidence: one side equals the base (take the other); both sides made the same change, or differ in whitespace only; both changed a
use ..., only:list (a three-way union: a name one side removed stays removed); both moved atag=pin in the fobos[dependencies](the higher version, flagged for review). - A draft, medium confidence: one side only appended or prepended lines, the other only edited the base lines (both changes). Drafts go to the model and are never accepted by
--yes.
$ fobis merge-assist --no-model
merge: ours = master (HEAD); theirs = midpoint
test/test_exactness.F90: conflict 1 of 1
base:
| real(R8P), parameter :: tolerance = 1.e-14_R8P !< Rounding allowance.
ours (master (HEAD)):
| real(R8P), parameter :: tolerance = 4.e-14_R8P !< Rounding allowance: a few ulps of the results, at -O3.
theirs (midpoint):
| real(R8P), parameter :: tolerance = 1.e-14_R8P !< Rounding allowance, checked on the midpoint rule too.
no proposal: resolve it by hand
Nothing was staged. Review with `git diff`, run `fobis test`, then `git add` the files yourself.The model gets the base, ours, theirs, the lines around the conflict, the commit messages of each side since the merge base (their intent), the procedures at the conflict and the draft. It answers a strategy (ours, theirs, both, custom), the text for custom, a rationale, a confidence, risks and citations; FoBiS builds the text of the other strategies itself, rejects a custom text with conflict markers, and keeps an uncited proposal at confidence low. The file, every proposal applied, is compiled syntax-only with the mode's compiler, -D/-I flags and module directory; a proposal that does not compile goes back with the errors, twice at most, then is shown with confidence low:
$ fobis merge-assist
merge: ours = master (HEAD); theirs = midpoint
[llm] test/test_exactness.F90: conflict 1/1
Using the running llama-server at http://127.0.0.1:41675
[llm] test/test_exactness.F90: conflict 1 does not compile, retry 1/2
[llm] test/test_exactness.F90: conflict 1 does not compile, retry 2/2
test/test_exactness.F90: conflict 1 of 1
base:
| real(R8P), parameter :: tolerance = 1.e-14_R8P !< Rounding allowance.
ours (master (HEAD)):
| real(R8P), parameter :: tolerance = 4.e-14_R8P !< Rounding allowance: a few ulps of the results, at -O3.
theirs (midpoint):
| real(R8P), parameter :: tolerance = 1.e-14_R8P !< Rounding allowance, checked on the midpoint rule too.
proposal [model, both, confidence low, does NOT compile]: Both sides changed the value of the tolerance parameter. The result keeps both changes, making the tolerance value 4.e-14_R8P, as this is the value from our side. The comment is updated to reflect the intent of both sides.
proposed:
| real(R8P), parameter :: tolerance = 4.e-14_R8P !< Rounding allowance: a few ulps of the results, at -O3.
| real(R8P), parameter :: tolerance = 1.e-14_R8P !< Rounding allowance, checked on the midpoint rule too.
note: does not compile: Symbol 'tolerance' at (1) already has basic type of REAL
cites: ours:test/test_exactness.F90#1, theirs:test/test_exactness.F90#1
Nothing was staged. Review with `git diff`, run `fobis test`, then `git add` the files yourself.
coder-7b on an NVIDIA A30 of a cluster node, reached through ssh; recorded in real time. The proposal declares the parameter twice: FoBiS compiles it, retries twice with the errors, then shows it marked does NOT compile with confidence low. Nothing is written.
The rationale describes the right resolution, the chosen strategy keeps both lines. The right answer here is an edit (e under --apply): keep 4.e-14_R8P and merge the comments.
- FoBiS never touches the git state: no
git add, no--continue. After--applythe files are still unmerged: review them withgit diff, runfobis test, thengit addthem yourself. - Your own edits are kept:
--applywrites only the conflict blocks still in the file and accepted by you. --yesaccepts the high-confidence rules and the model's proposals that compile, nothing else.--applywithout a terminal needs it (else exit 1).- Build once before merging. When the modules the file uses are not built yet, the compile check is impossible, not failed: the proposal is shown as
not compiled. - Semantic conflicts are not seen hunk by hunk. The report ends with the procedures changed by both sides: test them. Binary files and modify/delete conflicts are not handled.
- No model available: a warning, and the deterministic resolutions only.
--format jsonprints the report as one JSON object and asks nothing, so--applyneeds--yes(else exit 2).
What history says. Replayed on historical merges: on fpm (last 60 merges, 32 hunks) the 6 high-confidence resolutions all equalled the committed ones. On an HPC solver (26 hunks, 15 of them drafts of one pattern: an !$omp directive added after an !$acc one on one side, a variable added to the !$acc clause on the other), the human also added the variable to the new !$omp clause. Sent to coder-7b on an A30 (7.5 minutes), 3 of the 26 proposals equalled the human resolution, no draft was improved, 2 of 11 unresolved hunks were solved, and the model rated all 26 proposals high, right or wrong: its confidence carries no information. Trust the rules and the compile check; review every proposal.
Tutorial: merge conflicts.
The impact of a change on dependent projects
fobis ecosystem impact finds, in every project depending on a library, the places using what changed in its public interface (see Ecosystems). --deep adds a cited summary for the release train:
fobis ecosystem impact PENF --deep # what breaks, what to do
fobis ecosystem impact PENF --deep-context # the model's input; no model
fobis ecosystem impact PENF --format markdown --deepThe model gets one source per API change (before and after) and one per place a dependent uses a changed name, and answers claims of two kinds: what breaks (project, file, line) and what to do (which dependent to update, in which order, whether the change deserves a major version). It is asked only when something breaking changed. The summary is added to text, markdown and json, and the exit status stays the command's: 1 when a dependent is affected.
What the model sees
Every command prints the model's input, system prompt included, without running a model:
fobis build --explain-context # also: fobis run --explain-context, fobis test --explain-context
fobis ask --show-context -p src "..." # --json: the retrieved sections only
fobis testgen MODULE --context
fobis lint --deep-context # with --ask too
fobis review --deep-context
fobis merge-assist --context # the hunks the rules do not settle
fobis ecosystem impact PROJECT --deep-context
fobis commit --show-facts # the facts added to the commit promptThe same inputs give the same context. Read it when an answer surprises you: a wrong answer from a context missing the relevant code is a retrieval problem, not a model problem. To give an AI agent these facts directly, without the local model, use the MCP server: Setting up the AI.
What to trust
- Facts: the diagnoses of
--explain, the findings, the scope candidates, the deterministic merge resolutions, the compile checks. They are reproducible and can fail a CI job. - Checked outputs: a test that
testgenaccepted builds, runs and checks something; a merge proposal marked as compiling compiles. Neither is proven right. - Claims: advisory. A citation proves that the source exists and was given to the model, not that it supports the claim: read the cited source when the claim matters. The model's own confidence (merge) and verdicts (lint) carried no information in the measurements above.
- The model matters. coder-7b is the smallest that is useful; coder-0.5b and coder-1.5b failed every
testgenattempt, and 1.5b gave a wrong--explainfix and invented "about 10 seconds on a GPU" for a question no source answered. A larger model through another provider is a configuration line (Setting up the AI). - Speed: about 45 s per answer for coder-7b on 6 CPU cores; seconds on an NVIDIA A30 (3.4 s for the
askanswer above, 20 s for an accepted test). A persistent server saves the start-up of every command. - Overrides:
askandcommittake--providerand--model; the other commands use the user configuration.