COBOL modernisation with AI coding agents

AI agents can document, test and change COBOL quickly, and several can work on one code base at once. Their work meets in the copybooks, the program interfaces and the JCL. Give each agent its own worktree, track every copybook field to the programs that use it, and report every job change that gives code more authority. gitta Enterprise does all three.

gitta's golden pangolin typing at a vintage mainframe console

Why mainframe teams use agents

Large COBOL code bases hold decades of business rules, and fewer people know them well. IBM and others now sell AI tools that explain, document and change COBOL (IBM, watsonx Code Assistant for Z). Teams use agents to write documentation, add unit tests, widen fields, replace 3270 screens with services and split large programs into smaller modules.

Each of those jobs is easy to split into tasks, so it is tempting to run one agent per program. But the tasks are rarely independent. In COBOL, programs are coupled through shared record layouts. A merge tool that works line by line can't see that coupling.

A real code base to look at

IBM's general insurance application, GenApp, is a public CICS sample on GitHub (cicsdev/cics-genapp). The IBM Hursley CICS team built it to test new CICS releases, and IBM publishes it to show application modernisation. It is small enough to read, and its structure is like that of a production application. There are 31 COBOL programs and 13 copybooks in base/src, a BMS map for the 3270 screens, Db2 tables and a VSAM file.

The programs come in three layers. A presentation program calls a business logic program with EXEC CICS LINK. That program calls a data program, which reads and writes Db2 and VSAM. All three pass the same communication area, and its layout lives in one copybook, lgcmarea.cpy.

           03 CA-REQUEST-ID            PIC X(6).
           03 CA-RETURN-CODE           PIC 9(2).
           03 CA-CUSTOMER-NUM          PIC 9(10).
           03 CA-REQUEST-SPECIFIC      PIC X(32482).
      *    Fields used in INQ All and ADD customer
           03 CA-CUSTOMER-REQUEST REDEFINES CA-REQUEST-SPECIFIC.
              05 CA-FIRST-NAME         PIC X(10).
              05 CA-LAST-NAME          PIC X(20).
              05 CA-DOB                PIC X(10).
              05 CA-HOUSE-NAME         PIC X(20).
              05 CA-HOUSE-NUM          PIC X(4).
              05 CA-POSTCODE           PIC X(8).
              05 CA-NUM-POLICIES       PIC 9(3).
              ...

26 of the 31 programs bring this copybook in. 18 use COPY LGCMAREA. The 8 Db2 data programs use EXEC SQL INCLUDE LGCMAREA END-EXEC under their 01 DFHCOMMAREA. If you search for only one of the two forms, you miss part of the impact.

lgcmarea.cpy
  • LGACDB01
  • LGACDB02
  • LGACUS01
  • LGACVS01
  • LGAPDB01
  • LGAPOL01
  • LGAPVS01
  • LGASTAT1
  • LGDPDB01
  • LGDPOL01
  • LGDPVS01
  • LGICDB01
  • LGICUS01
  • LGICVS01
  • LGIPDB01
  • LGIPOL01
  • LGIPVS01
  • LGSETUP
  • LGSTSQ
  • LGTESTC1
  • LGTESTP1
  • LGTESTP2
  • LGTESTP3
  • LGTESTP4
  • LGUCDB01
  • LGUCUS01
  • LGUCVS01
  • LGUPDB01
  • LGUPOL01
  • LGUPVS01
  • LGWEBST5
  • COPY LGCMAREA, 18 programs
  • EXEC SQL INCLUDE LGCMAREA, 8 programs
  • Does not bring it in, 5 programs
  • Holds the literal length 225

The postcode also lives in lgpolicy.cpy, cntl/db2cre.jcl, ssmap.bms and cntl/adef121.jcl.

All 31 programs in GenApp's base/src. 26 of them bring in the copybook that holds CA-POSTCODE.

One small change, traced through GenApp

Say an agent gets the task to accept postcodes of up to ten characters. In the copybook this is one line. CA-POSTCODE PIC X(8) becomes PIC X(10). The table shows what else in GenApp depends on those eight bytes.

WhereWhat depends on the old layout
lgcmarea.cpyEvery field after CA-POSTCODE in the customer request moves two bytes. CA-POLICY-DATA has to shrink by two to keep the area the same size.
lgpolicy.cpyDB2-POSTCODE PIC X(8), the host variable for the Db2 row
cntl/db2cre.jclThe customer table's postcode CHAR(8) column
ssmap.bmsThe 3270 screen field ENT1HPC, LENGTH=08
cntl/adef121.jclThe VSAM customer file, RECORDSIZE(225 225)
Six programsThe number 225 as a literal length

People often miss the last row. 225 is the length of the customer number plus every customer field up to the email address, so it grows with the postcode. It appears as LENGTH(225) on EXEC CICS LINK in LGACDB01, LGUCDB01 and LGUPDB01, as CUSTOMER-RECORD-SIZE VALUE 0225 in LGACVS01 and LGUCVS01, and as F82 Value 225 in LGICVS01. None of these names the postcode. A literal holds no field name, so no tool can link it to the copybook by name. Search for these numbers before you change a layout.

grep -n -E "LENGTH\(225\)|VALUE 0?225|Value 225" base/src/*.cbl
grep -n -i -E "postcode" base/src/*.cpy base/src/*.bms base/cntl/*.jcl

Put this list in the task that you give the agent. An agent that knows all six places makes one consistent change. An agent that sees only the copybook can make a change that compiles, because the compiler does not check a literal length against a record.

Where two agents collide

Now run a second agent at the same time. Its task is to clean postcodes on the customer screen. It adds new code to LGTESTC1 that moves CA-POSTCODE into an eight-byte working field. LGTESTC1 already moves CA-POSTCODE to and from the map field and changes it to upper case, so the change fits there.

The work of each agent compiles and passes its own tests. The two changes are in different files, so Git merges them without a conflict. After the merge, the new code cuts every ten-character postcode down to eight. Nothing fails until a customer's address comes back wrong.

Two COBOL agents whose changes meet only after the mergeAgent A widens CA-POSTCODE in lgcmarea.cpy from PIC X(8) to PIC X(10). Agent B adds code to LGTESTC1 that moves CA-POSTCODE into an eight-byte field. gitta warns B and names LGCMAREA.CA-POSTCODE. Each change compiles and passes its own tests, and Git merges them without a conflict. After the merge, ten-character postcodes are cut to eight.mainagent Aagent BCA-POSTCODEPIC X(8) to X(10)gitta warns B, namingLGCMAREA.CA-POSTCODEEach compiles andpasses its testsgit merge, no conflictPostcodes cutto 8 charactersTwo COBOL agents whose changes meet only after the mergeAgent A widens CA-POSTCODE in lgcmarea.cpy from PIC X(8) to PIC X(10). Agent B adds code to LGTESTC1 that moves CA-POSTCODE into an eight-byte field. gitta warns B and names LGCMAREA.CA-POSTCODE. Each change compiles and passes its own tests, and Git merges them without a conflict. After the merge, ten-character postcodes are cut to eight.mainABA widens CA-POSTCODEfrom X(8) to X(10)gitta warns B, namingLGCMAREA.CA-POSTCODEEach change compiles andpasses its own testsgit merge, no conflictTen-character postcodesare cut to eight
Each change compiles and passes its own tests, and the two changes are in different files. Git merges them without a conflict. gitta warns the second agent while both still work.

We measured the same pattern on open source projects. We replayed 733 pairs of AI agent pull requests that were open at the same time in public GitHub repositories. They come from the dataset that Xu et al. studied (arXiv 2607.04697). Git reported a textual conflict in 24.4% of them. One of the pairs that Git merged cleanly no longer built. Its two changes were to one TypeScript object, about a hundred lines apart. In GenApp, 26 programs share one copybook.

How gitta follows a copybook field

gitta Enterprise reads COBOL programs, copybooks and JCL with its own reader. It handles COPY and REPLACING as a COBOL preprocessor does, and it follows the column rules of fixed format. It knows both ways GenApp brings in LGCMAREA, including EXEC SQL INCLUDE under a program's own 01 record. A field in such a copybook keeps the copybook's name, so the postcode is LGCMAREA.CA-POSTCODE.

gitta decides field by field what counts as a change of meaning. A change of PIC length, type, sign or decimals counts, and so do USAGE, OCCURS, VALUE and REDEFINES. A group changes when one of its fields changes, and 9(5) and 99999 are the same layout. A change that only adds comments, renumbers columns 1 to 6 or changes case is not a change of meaning.

gitta also tracks what the new code of each agent uses. This includes plain MOVEs, host variables in EXEC SQL, fields in EXEC CICS options such as COMMAREA and INTO, and program names in EXEC CICS LINK PROGRAM(...). In the case above, the second agent gets a warning at its next step. The warning names LGCMAREA.CA-POSTCODE, the agent that changed it, and the program whose new code uses it.

The agent then adapts its code. When you merge, gitta runs your build or tests on the combined code. If the tests fail, the merge stops before main moves.

gitta status                    # agents, changed files, warnings
gitta sync postcode-screen      # bring merged work into this agent's copy
git merge postcode-screen       # gitta checks the combined code during the merge
gitta watch                     # what gitta does, live in the terminal

Set it up for a COBOL repository

  1. Keep programs, copybooks and JCL in Git as UTF-8 text. With Git on z/OS, set the conversion for each folder in .gitattributes.
  2. Install gitta Enterprise on each machine where agents work and on the CI runners.
  3. If your copybooks have no file extension, list their folders in .gitta-copybooks. GenApp uses .cpy, so it needs none.
  4. List your own load libraries and sensitive datasets in .gitta-security.
  5. Run gitta init once. Add --gate "COMMAND" if the machine can run a compile step or unit tests.
  6. Commit .gitattributes, .gitta-copybooks and .gitta-security, so every developer, agent and CI runner uses the same settings.

Without a gate, gitta still warns the agents while they work. With a gate, it also runs your build on the combined work during every merge to main.

Get the code page right

An agent edits UTF-8 text in its worktree. On z/OS that text goes back through an EBCDIC conversion. IBM-1047 stores [ and ] at X'AD' and X'BD', while IBM-037 stores them at X'BA' and X'BB'. When Git converts a file with the wrong code page, nothing fails. The file still compiles, but [ is now Ý. Many sites hold files in both code pages in different libraries, so set the code page for each folder.

[in the agent's edit
X'AD'stored by IBM-1047
Ýread as IBM-037
[in the agent's edit
X'BA'stored by IBM-037
[read as IBM-037
The same character in two EBCDIC code pages. With the wrong code page the file still compiles.
# .gitattributes
cobol/** zos-working-tree-encoding=ibm-1047 git-encoding=utf-8
copy/**  zos-working-tree-encoding=ibm-1047 git-encoding=utf-8
rexx/**  zos-working-tree-encoding=ibm-037  git-encoding=utf-8

Some changes only alter characters that EBCDIC code pages store differently, or add replacement characters. gitta reports such a change as a weakened check, because these are the signs of a conversion with the wrong code page. gitta tells the agent at once, and the finding goes into the audit trail. gitta encoding shows the code page of every mainframe file and the files that have none.

Job changes that raise authority

Modernisation also changes JCL, and some JCL changes give a job more authority. gitta compares each changed member with the version that the agent started from. It reports a job that now runs shell or TSO commands in batch, uses a more privileged library or runs with raised authority. The report goes into the merge output and the audit trail, and the team policy decides whether it stops the merge. gitta does the same for REXX and CLIST members with new authorised commands, and for any assembler change that touches security interfaces. In COBOL, it reports SQL built at run time from variables, and passwords in the code. It does not flag static SQL.

Each finding is printed in the merge output and the CI log, and recorded in the audit trail. With weakened_checks set to block, the default in CI, such a merge stops until the agent changes the job.

Put the check in front of your build

Agents that only open pull requests get the same comparison in CI. With IBM Dependency Based Build and zAppBuild, run gitta pr-check on a Linux or macOS runner before the DBB step. Some pull requests do not merge with another open one, or their new code uses something that another open pull request changed. Such a pull request then stops before it uses mainframe build time.

# on the CI runner, before the DBB build step
gitta pr-check --pr "$FEATURE_REF" --base origin/main \
  --other "refs/pr/119=#119" --other "refs/pr/121=#121" --json > gitta.json
jq -e 'all(.[]; .merges and (.warnings | length == 0))' gitta.json
groovyz dbb-zappbuild/build.groovy --workspace "$WORKSPACE" --application app --impactBuild

The check reads the committed .gitta-copybooks and .gitta-security files, so CI sees what the agents see.

Where it runs

gitta runs where your agents and pipelines run, on developer machines and CI runners with macOS, Linux or Windows. With gitta Enterprise it can also run in zCX on z/OS, and your build and run process stays as it is. Every warning, check and merge goes into an audit trail that holds paths, symbols, commits and results, never source code.

Read the Mainframe guide to set up from zero, and COBOL, copybooks and JCL for everything gitta follows. For an overview for your team, see gitta for mainframes.