DiegoVallejo

COBOL to C#: The Skill Is the Process, Not the Converter

cobolcsharpaisoftware engineering
A hand-drawn COBOL dinosaur facing an incoming C# meteor

Generating C# from COBOL is not the hard part. Knowing whether the C# still does the same thing is.

I started this project with a three-step plan: document the COBOL, translate it to C#, then integrate and test it. The first version ended up as a working port of a specific CardDemo batch slice. It was a useful experiment. It also missed the point.

If I handed that repository to someone with their own COBOL programs, they would get my example, not a repeatable way to understand their system. So I removed the demo and kept the procedure. COBOL to C# is now an installable Agent Skill: instructions, references, templates, and checklists that a file-capable coding agent applies inside your project. No bundled COBOL application, generated port, runtime library, or mystery converter.

The dinosaur and C# meteor at the top are a joke. The output contract underneath is not.

Why analysis has to come first

In a typical rewrite, the old application gives you something close to a specification. In COBOL, a lot of the specification lives outside the program you are reading. A copybook defines record fields; JCL supplies job parameters and file assignments; the bytes in an input file may disagree with what its FD declares. Even the way a file is written can change the byte stream you need to reproduce.

The skill starts by inventorying programs, copybooks, JCL, called modules, files, and external dependencies. For each program it asks for a Markdown analysis, Mermaid dependency/control-flow diagrams, an error-path and file contract, and a JSON map of fields with widths, offsets, PIC/USAGE, signs, and scales. Unknowns stay unknown until the source or runtime settles them. The analysis procedure makes that a gate before verified translation.

Here is why I care about the map. One course dataset had physical records of 170 bytes, while a program declared a 174-byte FD. If you read that file in 174-byte chunks, the boundary shifts four bytes on each read. A translation that assumes each READ means one physical record can look plausible and still be wrong. The file-size calculation flags the discrepancy; it does not tell you the exact number of application records, because READ status handling matters too.

Encoding has a similar trap. A fixed record can mix EBCDIC text with COMP-3 packed numbers. Running the whole file through a text converter will corrupt the packed bytes. The skill tells the agent to inspect the actual code page and convert only verified text ranges, preserving packed, binary, and opaque ranges. That is less exciting than asking an LLM to "migrate the program." It is also the difference between a field map and guesswork.

The three-phase COBOL to C# workflowDocument the original source and byte contracts, translate one paragraph at a time while building and comparing, then integrate and report verified, unverified, blocked, or failed cases.01 / DOCUMENTSource, copybooks, JCL, input filesDiagrams + byte maps + open questions02 / TRANSLATEParagraphs to C# against the mapBuild first; compare as you go03 / INTEGRATE + VALIDATEWire jobs, adapters, and regression testsIndependent runs, artifacts, verdictsOriginal source and data stay untouched
A workflow, not a one-click COBOL compiler.

Translate one behavior at a time

After the map, the agent builds ordinary C# files in a .NET project by default. If I explicitly ask for .csx, I also have to name a compatible script host; the skill does not assume one. It translates a paragraph or cohesive block at a time, with a reference back to the original. Project-local helpers are fine when the program needs them, but this repository does not ship a compatibility runtime for every COBOL dialect.

The rule is to preserve observable behavior, including quirks. A case-sensitive comparison that never matches an input is part of what the old program actually does. Correcting it may be desirable, but it is a separate business change, not something a migration should smuggle in.

I learned the value of a build gate on a course program: a hand-counted report header did not match its declared byte layout, and a C# class was declared static while containing instance members. The compiler caught the second problem immediately. Building each coherent increment before comparing behavior makes sure the harness is not accidentally testing an old binary. Headers and FILLER groups should be assembled field by field at mapped byte offsets, not as heroic strings of spaces. The translation checklist spells out that hygiene.

Parity starts here, in phase two. Waiting until the entire C# solution is integrated makes every mismatch harder to locate.

Integrate after the contracts are stable

Phase three wires entry points, job parameters, IO adapters, and dependencies into a runnable project. This is where I can consolidate genuinely shared behavior, once its contracts are known. Optimization comes after a verified baseline, followed by another run of the relevant comparisons and regression tests.

I do not want a compatibility migration and a business-rule redesign in the same step. If a cleanup changes output, I want to know which change did it.

What I mean by "verified"

For a named test case, run the original COBOL and the C# target on independent disposable copies of the same input. Capture the exit code, stdout, stderr, and every declared output file. Compare bytes, not trimmed strings. If a date field is volatile, record a mask with its artifact, byte offset, length, source reference, and validation rule; validate the raw field before applying the mask to comparison copies.

There are four outcomes, and they are not synonyms:

Verdict What it means
Verified for named cases Required artifacts match the stated oracle for the specified cases.
Unverified Behavior or evidence is insufficient, including documented undefined-data cases.
Blocked A source compile defect, unavailable runtime, or missing dependency prevents a required step.
Failed A defined-input mismatch, build error, or other concrete failure occurred.

In a full-corpus run over 24 programs from the Open Mainframe Project's COBOL Programming Course #2, the report counted 18 verified for named cases, 1 unverified, 5 blocked, and 0 failed. The unverified case involved a misaligned FD and undefined data; the blocked programs had source compile defects. Neither category was quietly removed from the denominator. That count describes that run, those cases, and its GnuCOBOL 3.1.2 oracle, not an independent claim of IBM mainframe equivalence or a guarantee for other projects.

Even a working local oracle has boundaries. Some course sources needed a missing PROGRAM-ID terminator to compile with GnuCOBOL. For that kind of authorized adaptation, the skill uses a scratch-patched copy, records the diff and both hashes, and labels the oracle as patched source. The original stays untouched. A patched-source match does not become a claim that the unmodified source ran successfully. My first assumptions about COMP-3 sign handling and WRITE AFTER ADVANCING newlines also failed local probes; the runtime notes record the version and options instead of turning those observations into universal COBOL rules.

That is the part I want an agent to be stubborn about. A dotnet build pass is not parity. A matching stdout line is not parity if the output file differs. "All pass" is not a verdict when five programs never compiled. The harness patterns and validation template make the evidence inspectable.

Install it in your own project

From the root of the COBOL project, run:

npx skills add DiegoVallejoDev/cobol-to-csharp --skill cobol-to-csharp

Select your agent when prompted. npx gets the skills installer from npm; it fetches this skill from GitHub. I do not publish the skill itself to npm. You can also install it manually or ask any file-capable agent to read its SKILL.md in a local checkout.

Then give the agent a scoped request. For example:

Use the cobol-to-csharp skill on the programs in source/. Include their copybooks, JCL, and input files. Start with one dependency cluster. Put all generated work under a new migration-output/ folder; do not edit source/ or data/. Document first, translate to C#, then compare with the original runtime where available. Report what you could not verify.

The result is not a new application inside the skill repository. It is a set of artifacts in your project. A full run might leave this shape (the exact contents depend on the programs and which phases can run):

your-project/
  source/                         original COBOL, unchanged
  data/                           original inputs, unchanged
  migration-output/
    README.md                     how to build, run, and inspect results
    docs/                         per-program analysis + Mermaid diagrams
    maps/                         field offsets, PIC and byte widths
    csharp/                       translated source and project-local adapters
    tests/                        contract and regression checks
    inputs/                       disposable converted/staged inputs
    build/oracle-src/              scratch source copies, if needed
    build/oracle-bin/              local oracle binaries, if available
    run/<CASE>/{oracle,target}/    separate captured run artifacts
    harness/                       comparison scripts and masks
    original-hashes.json          before/after source and input hashes
    validation-report.md           commands, cases, verdicts, and gaps

Analysis-only requests should stop after the docs and maps; a blocked runtime will not magically produce oracle binaries. All generated files, including temporary conversions and test runs, stay inside the output root. The output checklist is the handoff audit.

I am not trying to make an agent a mainframe emulator. I want it to make the migration legible: what was found, what got translated, what was compared, and what is still unknown. That is a more useful deliverable than another impressive demo that only works on the code I bundled with it.

Diego Vallejo, October 2026