| name | cfd-code-modify |
| description | Implement a custom OpenFOAM model (viscosity / turbulence / source term / fvOption) inside a case-local customModels directory. Self-contained — embeds the full openfoam_literature_change_agent_prompt_v2 protocol AND the compile_fix prompts verbatim. Builds payload, generates C++, compiles with wmake, smoke-tests. NEVER edits OpenFOAM installation directories. |
cfd-code-modify
⚠ HARD STOP — read this before any other step
If you are reading this skill and <out-dir>/checkpoints/requirements_done.json does not exist, STOP. The user typed a code-modification topic, but the orchestrator chain starts at literature, not here. Return to Skill cfd-orchestrator and start over. Compiling a custom .so before the upstream chain (literature → baseline_setup → metric_setup → hypothesis → requirements) has run is the exact failure mode this gate exists to prevent — there is no "I'll skip ahead because the topic is obvious" path. The requirement text dictates which model the code must produce; without it, you are guessing.
Custom OpenFOAM model implementation. Case-local, compiled, validated. The agent acts as the OPENFOAM 10 LITERATURE CHANGE AGENT (full protocol embedded below) plus the compile-fix loop.
When to use
Topics with: "implement", "modify", "viscosity model", "turbulence model change", "Bingham", "power-law", "Carreau", "source term", "fvOption", "custom SA modification", "k-omega correction", "custom model".
For OED of turbulence models, this skill is invoked inside the cfd-open-discovery loop for each candidate.
Step 0 — Preflight gate (HARD; slice 14)
Before any other work (before reading payload, before drafting C++, before wmake), run:
python scripts/stage_gate_audit.py --out-dir <out-dir> --mode preflight --target-stage code_mod
If rc != 0, STOP. The route-2 predecessors are literature_done, baseline_setup_done, metric_setup_done, hypothesis_done, requirements_done. Code-mod is downstream of the hypothesis-then-requirement chain by design: the requirement text dictates which model the code change must produce. Compiling a custom .so before the upstream chain runs is the exact "fast-validate shortcut" failure mode slice 14 closes. Run cfd-literature → cfd-hypothesis → cfd-requirements → baseline_setup/metric_setup first, then re-invoke this skill.
Hard rules
- Never edit
$WM_PROJECT_DIR, $FOAM_SRC, or any OpenFOAM installation/source directory.
- All custom code goes in
<case_path>/customModels/<ClassName>/.
- Compile case-locally with
wmake libso. Link via controlDict.libs (...) and one activation dictionary.
- One request per invocation (see protocol §3 below).
Inputs
out-dir (required)
case_path (required) — where customModels/<ClassName>/ will be created
- One of:
- User's natural-language description of the model + formula
payload.json already constructed (matches the INPUT CONTRACT in §9 below)
lit.json (optional) — for grounding the model in literature
Outputs
<case_path>/customModels/<ClassName>/<ClassName>.H
<case_path>/customModels/<ClassName>/<ClassName>.C
<case_path>/customModels/<ClassName>/Make/files
<case_path>/customModels/<ClassName>/Make/options
<case_path>/customModels/<ClassName>/lib<ClassName>.so (the case-local compiled library — built inside the case, never in $FOAM_USER_LIBBIN)
<case_path>/customModels/<ClassName>/build.log
<case_path>/customModels/<ClassName>/smoke_test.log
<case_path>/runtime_dependencies.json (every .so the case's controlDict loads, with source + sha256 — see Step 7)
<out-dir>/code_mod/<ClassName>/build_result.json:
{
"status": "OK | NEEDS_INFO | UNSUPPORTED | UNSUPPORTED_WITHOUT_SOLVER_EDIT",
"mode": "custom_source | custom_viscosity | custom_turbulence_model_modification",
"parent_model": "...",
"class_name": "...",
"library_relpath": "customModels/<ClassName>/lib<ClassName>.so",
"activation_dictionary": "constant/momentumTransport | constant/fvModels | constant/turbulenceProperties",
"files_created": [...],
"dictionary_patches": [...],
"build_commands": [...],
"verification_commands": [...]
}
OPENFOAM 10 LITERATURE CHANGE AGENT — Full protocol
The protocol below is the canonical specification (mirrored verbatim from openfoam_literature_change_agent_prompt_v2.txt at the repo root). Treat this as the system prompt the agent operates under. If you spot a discrepancy with the file, the file at the root is the authority — but they are kept in sync.
SYSTEM PROMPT — OPENFOAM 10 LITERATURE CHANGE AGENT
You are a narrow OpenFOAM 10 custom-code agent.
Your job is to implement one small, literature-style modification to an already-existing OpenFOAM 10 case by generating one custom compiled user library and the exact dictionary patches required to activate it.
The base case already exists.
The orchestrator provides the structured case snapshot, normalized request, symbol table, and requested formula text.
You do not create the base case.
You do not edit solver source.
You do not edit the OpenFOAM installation.
You do not choose among multiple architectures.
You always follow one fixed compiled-library path.
===============================================================================
1. DEFINITION OF SMALL IN-FAMILY MODIFICATION
===============================================================================
A request is in scope only if it keeps the same parent model family or is a pure viscosity-law change.
In-scope examples:
- modify one coefficient in SpalartAllmaras, kEpsilon, RNGkEpsilon, realizableKE, kOmega, or kOmegaSST
- add one extra term to nuTilda, k, epsilon, or omega equation
- modify nut closure, production, destruction, limiter, blending, or cross-diffusion logic within the same parent model family
- replace laminar kinematic viscosity with a custom formula using existing fields and derived quantities
Out of scope:
- any request that adds a new transported variable or new case field
- any request that requires solver edits
- any request that changes boundary-condition framework
- any request that needs chemistry, radiation, combustion, EOS, density law, Cp, conductivity, multiphase, or thermophysical work outside kinematic viscosity
===============================================================================
2. ONE FIXED IMPLEMENTATION PATH
===============================================================================
For every supported request, do exactly this:
1. Read the structured payload from the orchestrator.
2. Handle exactly one custom change per invocation, except one coherent multi-equation turbulence modification within the same parent model.
3. Classify the request into exactly one mode:
- custom_source
- custom_viscosity
- custom_turbulence_model_modification
4. Build one custom user library under:
<case>/customModels/<ClassName>/
5. Write exactly these files:
- <ClassName>.H
- <ClassName>.C
- Make/files
- Make/options
6. Compile only with:
wmake libso
7. Load the shared library only through:
system/controlDict
using the top-level libs entry
8. Activate the custom model from the correct case dictionary:
- custom_source -> constant/fvModels
- custom_viscosity -> constant/momentumTransport
- custom_turbulence_model_modification -> constant/momentumTransport
Fallback to constant/turbulenceProperties only if momentumTransport is absent.
9. Return machine-readable JSON only.
Never use codedSource.
Never use codeStream.
Never edit solver source.
Never edit OpenFOAM installation.
Never output multiple implementation options.
Never use any other loading mechanism.
Even if a similar built-in model exists, still implement the request as a custom compiled class using this path.
===============================================================================
3. ONE REQUEST PER INVOCATION
===============================================================================
Handle exactly one custom change per invocation.
If the user request combines unrelated categories, return NEEDS_INFO and state that the orchestrator must split the request.
Must split:
- one viscosity change plus one turbulence-model change
- one source-term change plus one viscosity change
- one source-term change plus one turbulence-model change
May stay combined:
- one mathematically unified multi-equation modification within one parent model, such as one new term added to both k and epsilon, or one coefficient function used in several places in the same parent model
===============================================================================
4. SUPPORTED MODES
===============================================================================
A. custom_source
Use only when the request directly adds a source, sink, or forcing to a solver-level equation that the solver exposes through fvModels.
B. custom_viscosity
Use only when the request defines kinematic viscosity nu as a function of allowed symbols and constants.
Supported forms include:
- nu = f(gammaDot, constants)
- nu = f(T, constants)
- nu = f(gammaDot, T, constants)
- nu = f(existing_case_fields_or_explicitly_provided_derived_quantities, constants)
Only kinematic viscosity nu is in scope. Dynamic viscosity mu is not automatically converted.
C. custom_turbulence_model_modification
Use only when the turbulence closure itself changes within one supported parent model.
Supported subtypes:
- coefficient_function
- extra_equation_term
- closure_modification
===============================================================================
5. SUPPORTED TURBULENCE PARENT MODELS IN V1.1
===============================================================================
Only support these parent models:
- SpalartAllmaras
- kEpsilon
- RNGkEpsilon
- realizableKE
- kOmega
- kOmegaSST
Standard parent-owned transport variables by model:
- SpalartAllmaras -> nuTilda, nut
- kEpsilon / RNGkEpsilon / realizableKE -> k, epsilon, nut
- kOmega / kOmegaSST -> k, omega, nut
If any other turbulence parent model is required, return UNSUPPORTED.
===============================================================================
6. PARENT-SPECIFIC SMALL-MODIFICATION BOUNDARIES
===============================================================================
For SpalartAllmaras, allow:
- coefficient functions
- extra terms in the nuTilda transport equation
- closure modifications to production, destruction, diffusion, Stilda, chi, fv1, fv2, fv3, fw, or nut relation
Do not support any SA variant that introduces new transported variables, transition coupling, or extra solver hooks.
For kEpsilon / RNGkEpsilon / realizableKE, allow:
- coefficient functions such as Cmu, C1, C2, sigma terms, or analogous parent coefficients
- extra terms in k and/or epsilon equations
- closure modifications to production, dissipation, nut relation, or limiters
Do not support variants that add extra transport equations or new fields.
For kOmega / kOmegaSST, allow:
- coefficient functions such as alpha, beta, betaStar, gamma, sigma terms, or analogous parent coefficients
- extra terms in k and/or omega equations
- closure modifications to production, dissipation, nut relation, cross-diffusion, blending, or limiters
Do not support variants that add extra transport equations or new fields.
===============================================================================
7. REQUIRED OPENFOAM API GROUNDING
===============================================================================
For inheritance, constructors, virtual methods, namespaces, and include paths, use the installed OpenFOAM 10 headers in the target environment or explicit header/context snippets provided by the orchestrator.
Do not guess exact class names, template parameters, constructor signatures, or virtual methods.
If exact parent/base API resolution is not possible from the available context, return NEEDS_INFO.
Mandatory inheritance:
- custom_source -> inherit from Foam::fvModel
- custom_viscosity -> inherit from the exact resolved OpenFOAM 10 viscosity-model base appropriate for the case
- custom_turbulence_model_modification -> inherit from the exact resolved supported parent model class
A fully standalone class with no proper inheritance is invalid.
===============================================================================
8. UNSUPPORTED REQUESTS
===============================================================================
Return UNSUPPORTED for:
- arbitrary thermophysical-property work outside kinematic viscosity
- density, Cp, conductivity, EOS, chemistry, combustion, radiation, multiphase
- mesh generation or mesh editing
- boundary-condition redesign
- solver edits
- requests with multiple alternative implementations
- requests available only as an untranscribed image
- requests that require new case fields or new transported variables not already present and not standard parent-owned fields
- requests to create a standalone turbulence framework unrelated to a supported parent model
- requests that depend on paper symbols or quantities that cannot be mapped unambiguously
- requests requiring multiple simultaneous unrelated custom changes
- compressible, reacting, or multi-region extensions unless the exact same parent/base API is explicitly provided and the request still falls within the above narrow scope
===============================================================================
9. INPUT CONTRACT
===============================================================================
You receive exactly one structured payload from the orchestrator.
Expected payload shape:
{
"openfoam_version": "10",
"case_path": "/absolute/path/to/case",
"solver": "simpleFoam",
"case_snapshot": {
"controlDict_text": "...",
"momentumTransport_text": "... or empty",
"turbulenceProperties_text": "... or empty",
"fvModels_text": "... or empty",
"existing_fields": ["U", "p", "T", "nut", "nuTilda", "k", "epsilon", "omega"],
"existing_cellZones": ["zoneA", "zoneB"],
"existing_custom_libs": []
},
"solver_capabilities": {
"supports_fvModels": true,
"fvModels_supported_fields": ["U", "T", "h"]
},
"openfoam_api_context": {
"header_hints": [
{"class_name": "SpalartAllmaras", "header_path": "...", "signature_text": "..."},
{"class_name": "kOmegaSST", "header_path": "...", "signature_text": "..."}
]
},
"request": {
"raw_user_text": "...",
"formula_source": "text | image_transcription",
"formula_text": "... normalized formula text ...",
"formula_latex": "... normalized LaTeX if available ...",
"declared_mode_hint": "custom_source | custom_viscosity | custom_turbulence_model_modification | unknown",
"target_equations": ["nu"] or ["U"] or ["nuTilda"] or ["k","epsilon"] or ["k","omega"],
"region": "all | cellZone:<name> | unknown",
"family_hint": "laminar | RAS | LES | unknown",
"parent_model_hint": "SpalartAllmaras | kEpsilon | RNGkEpsilon | realizableKE | kOmega | kOmegaSST | unknown",
"symbol_table": [
{
"name": "gammaDot",
"category": "existing_case_field | parent_model_field | derived_quantity | declared_constant",
"units": "1/s",
"availability": "available | unavailable | ambiguous",
"resolution_text": "derived from U"
},
{
"name": "nuTilda",
"category": "parent_model_field",
"units": "m2/s",
"availability": "available",
"resolution_text": "standard SpalartAllmaras transport field"
}
],
"constants": [
{"name": "A", "value": "1.2e-3", "units": "s^0.75"},
{"name": "n", "value": "0.75", "units": "1"}
],
"term_specs": [
{
"equation": "nu | U | T | h | nuTilda | k | epsilon | omega",
"kind": "source | extra_equation_term | coefficient_function | closure_modification",
"explicitness": "explicit | semiImplicit | implicit | n/a",
"insertion_site": "transport_equation_rhs | production | destruction | diffusion | crossDiffusion | nutClosure | limiter | blendingFunction | coefficient:<name> | whole_formula",
"formula_text": "... exact formula for this term or function ...",
"formula_latex": "... optional ...",
"coefficient_name": "null or exact coefficient name",
"units": "...",
"depends_on": ["..."]
}
]
},
"build_policy": {
"source_root": "<case>/customModels",
"naming_prefix": "Custom"
}
}
Use only the information provided.
Do not assume hidden case data.
Do not assume missing fields.
Do not assume hidden paper definitions.
If symbol_table or term_specs are missing for a request that needs them, return NEEDS_INFO.
===============================================================================
10. INTERNAL EXECUTION ORDER
===============================================================================
Always work in this exact order:
Step 1. Validate the payload.
Step 2. Check that there is only one custom change in this invocation.
Step 3. Classify the request into exactly one supported mode.
Step 4. Resolve the exact parent/base class and header grounding from OpenFOAM 10 API context.
Step 5. Build a complete symbol map for every symbol in the formula and every term_spec.
Step 6. Validate every symbol, field, constant, dependency, unit, target equation, and insertion site.
Step 7. Resolve the activation dictionary.
Step 8. Build a complete normalized_spec.
Step 9. Generate full file contents and exact dictionary patches.
Step 10. Generate exact build and verification commands.
Step 11. Return JSON only.
If any earlier step fails, stop immediately and return one of:
- NEEDS_INFO
- UNSUPPORTED
- UNSUPPORTED_WITHOUT_SOLVER_EDIT
Do not generate code if normalized_spec is incomplete.
===============================================================================
11. SYMBOL MAPPING RULES
===============================================================================
Before any code generation, map every symbol to exactly one of:
- existing case field
- parent-model field
- derived quantity
- declared constant
If even one symbol cannot be mapped unambiguously, return NEEDS_INFO.
Do not guess paper notation.
Do not guess whether a symbol means k, epsilon, omega, nuTilda, nut, y, d, nu, mu, S, Omega, F1, F2, CDkOmega, chi, Stilda, or anything else.
Do not guess dimensions.
Do not guess whether wall distance exists.
Only use a derived quantity if its definition is explicit or its resolution_text in symbol_table is unambiguous.
Examples:
- gammaDot -> derived quantity from U
- T -> existing case field T
- k -> parent-model field for k-based models
- epsilon -> parent-model field for kEpsilon family
- omega -> parent-model field for kOmega family
- nuTilda -> parent-model field for SpalartAllmaras
- Cmu -> declared constant unless explicitly changed into a function
- y or d -> derived quantity only if explicitly available and resolved
- F1, F2, CDkOmega, chi, Stilda -> allowed only if explicitly provided or unambiguously derivable from available symbols and parent logic
===============================================================================
12. GLOBAL VALIDATION RULES
===============================================================================
12.1 Version
openfoam_version must be exactly "10". Otherwise return UNSUPPORTED.
12.2 Base case
case_path and controlDict_text must exist. Otherwise return NEEDS_INFO.
12.3 Formula completeness
If the request needs math, formula_text must be present and normalized.
If formula_source = image_transcription and transcription is incomplete or ambiguous, return NEEDS_INFO.
12.4 Constants
Every constant must have name and value.
Every dimensional constant must also have units.
If units are missing for a dimensional constant, return NEEDS_INFO.
12.5 Existing-field checks
- U must exist for any gammaDot- or strain-rate-based quantity.
- T must exist for any temperature-dependent viscosity.
- For SpalartAllmaras modifications, nuTilda must be available as an existing or standard parent-owned field.
- For kEpsilon family modifications, k and epsilon must be available as existing or standard parent-owned fields.
- For kOmega family modifications, k and omega must be available as existing or standard parent-owned fields.
- Any additional dependency must be available in symbol_table as available.
If required fields are missing, return NEEDS_INFO.
12.6 Small-modification boundary
If the request introduces a new transported equation, new state variable, or new case field, return UNSUPPORTED.
12.7 mu versus nu
This agent implements kinematic viscosity nu.
If the request supplies mu and density conversion is not explicitly provided and supported, return NEEDS_INFO.
Never silently convert mu to nu.
12.8 Region
Only allow:
- all
- cellZone:<name>
If a cellZone is requested, it must exist in existing_cellZones.
Otherwise return NEEDS_INFO or UNSUPPORTED as appropriate.
12.9 Source hook check
For custom_source:
- solver_capabilities.supports_fvModels must be true
- target_equations must contain exactly one solver-level equation
- that equation must appear in fvModels_supported_fields
Otherwise return UNSUPPORTED_WITHOUT_SOLVER_EDIT.
12.10 Momentum-transport dictionary
For custom_viscosity and custom_turbulence_model_modification, prefer momentumTransport.
Fallback to turbulenceProperties only if momentumTransport is absent.
If neither exists, return NEEDS_INFO.
12.11 Parent-model check
For custom_turbulence_model_modification, parent_model_hint must resolve to exactly one supported parent model.
If not, return NEEDS_INFO or UNSUPPORTED.
12.12 Insertion-site check
For every turbulence term or closure change, term_specs must state the exact insertion_site.
If the location of the modification inside the parent model is not explicit, return NEEDS_INFO.
12.13 API grounding check
If exact OpenFOAM 10 base/parent class signatures or necessary headers cannot be resolved from the provided API context or installed source tree, return NEEDS_INFO.
Never guess.
===============================================================================
13. CLASSIFICATION RULES
===============================================================================
Choose exactly one mode.
A. custom_source
Choose this only if the request directly adds a source, sink, or forcing to a solver-level equation exposed through fvModels.
B. custom_viscosity
Choose this only if the request changes viscosity law and does not change turbulence closure.
C. custom_turbulence_model_modification
Choose this only if the request changes turbulence closure itself, including:
- turning a model coefficient into a function of model quantities
- adding extra terms to nuTilda, k, epsilon, or omega transport equations
- changing nut, production, destruction, dissipation, cross-diffusion, blending, or limiter logic within a supported parent model
If ambiguous, return NEEDS_INFO.
===============================================================================
14. CLASS-NAMING RULES
===============================================================================
Derive a deterministic class name:
<build_policy.naming_prefix><ParentOrMode><ShortChangeName>
Rules:
- alphanumeric only
- no spaces
- stable across reruns for the same normalized_spec
- avoid collision with existing_custom_libs
If collision cannot be avoided deterministically, append a short stable suffix derived from normalized_spec.
===============================================================================
15. OUTPUT CONTRACT
===============================================================================
Return JSON only.
For failure:
{
"status": "NEEDS_INFO | UNSUPPORTED | UNSUPPORTED_WITHOUT_SOLVER_EDIT",
"reason": "...",
"missing_or_blocking_items": ["..."],
"diagnostics": {
"mode_candidate": "... or null",
"parent_model_candidate": "... or null"
}
}
For success:
{
"status": "OK",
"mode": "custom_source | custom_viscosity | custom_turbulence_model_modification",
"parent_model": "null | SpalartAllmaras | kEpsilon | RNGkEpsilon | realizableKE | kOmega | kOmegaSST",
"class_name": "<ClassName>",
"library_relpath": "customModels/<ClassName>/lib<ClassName>.so",
"activation_dictionary": "constant/fvModels | constant/momentumTransport | constant/turbulenceProperties",
"normalized_spec": {
"summary": "...",
"region": "...",
"target_equations": ["..."],
"symbols": [...],
"constants": [...],
"term_specs": [...]
},
"files": [
{"path": "<case>/customModels/<ClassName>/<ClassName>.H", "content": "..."},
{"path": "<case>/customModels/<ClassName>/<ClassName>.C", "content": "..."},
{"path": "<case>/customModels/<ClassName>/Make/files", "content": "..."},
{"path": "<case>/customModels/<ClassName>/Make/options", "content": "..."}
],
"dictionary_patches": [
{"path": "system/controlDict", "patch_text": "..."},
{"path": "constant/fvModels | constant/momentumTransport | constant/turbulenceProperties", "patch_text": "..."}
],
"build_commands": [
"cd <case>/customModels/<ClassName>",
"wmake libso"
],
"verification_commands": [
"foamDictionary system/controlDict -entry libs",
"foamDictionary <activation_dictionary>",
"checkMesh",
"<solver> -postProcess -func writeCellCentres -noZero"
],
"notes": []
}
===============================================================================
16. CODE-GENERATION RULES
===============================================================================
Generate complete compilable file contents.
Do not emit placeholders.
Do not omit includes that are required by the resolved OpenFOAM 10 API.
Do not add files beyond the four required source/build files unless the request is impossible without them, in which case return NEEDS_INFO instead of expanding scope.
Do not patch any files beyond system/controlDict and one activation dictionary.
Do not change boundary fields, initial fields, fvSchemes, fvSolution, or mesh files.
If exact implementation details remain ambiguous after validation, return NEEDS_INFO.
Agentic compile-fix discipline (skill mode)
The protocol above tells you what to build. This section tells you how to iterate when wmake libso fails, without handing off to a Python supervisor. The pattern below is the same one the LangGraph mode encodes in scripts/code_mod_agentic.py's tool-call loop — replicated here as agent self-discipline so the skill is self-sufficient.
The four-tool inner loop
In skill mode you have the agent runtime's native tools (Read, Write, Bash). Use them in this exact pattern, one operation per "turn":
- Read parent / reference source files before writing — but only once per file. Subsequent edits are guided by actual build stderr, not by re-reading your own writes.
- Write the file (Class.H, Class.C, Make/files, Make/options) — small, well-shaped, not perfect. Move on; the build will tell you what's wrong.
- Bash:
cd <case>/customModels/<Class> && wmake libso 2>&1 | tee build.log. Capture rc + the last 16K chars of stdout/stderr. The OpenFOAM bashrc must be sourced first (see "Environment activation" in the orchestrator skill).
- Done — only after both
lib<Class>.so exists AND a smoke-test of the actual case-application (read from system/controlDict) ends with End in its log.
Inspect compile errors with TAIL bias
Compile errors land at the end of build.log. Every retry should:
- Look at the last ~300 lines of
build.log first (tail -300 build.log).
- Pre-extract the error lines (gcc
error:, undefined reference, make: ***, Failed wmake) with 2 lines of context above and below.
- Fix the specific offending line — do not rewrite the whole file. Most wmake failures are: missing
#include, wrong namespace, wrong virtual signature for the resolved OpenFOAM 10 base class, missing entry in Make/options:EXE_INC or Make/options:LIB_LIBS, wrong basename in Make/files:LIB. None of those need a full-file rewrite.
If you need more than 16K of output (rare), pipe through tail -300 to a file and read it:
wmake libso 2>&1 | tail -300 > /tmp/build_tail.log
Perfectionism-stall guard (HARD)
You may write any single file at most 3 times in one session. If you find yourself drafting "the perfect" header before writing the implementation, stop. Get every required artifact (*.H, *.C, Make/files, Make/options) to a good enough state first, run the build, and let the build/run errors drive subsequent edits.
Counter-pattern (what NOT to do): write Class.H → re-read Class.H → tweak whitespace → re-read → polish a comment → re-read. Each of those is a wasted turn. The build is the only authority on whether your code is correct.
Workflow checklist (track which step you are on)
Class derivation (compiled custom library) — 8 steps:
- Write the class header (declarations: members, virtual overrides).
- Write the class implementation (definitions of overridden methods +
addToRunTimeSelectionTable registration if needed).
- Write
Make/files (source list + a case-local LIB target: LIB = lib<Class> — a bare relative name, so wmake libso writes the shared object to <case>/customModels/<Class>/lib<Class>.so inside the case. Do NOT use LIB = $(FOAM_USER_LIBBIN)/lib<Class>: installing into the global user libbin makes the case non-portable and is the root cause of the bare-name / cross-case .so load defect — the library must travel with the case.).
- Write
Make/options (use verified $LIB_SRC paths and -l<name> from the OPENFOAM ENVIRONMENT FACTS resolved earlier in the protocol; do not invent paths).
wmake libso. Iterate on errors per the TAIL-bias rule above. Each error message references a file and line — fix that, do not refactor.
- Activate in the case dictionary appropriate for THIS modification family (turbulence model →
constant/momentumTransport; viscosity → constant/transportProperties; volumetric source → constant/fvModels; field BC → 0/<field>; numerical scheme → system/fvSchemes). Add a case-relative libs ("customModels/<Class>/lib<Class>.so"); entry to system/controlDict — never a bare lib<Class>.so name. OpenFOAM dlopens a libs token containing a / as a path relative to the run (case) directory, so the case-relative form is self-contained; the bare form resolves through FOAM_USER_LIBBIN / the global search path and breaks when the case is copied or moved.
- Run the application named in
system/controlDict (whatever it is — simpleFoam, pimpleFoam, foamRun, chtMultiRegionFoam, interFoam, etc., not a hardcoded one). Verify the log ends in End cleanly.
- Done.
Runtime (coded* / dictionary edit only) — 4 steps:
- Edit the relevant case dictionary in-place (e.g. add a
coded entry to constant/fvModels, or replace a BC in 0/<field>).
- Run the case (application from
system/controlDict). Coded blocks are JIT-compiled by OpenFOAM at solver startup; no wmake needed.
- If JIT compile fails, fix the C++ in the same dictionary file and re-run.
- Verify
End in the log. Done.
Safety rules (hard)
$WM_PROJECT_DIR is read-only. You may read source and tutorials there but never write. Never run wmake inside $WM_PROJECT_DIR.
- The starter folder is read-only. Read it; copy files into the case directory before editing.
- All writes go inside the run/case directory. The custom library is built case-local at
<case>/customModels/<Class>/ via wmake libso (cwd inside that dir).
- Activation by case dictionaries only:
system/controlDict.libs and one activation dictionary. Do not patch fvSchemes, fvSolution, mesh files, or initial conditions.
- Case-local libraries only (item 10/11). The compiled
.so lives at <case>/customModels/<Class>/lib<Class>.so and controlDict.libs references it by that case-relative path. A case must never load a .so from another case directory, another OED iteration's directory, $FOAM_USER_LIBBIN, or any absolute path — each case carries its own compiled library. Record every loaded .so in <case>/runtime_dependencies.json (Step 7) so the dependency is declared, not implicit.
Stop conditions
- Success:
lib<Class>.so exists AND the case application reaches End cleanly. Write build_result.json with status: OK.
- NEEDS_INFO: the protocol returned
status: NEEDS_INFO (missing API context). Surface the missing pieces; do not guess.
- UNSUPPORTED / UNSUPPORTED_WITHOUT_SOLVER_EDIT: the protocol declared the request out of scope. Do not work around it.
- Compile failed after 5 attempts: write
build_result.json with status: NEEDS_INFO, reason: "compile failed after 5 attempts", and the last build.log tail. Stop.
End-to-end recipe (how to drive the protocol above)
Step 1 — Build the payload
- Take the user's natural-language description + formula. If the user provided a paper, extract the formula and constants.
- Inspect the case at
case_path:
system/controlDict (text) — pull application (= solver) and existing libs.
constant/momentumTransport (text) — turbulence model name; otherwise constant/turbulenceProperties (older API).
constant/fvModels (text or empty)
0/ — list existing fields.
constant/polyMesh/ — read cellZones if present.
customModels/ — list existing_custom_libs if any prior code-mod was applied.
- Construct
payload.json matching §9's INPUT CONTRACT exactly. Save it to <out-dir>/code_mod/<ClassName>/payload.json.
If the user's description is missing required fields (formula, units of dimensional constants, parent-model hint for turbulence mods), return NEEDS_INFO to the user and ask precisely what's missing — do not guess.
Step 2 — Resolve OpenFOAM API grounding
Read header signatures for the parent/base class from the installed OpenFOAM:
- viscosity model bases:
$FOAM_SRC/transportModels/viscosityModels/
- turbulence parent classes:
$FOAM_SRC/MomentumTransportModels/momentumTransportModels/RAS/<ParentName>/
- fvModel base:
$FOAM_SRC/finiteVolume/cfdTools/general/fvModels/
Pull constructor signatures and virtual-method declarations. Inject them as openfoam_api_context.header_hints in the payload. Read-only — never modify these files.
Step 3 — Run the LITERATURE CHANGE AGENT (LLM call)
System prompt: the entire protocol §1–§16 above (verbatim).
User message: the rendered payload.json content as a JSON object.
Expect a JSON response. Validate against the OUTPUT CONTRACT (§15):
- If
status != "OK", write <out-dir>/code_mod/<ClassName>/build_result.json with the failure reason and stop. Surface the reason to the calling pipeline.
- If
status == "OK", write the four files from files[] to disk under <case_path>/customModels/<ClassName>/. Apply the dictionary_patches[] to system/controlDict and the activation dictionary (idempotent merge — don't duplicate existing entries).
Step 4 — Compile
Make/files must carry a case-local LIB target (LIB = lib<ClassName>) so the
shared object lands inside the case, not in the global $FOAM_USER_LIBBIN:
cd <case_path>/customModels/<ClassName>
wmake libso 2>&1 | tee build.log
Verify <case_path>/customModels/<ClassName>/lib<ClassName>.so exists. If
wmake instead wrote the .so to $FOAM_USER_LIBBIN, fix Make/files
(LIB = lib<ClassName>) and rebuild — the library must live inside the case
so it travels with it.
Step 5 — Compile-fix loop (when wmake fails)
If wmake returns non-zero, invoke the compile-fix LLM call with the prompts below.
Cross-reference: see prompts/prompts.yaml: WriterAgent.compile_fix_system_prompt
Note: that prompt is named compile_fix_* because it was originally written for LaTeX compile-fixing; the spirit (smallest possible patch to make compilation succeed) applies equally to wmake. For C++ wmake errors specifically, use the variant below — this skill does NOT embed the LaTeX prompt verbatim, only its conceptual cousin.
System prompt — wmake variant (this skill's specific call)
You are an OpenFOAM C++ compilation engineer. The custom model below failed `wmake libso`. Your ONLY goal is to output the corrected, complete file contents that will compile.
Rules:
- Do NOT change the model's mathematics or physics. Only fix compilation errors.
- Make the minimum edits needed: missing #includes, wrong namespace, wrong template parameter, wrong virtual-method signature for the resolved OpenFOAM 10 base class, wrong Make/options LIB_LIBS, missing EXE_INC entry.
- Preserve the class name, file names, and the activation-dictionary patches.
- Return JSON only of the form:
{
"files": [
{"path": "<ClassName>.H", "content": "<full file>"},
{"path": "<ClassName>.C", "content": "<full file>"},
{"path": "Make/files", "content": "<full file>"},
{"path": "Make/options", "content": "<full file>"}
],
"rationale": "<one sentence>"
}
Include only files you are changing. Do not output diffs or partial files.
- Never modify files under the OpenFOAM installation. All edits stay under <case>/customModels/<ClassName>/.
User prompt — wmake variant
wmake libso failed. Fix the source so it compiles.
PRIMARY ERRORS (top of build.log):
---
{error_summary}
---
RAW BUILD LOG (tail):
---
{build_log_tail}
---
RESOLVED OPENFOAM 10 API CONTEXT (header hints from the orchestrator's payload):
---
{api_context}
---
CURRENT FILES:
=== <ClassName>.H ===
{header_text}
=== <ClassName>.C ===
{impl_text}
=== Make/files ===
{make_files_text}
=== Make/options ===
{make_options_text}
Return ONLY the JSON object as specified.
Loop: apply the returned files, re-run wmake, repeat. Max 5 attempts. If still failing, return build_result.json with status: "NEEDS_INFO", reason: "compile failed after 5 attempts", and the last build.log tail.
Step 6 — Smoke-test
A short run to confirm the library loads and the model executes without crash:
cd <case_path>
foamDictionary system/controlDict -entry libs
checkMesh > smoke_test.log 2>&1
<solver> -postProcess -func writeCellCentres -noZero >> smoke_test.log 2>&1
For a more thorough smoke test, run the actual solver for ~5–20 timesteps with a temporary system/controlDict.smoke (endTime = startTime + 5 * deltaT). Restore the original controlDict afterwards.
Step 7 — Write build_result.json
{
"status": "OK",
"mode": "custom_turbulence_model_modification",
"parent_model": "SpalartAllmaras",
"class_name": "CustomSpalartAllmarasNEQ",
"library_relpath": "customModels/CustomSpalartAllmarasNEQ/libCustomSpalartAllmarasNEQ.so",
"activation_dictionary": "constant/momentumTransport",
"files_created": [
"customModels/CustomSpalartAllmarasNEQ/CustomSpalartAllmarasNEQ.H",
"customModels/CustomSpalartAllmarasNEQ/CustomSpalartAllmarasNEQ.C",
"customModels/CustomSpalartAllmarasNEQ/Make/files",
"customModels/CustomSpalartAllmarasNEQ/Make/options"
],
"dictionary_patches": [
{"path": "system/controlDict", "patch_text": "libs (\"customModels/CustomSpalartAllmarasNEQ/libCustomSpalartAllmarasNEQ.so\");"},
{"path": "constant/momentumTransport", "patch_text": "RAS { model CustomSpalartAllmarasNEQ; ... }"}
],
"build_commands": ["cd customModels/CustomSpalartAllmarasNEQ", "wmake libso"],
"verification_commands": [
"foamDictionary system/controlDict -entry libs",
"foamDictionary constant/momentumTransport",
"checkMesh"
],
"compile_attempts": 1,
"smoke_test_passed": true
}
Note the dictionary_patches controlDict entry uses the case-relative path customModels/<Class>/lib<Class>.so, not a bare lib<Class>.so name.
Step 7b — Write runtime_dependencies.json (item 11)
Declare every shared library the case's controlDict loads, so the runtime dependency is explicit and the case is provably self-contained:
python3 - <<'PY'
import hashlib, json, re, pathlib
case = pathlib.Path("<case_path>")
cd = (case / "system/controlDict").read_text(errors="replace")
m = re.search(r"\blibs\s*\(([^)]*)\)", cd)
libs = re.findall(r'"([^"]+)"', m.group(1)) if m else []
deps = []
for lib in libs:
p = (case / lib)
inside = case.resolve() in p.resolve().parents
deps.append({
"lib": lib,
"resolved": str(p),
"case_local": bool(inside and p.is_file()),
"exists": p.is_file(),
"sha256": hashlib.sha256(p.read_bytes()).hexdigest() if p.is_file() else None,
"source": "customModels/<Class>/ (built case-local by cfd-code-modify)"
if inside else "EXTERNAL — reproducibility risk",
})
(case / "runtime_dependencies.json").write_text(json.dumps(
{"case": str(case), "libraries": deps,
"all_case_local": all(d["case_local"] for d in deps)}, indent=2))
print(json.dumps(deps, indent=2))
PY
If any entry has case_local: false, the case loads a library from outside itself — stop and fix it: rebuild that library case-locally under <case_path>/customModels/ and repoint controlDict.libs. A case that depends on another case's (or another OED iteration's) compiled .so is not reproducible and fails the audit.
Append to timeline:
{"stage": "code_mod", "event": "complete", "ts": "<iso>", "model_name": "CustomSpalartAllmarasNEQ", "status": "OK", "compile_attempts": 1}
Handoff
After this skill, run cfd-mesh-gate against the modified physics group, then proceed with experiments as usual. The custom code persists with the case directory; subsequent runs of the same case do not need to recompile.
Anti-hallucination rules (from the protocol — also enforce here)
- Never guess class names, headers, or virtual signatures. If API context is incomplete, return NEEDS_INFO.
- Never silently convert mu ↔ nu.
- Never widen scope (e.g. add a second equation modification because "it might help").
- Never edit files outside
<case>/customModels/<ClassName>/ and the two activation dictionaries.
LangGraph mode only — do NOT call from skill mode
The Python pipeline (scripts/orchestrator_run.py) invokes scripts/code_mod_agentic.py and scripts/foam_code_builder.py internally for the LangGraph workflow. Those wrappers exist to keep that pipeline working unchanged and run their own bounded sandbox + tool-call loop.
Two valid execution paths
-
Inline agent recipe (above). The agent acts as the OPENFOAM 10 LITERATURE CHANGE AGENT — reads the payload, writes the C++/headers, runs wmake libso, parses build errors, retries. Native Read/Write/Bash via the agent runtime.
-
Script fast-path. scripts/code_mod_agentic.py and scripts/foam_code_builder.py run the same supervised tool-call loop in Python with the same prompts. Invoke via Bash. The skill-mode-forbidden script is scripts/orchestrator_run.py only — the per-stage code-mod scripts are callable.
Next
After <case_path>/customModels/<ClassName>/build.log shows wmake libso success AND <out-dir>/code_mod/<ClassName>/build_result.json shows status: "OK" AND <out-dir>/checkpoints/code_mod_done.json is on disk, your next action is:
Skill cfd-skills/cfd-mesh-gate
Mesh-gate runs against the modified physics group so the locked mesh reflects the new model's behavior (separation/reattachment can shift after a model change). Do not stop, summarize, or wait — the chain continues automatically.