Can an AI agent work without whole codebase context?
Why whole codebase context determines whether AI agents find hidden dependencies, preserve legacy behavior, and survive production batch runs.

An AI agent that sees one file can make a locally convincing change and still damage the system. The failure is not usually bad syntax or an obviously wrong branch. It is a missing caller, a rule encoded in data, an overnight job with a different entry point, or an operational dependency that never appears in the file under review.
That is why context boundaries decide whether agentic coding works on legacy software. The useful unit of work is the behavior that crosses programs, scripts, database objects, schedules, files, and operator procedures. A file is merely one place where part of that behavior happens to be written.
I have seen tidy patches pass review because every visible line made sense. The failure arrived later, when a control file selected an old mode, a dynamically named program received a parameter in a position no one documented, or a batch rerun encountered records that the online path never produces. More careful line editing does not solve this class of problem. The agent must first discover the system that surrounds the line.
A file is not the unit of behavior
Legacy behavior rarely fits inside the file that appears to own it. A COBOL program may calculate an amount, but JCL chooses its input data set, a SORT step changes record order, a copybook fixes field offsets, and a later program interprets the output status byte. Reading only the calculation gives the agent a coherent story that is incomplete.
The same pattern appears outside mainframes. A VB6 form calls a COM component whose registration selects a version. A PHP controller includes a configuration file assembled by deployment scripts. An RPG program reads a data area and calls another program by a name stored in a field. A PL/SQL package relies on a trigger that changes the row after the package writes it. None of those dependencies needs to look like a normal import.
This distinction matters: source proximity is not behavioral ownership. Two functions in one file can be unrelated at runtime, while a JCL member and a COBOL paragraph in different libraries can form one indivisible production operation. If an agent ranks context mainly by directory distance, import statements, or matching identifiers, it will miss dependencies that operators consider obvious.
Before changing a file, ask what starts this behavior, what inputs select its branches, what persistent state it reads or writes, and who consumes the result. Those questions produce a system boundary. The boundary may include twelve files or twelve thousand. Its size follows the behavior, not the editor tab.
The practical consequence is blunt. A tool that cannot search and reason across the repository, build definitions, job control, schemas, and tests should not be allowed to make an autonomous legacy change. It can explain a paragraph or draft a unit test. It cannot establish the impact of a production edit.
Call sites hide outside the import graph
A call graph built from explicit function calls is useful, but it is not a system call graph. Legacy systems resolve work through strings, tables, schedulers, generated source, link steps, command files, and conventions. The missing edge is often the edge that matters.
IBM's ILE documentation makes a clean example. An IBM i application can use static procedure calls resolved when the program is bound, or dynamic program calls whose target name is resolved at runtime. A scan limited to source can often find the static reference. A dynamic call through an identifier may depend on a value loaded from a file, message, data area, or parameter. Searching for the callee name will not find a value that production constructs later.
The same blind spot shows up in mundane forms:
- A scheduler invokes a shell script that launches a program under an alias.
- A database table maps transaction codes to handler names.
- Reflection loads a class named in configuration.
- A spreadsheet macro calls a COM method through an object bound late.
- Generated JCL inserts a procedure name only after symbolic substitution.
The field routinely blurs static reachability and runtime dependency. Static reachability asks whether source text or compiled metadata exposes a path. Runtime dependency asks whether production can direct data or control through that path under some state. Treating the first as proof of the second produces an attractive, incomplete diagram.
An agent needs evidence for both. It should extract explicit references, then scan string literals, configuration keys, job steps, bind metadata, database dispatch tables, and production traces. When it cannot resolve a dynamic target, it should record an unresolved edge with the expression and its possible sources. Silence is not a valid resolution.
A basic repository probe can expose how quickly a supposedly isolated symbol escapes its module:
rg -n -uu 'CALC-TAX|CALCTAX|calc_tax' .
rg -n -uu 'EXEC PGM=|CALL +[A-Z0-9-]+|CALLP|PROCEDURE DIVISION' .
rg -n -uu 'handler|program_name|transaction_code' config db jobs src
Real output has paths and line numbers, such as jobs/NIGHTTAX.jcl:18://STEP20 EXEC PGM=CALCTAX. The artifact is not sophisticated, but it forces a reviewer to inspect callers beyond the open file. A serious agent should build a richer version of this map automatically and retain the evidence behind every edge.
Business rules travel through data
Many legacy rules are values, layouts, and sequences rather than named functions. An agent can preserve every visible conditional and still change the result by misunderstanding a packed decimal field, a sentinel date, a record type, a collating sequence, or the meaning of a blank.
Consider a nightly fee program. The code says that account class P receives a waiver. The class does not come directly from the account row. An earlier extract maps product codes through a control table, writes a class occupying one byte at offset 47, and sorts exceptions ahead of normal records. Operations replaces that table before month-end close. The rule that the reviewer thinks lives in one IF statement actually spans a table, a file layout, a sort contract, and an operating procedure.
This is where agents limited to files produce plausible transliterations. They convert the IF correctly, define a pleasant enum, and read a CSV into a modern service. Then they trim whitespace or parse an empty field as null. The original program compared a blank of fixed width, so a subset of accounts moves to a different branch. Unit tests derived from the rewritten function all pass because they repeat the new interpretation.
A system inventory must therefore include data semantics, not just schema names. For every boundary record or table, capture field positions, encodings, default values, null behavior, sign formats, rounding, ordering, duplicate handling, and the policy for invalid records. If a control value changes outside source control, record how it is promoted and which job reads it.
Do not assume a modern type is more correct than the old representation. Converting 9(7)V99 COMP-3 to a decimal type can be sensible, but only after you preserve scale, sign, rounding, overflow, and the behavior of malformed input. Replacing a date with six characters with a timestamp may remove ambiguity in the target design while silently inventing a century rule the source never had.
The agent also has to connect writers to readers. A field that looks unused in the producer may be positional padding required by a consumer three steps later. Deleting it can shift every subsequent field without causing a compile error. The safest representation of such a relationship is an explicit contract with sample bytes and parsed values, not a prose note that says the files are compatible.
The nightly batch is a different application
An online path and a batch path that share code are still different applications when they run under different inputs, identities, timing, and recovery rules. Passing an interactive test says little about a job that processes accumulated state after midnight.
IBM's z/OS documentation describes JCL as the place that tells the system where to find input, how to process it, and what to do with output. Its DD statements connect the names a program uses to actual data sets and specify details such as disposition and record format. That is not packaging around the application. It is executable context.
Take a change that adds a new status to an online order function. The request path writes H for held orders, displays the right message, and passes review. The nightly settlement job reads the same file. Its first step sorts only the older status values into the settlement input, while an error step copies everything else to a temporary data set with DISP=(NEW,PASS). A later step runs only when a condition on the return code matches. The new status skips settlement, lands in the temporary file, and disappears when the job ends normally. No source file in the reviewed service reveals that outcome.
The failure may wait for volume or calendar state. A daytime test uses one record and a clean database. The batch job encounters duplicates accumulated across retries, closes an accounting date before processing, and commits every few thousand records. A restart begins after the last checkpoint, not after the transaction the test expected. Correctness includes restart behavior because operators will eventually rerun a partially completed job.
For every scheduled flow, the agent should model five facts:
- The trigger, calendar, identity, and environment.
- The ordered steps and the conditions that skip or repeat them.
- The concrete inputs and outputs, including temporary data sets.
- Commit, checkpoint, retry, and rerun behavior.
- The evidence that operations uses to declare success.
A green process exit code may not be the success condition. Some shops accept defined warning codes, inspect record counts, or reconcile a control total in a later report. An agent that sees only source and unit tests will optimize for the wrong signal.
Configuration executes policy
Configuration deserves the same scrutiny as source because it selects behavior, supplies business values, and connects components at runtime. Calling it "just configuration" is a good way to approve a change without reviewing the rule that will actually run.
Legacy configuration rarely lives in one neat directory. It may be a JCL symbolic parameter, an IBM i data area, an INI file beside a desktop executable, a row maintained through an Access form, a registry value, an environment member, or a spreadsheet copied to a watched folder. Some values sit in source control. Others arrive through deployment tooling or an operator procedure. The agent must find both kinds and distinguish them.
Suppose a claims program chooses a pricing routine from a table. The source contains a harmless default, so the agent rewrites and tests that branch. Production has rows for each region that name four older routines, one of which accepts an extra argument through a shared buffer. The new service starts cleanly and handles the default test cases. The first claim for that region either calls no handler or calls the new handler with an incomplete contract. The missing call site was a row, not a line of code.
Treat configuration values according to their effect. A value that changes logging verbosity carries little behavioral risk. A value that chooses a program, changes a threshold, controls rounding, grants access, sets a file layout version, or alters commit frequency belongs in the impact map. The distinction should follow consequence, not file extension.
The agent should answer four questions for every value that selects behavior:
- Where is the value defined and who can change it?
- Which code reads it, and when does that read occur?
- What values have appeared in real environments?
- What happens when the value is blank, stale, unknown, or unavailable?
Defaults need particular suspicion. A fallback that makes a unit test convenient can conceal a failed configuration load in production. The source system may stop on a missing control member while a rewrite silently selects a default. Both implementations produce valid output for configured cases, but their failure contracts differ. Parity tests should include absent and malformed configuration, not only the expected values.
Capture a deployment snapshot alongside the source revision used for analysis. Hash or version the scheduler export, control tables, schema, and environment files when possible. If a reviewer cannot tell which configuration the agent assumed, the impact claim is not reproducible. An index of the whole codebase paired with unknown production configuration is still partial context, and the agent should say so plainly.
Build the map before asking for a patch
The agent should construct an impact map backed by evidence before it proposes code. This is not an architectural poster. It is a working set of nodes and edges tied to files, definitions, runtime observations, and unresolved questions.
Start with entry points: online routes, message consumers, scheduled jobs, command programs, stored procedures, desktop events, and operator commands. Then connect program calls, file reads and writes, table access, generated artifacts, configuration selection, and deployment bindings. Mark edges as static, configured, observed, or inferred. Those labels stop a guess from acquiring the authority of a fact.
I use a compact change record for each proposed edit:
{
"change": "add held order status H",
"entry_points": ["POST /orders/{id}/hold", "NIGHTSET STEP20"],
"writers": ["OrderStatus.bas", "HOLDORDR.cbl"],
"readers": ["SETTLE.cbl", "RECON.sql"],
"contracts": ["ORDER-REC copybook", "status_control table"],
"unresolved": ["Does restart input retain H records?"],
"required_evidence": ["online trace", "nightly replay", "reconciliation totals"]
}
That object is intentionally uncomfortable. It makes the second entry point and the unresolved restart question visible before anyone approves the patch. The exact field names do not matter. Requiring the agent to state affected entry points, readers, contracts, and missing evidence does.
The popular alternative is progressive disclosure: give the agent the target file, let it request related files, and stop when it says it has enough. This saves tokens and looks efficient in a demo. It is wrong for impact discovery because the first file shapes every later request. If the file contains no clue that a scheduler, control table, or generated procedure exists, the agent never asks for it.
Progressive disclosure is useful after discovery, when the agent needs detailed text for a known part of the map. Discovery itself needs indexing across the repository and analysis across languages. The agent may focus its reasoning, but the search space cannot begin at the file boundary.
The map also gives humans a better review surface. A reviewer can challenge a missing edge, request evidence for an inference, or add an operator procedure that the repository does not contain. Reviewing only the final diff asks the human to reconstruct this map mentally, which is exactly the work the agent was supposed to help with.
Context needs layers, not one enormous prompt
Context for the whole system does not mean pasting a million lines into one prompt. It means the agent can retrieve and reason over the complete system through representations suited to different questions, while preserving a path back to source evidence.
One layer holds the inventory: languages, build units, schemas, jobs, entry points, files, procedures, and configuration sources. Another holds relationships such as calls, reads, writes, schedules, includes, binds, and generates. A semantic layer records contracts and likely responsibilities. Runtime evidence adds traces, examples shaped like production, job logs, and observed dispatch targets.
These layers serve different queries. To rename a field, the agent needs layout definitions and every reader. To change a calculation, it needs callers, data provenance, rounding rules, and comparison outputs. To split a batch job, it needs step conditions, temporary resource lifetimes, checkpoint behavior, and operator recovery. No fixed chunking strategy answers all three.
Summaries help, but a summary is a lossy cache. It can tell the agent that a program calculates fees, yet omit the branch that applies only to reversed transactions during close. Every summarized claim should retain citations to concrete source spans or runtime records. When a change touches the claim, the agent must reopen those sources rather than reasoning from the summary alone.
Context freshness matters too. Generated copybooks, database definitions, scheduler exports, and deployed configuration can drift from the main repository. The agent should show which snapshot it analyzed. Mixing a current COBOL file with last quarter's JCL export creates a synthetic system that has never run anywhere.
There is also a hard limit to what repository analysis can know. An operator may edit a control member during an incident. A partner may send undocumented record variants. A desktop application may depend on machine registration. The correct response is to name the gap and demand runtime evidence, not to fill it with a confident assumption.
This layered approach controls cost without sacrificing scope. Broad indexes identify candidates cheaply. Focused retrieval supplies exact source when the agent reasons about an edge. Runtime replay tests the resulting behavior. The system remains available to the agent even though every token is not active at once.
Parity belongs at the behavior boundary
Tests written against the new implementation alone prove internal consistency, not preservation. A rewrite can pass a complete new unit suite while disagreeing with production on the cases that the rewrite misunderstood.
The strongest practical oracle is the old system itself. Capture representative inputs at its real boundaries, run them through both implementations, normalize only values that are intentionally nondeterministic, and compare observable outputs. Those outputs can include response bodies, database changes, emitted files, messages, return codes, control totals, and logs that operations treats as contractual.
A parity case should retain enough detail to reproduce a mismatch:
{
"case_id": "nightly-held-order-restart",
"entry_point": "NIGHTSET",
"input_refs": ["orders.dat#sha256:...", "status_control#2026-08-01"],
"source": {"rc": 4, "settled": 812, "held": 17, "control_total": "194033.22"},
"target": {"rc": 0, "settled": 829, "held": 0, "control_total": "196801.04"},
"comparison": "mismatch"
}
The example shows why matching exit codes is weak. The source considers return code 4 acceptable and preserves held records. The target returns zero after settling them. A conventional health check prefers the incorrect result.
Recorded traffic needs discipline. Remove or protect sensitive values, preserve ordering when it affects behavior, and include failures and retries rather than sampling only successful requests. Batch fixtures need volume categories shaped like production, boundary dates, duplicates, malformed records, and restart points. You do not need every production record, but you do need every known behavior class.
Parity does not forbid architectural change. It separates intended change from accidental change. You can replace sequential file processing with database transactions, split a monolith into services, or move a numeric kernel to Rust. The comparison tells you where externally visible behavior moved. A human can then approve a deliberate difference with a reason instead of discovering it in reconciliation.
CodeHero uses this boundary deliberately: its platform reads the whole codebase across languages, then checks the rewritten system with a parity test rig against recorded production traffic. That mechanism matters more than whether generated code looks idiomatic in a pull request.
Review the impact claim, not just the diff
Agentic review should evaluate the agent's claim about system impact. The diff remains necessary, but it is the last artifact in a chain that begins with discovery and ends with behavioral evidence.
A useful review packet contains the requested behavior, the affected entry points, changed contracts, discovered consumers, unresolved edges, test evidence, and any accepted differences. Keep each statement traceable. If the agent says a field has one reader, the reviewer should be able to open the search or trace that supports the count.
This changes the approval conversation. Instead of asking whether the new function looks reasonable, the reviewer asks why RECON.sql is unaffected, whether the nightly rerun was replayed, and which evidence covers blank status values. Those questions are harder for an agent to bluff and easier for an experienced engineer to answer decisively.
Watch for three warning signs in an agent's work. First, it describes dependencies without distinguishing observed facts from inference. Second, its tests originate entirely from the new design rather than captured legacy behavior. Third, it uses phrases such as "all callers" without exposing the search boundary. Each sign means the context claim is stronger than the evidence.
Reviewers also need a stop rule. Block the change when an unresolved edge can alter money, permissions, regulated records, irreversible output, or recovery behavior. For a display defect with low risk, a bounded inference may be acceptable. Context completeness is not binary; the required evidence rises with the consequence of being wrong.
Do not measure an agent by accepted lines or pull request speed. Those metrics reward local plausibility. Measure how often impact predictions match observed effects, how many parity mismatches escape, and whether reruns and operational controls behave as expected. Even without a formal score, these are the questions that separate typing assistance from engineering work.
Scope across the whole system changes the economics
Discovery across the repository costs more before the first edit, and that is precisely why it saves time on legacy work. The expensive failures happen after a cheap local change has crossed an unseen boundary: during close, during a batch window, in reconciliation, or after the one operator who knew the restart sequence has left.
The cost is not just repair. A bad modernization teaches the organization to distrust the target system. Teams keep the legacy application running as a shadow, compare results manually, and refuse later changes. The nominal rewrite finishes while the operational migration never does.
Analysis of the whole system also prevents a subtler waste: transliterating obsolete architecture because the agent cannot see why it exists. If it sees only a program, the safest apparent move is to reproduce its paragraphs in a new language. If it sees the callers, data contracts, job flow, and behavior at the boundary, it can preserve the required outcomes while replacing accidental structure.
That is the standard we apply when CodeHero rewrites legacy systems into Go, Rust, TypeScript, and Postgres in under 30 days. The short schedule is credible only when discovery, reasoning across languages, and parity checking operate on the system as a whole instead of waiting for humans to feed files one by one.
An AI agent does not need mystical understanding of every historical decision. It needs an honest boundary, evidence for the edges inside it, and tests at the points where behavior leaves it. If your coding agent cannot tell you which nightly job reads the record it wants to change, it has not earned the right to change the record.
FAQ
Why is context at file level risky for legacy code?
A legacy behavior often crosses source files, job definitions, schemas, control tables, and operator procedures. Context at file level hides those edges, so an agent can produce code that looks correct while changing the system elsewhere.
Does whole codebase context mean putting every file in one prompt?
No. It means indexing the complete system and retrieving the right source, relationship, contract, and runtime evidence for each question. Every summary should still point back to concrete evidence.
Can a static call graph find every legacy dependency?
No. Dynamic program names, scheduler entries, database dispatch tables, generated artifacts, and deployment bindings can all create runtime edges. Keep unresolved dynamic edges visible until traces or configuration resolve them.
Why do AI coding changes fail in nightly batch jobs?
Batch jobs use different entry points, accumulated inputs, identities, step conditions, and restart rules. An online test rarely exercises that combination, even when both paths share business code.
What should an AI agent inspect before changing a legacy program?
It should identify entry points, callers, readers and writers, data contracts, scheduled flows, configuration, recovery behavior, and observable outputs. It should also state which dependencies remain inferred or unresolved.
How do you test a legacy rewrite generated by AI?
Replay representative inputs shaped like production through the source and target, then compare all observable effects. Include database changes, files, messages, return codes, control totals, failures, and restart cases where they matter.
Is behavior parity the same as copying the old architecture?
No. Parity preserves approved observable behavior while leaving room to change internal design. When the target differs, the test rig makes that difference explicit so a human can approve or reject it.
What evidence should accompany a patch generated by an agent?
Require an impact map, affected entry points, changed contracts, discovered consumers, unresolved edges, and test results. Claims such as "all callers" should expose the search scope that supports them.
When should a reviewer block a legacy change generated by AI?
Block it when an unresolved dependency can change money, access, regulated records, irreversible output, or recovery. Edits with lower risk can tolerate bounded uncertainty, but the agent must state it plainly.
Can AI modernize a legacy system with a million lines?
Scale alone is not the deciding factor. The agent needs discovery across the repository, dependency analysis across languages, controlled retrieval, and parity evidence; otherwise a larger system only creates more places for hidden behavior to live.