A coding agent gets a small bug fix, asks where the tests live, reads half the repository, then asks where the tests live again two turns later. The problem is not necessarily that the model needs “better memory.” Often, the harness assembled poor context: it included too much, left out a required rule, or failed to record what it gave the model.
The responsibility needs a clear boundary. The model interprets context. The harness—the application coordinating the model, tools, instructions, and history—decides what enters that context. Task-aware loading, budgets, discards, and auditing belong there.
An AGENTS.md file helps keep stable conventions close to the code, but it cannot choose the files for a particular fix or guarantee that everything fits in the context window. It is an input to the loader, not the entire loader.
Project memory is not a repository dump
For a task in payments/refund.py, the agent may need:
- short project instructions;
- the file being changed and its related tests;
- contracts imported directly by that code;
- the PR description and diff.
It probably does not need old examples, documentation for another service, and every generated file. Adding everything “just in case” replaces an explicit decision with expensive noise. The whole directory showed up; comprehension, rather more modestly, did not.
A task-aware loader starts with two sets:
- required files defined by the task profile;
- focus paths taken from the PR or supplied by the operator.
For a security review or release, a missing required policy should stop the work. For initial exploration or drafting, optional files can be discarded when the budget runs out. Dependency discovery can happen later, on demand, instead of front-loading every file that might become useful.
Current tools express this design in different ways. The Aider repo map sends a ranked subset of signatures within a budget rather than the complete body of every project file. Cursor separates project rules from search tools in its Agent context. GitHub Copilot instructions can be scoped by path, as shown in its customization reference. There is no single universal discovery algorithm. There is, however, a shared responsibility: the harness assembles the input.
Size and relevance are separate decisions
Size is measurable. Relevance is a policy the team must state.
For size, record at least the UTF-8 byte count. If the model belongs to a family supported by the tokenizer used at runtime, record tokens too. tiktoken 0.14.0 provides o200k_base for compatible OpenAI models. That count should not be reused as though it were the official measure for every other provider.
In the example below, the byte budget works with only the Python 3.14.4 standard library. The token budget is optional and, when enabled, requires tiktoken==0.14.0. The program does not use a rough divide-by-four estimate as a gate: a convenient estimate remains an estimate, even when it wears a tie and attends the meeting.
For relevance, a simple, verifiable rule is usually better than a mysterious “smart score”:
- required files enter first;
- paths changed in the PR come next, in the order received;
- extra files enter only when a tool or an explicit relationship justifies the read;
- every decision gets a reason in the log.
This can later grow to include task-specific globs, direct imports, or a symbol map. The important part is preserving the explanation: required-policy, pr-path, dependency-of:X. Without it, you cannot tell whether the loader helped or merely rearranged chance.
An executable packer with budgets and an audit trail
The following script accepts a repository root, required files, and PR paths. It applies a byte limit and, when configured, a token limit at the same time. Every run appends one JSON line to an audit file.
#!/usr/bin/env python3
"""Pack explicit repository files as context for a coding agent."""
from __future__ import annotations
import argparse
import json
import sys
from dataclasses import dataclass, asdict
from pathlib import Path
@dataclass
class Decision:
path: str
role: str
bytes: int
tokens: int | None
action: str
reason: str
def parser() -> argparse.ArgumentParser:
p = argparse.ArgumentParser()
p.add_argument("root", type=Path)
p.add_argument("--pr-path", action="append", default=[], dest="pr_paths",
help="path changed in the PR; repeat this option")
p.add_argument("--required", action="append", default=[])
p.add_argument("--byte-budget", type=int, default=32_768)
p.add_argument("--token-budget", type=int)
p.add_argument("--mode", choices=("fail-closed", "compact"), required=True)
p.add_argument(
"--audit", type=Path, default=Path(".harness/context-audit.jsonl")
)
return p
def token_counter(enabled: bool):
if not enabled:
return None
try:
import tiktoken
except ImportError as exc:
raise SystemExit(
"--token-budget requires: python -m pip install tiktoken==0.14.0"
) from exc
encoding = tiktoken.get_encoding("o200k_base")
return lambda text: len(encoding.encode(text))
def safe_path(root: Path, relative: str) -> Path:
candidate = (root / relative).resolve()
if not candidate.is_relative_to(root):
raise ValueError(f"path outside repository: {relative}")
return candidate
def append_audit(path: Path, record: dict) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("a", encoding="utf-8") as stream:
stream.write(json.dumps(record, ensure_ascii=False) + "\n")
def main() -> int:
args = parser().parse_args()
root = args.root.resolve()
if not root.is_dir():
raise SystemExit(f"invalid root: {root}")
if args.byte_budget <= 0 or (
args.token_budget is not None and args.token_budget <= 0
):
raise SystemExit("budgets must be positive")
count_tokens = token_counter(args.token_budget is not None)
queue = [(path, "required") for path in args.required]
queue.extend((path, "pr-path") for path in args.pr_paths)
used_bytes = 0
used_tokens = 0
packed: list[str] = []
decisions: list[Decision] = []
seen: set[str] = set()
failure: str | None = None
for relative, role in queue:
if relative in seen:
continue
seen.add(relative)
try:
path = safe_path(root, relative)
except ValueError as exc:
failure = str(exc)
decisions.append(Decision(relative, role, 0, None, "reject", failure))
break
if not path.is_file():
reason = "required-missing" if role == "required" else "not-a-file"
decisions.append(Decision(relative, role, 0, None, "discard", reason))
if role == "required":
failure = f"required file is missing: {relative}"
break
continue
raw = path.read_bytes()
text = raw.decode("utf-8", errors="replace")
tokens = count_tokens(text) if count_tokens else None
exceeds_bytes = used_bytes + len(raw) > args.byte_budget
exceeds_tokens = (
args.token_budget is not None
and tokens is not None
and used_tokens + tokens > args.token_budget
)
if exceeds_bytes or exceeds_tokens:
limit = "bytes+tokens" if exceeds_bytes and exceeds_tokens else (
"bytes" if exceeds_bytes else "tokens"
)
decisions.append(
Decision(relative, role, len(raw), tokens, "discard", f"budget:{limit}")
)
if role == "required" or args.mode == "fail-closed":
failure = f"fail-closed: {relative} exceeds the {limit} budget"
break
continue
used_bytes += len(raw)
used_tokens += tokens or 0
packed.append(f"## {relative}\n{text}")
decisions.append(
Decision(relative, role, len(raw), tokens, "inject", "budget-ok")
)
record = {
"schema": 1,
"mode": args.mode,
"byte_budget": args.byte_budget,
"token_budget": args.token_budget,
"used_bytes": used_bytes,
"used_tokens": used_tokens if count_tokens else None,
"status": "failed" if failure else "packed",
"failure": failure,
"items": [asdict(item) for item in decisions],
}
audit_path = args.audit if args.audit.is_absolute() else root / args.audit
append_audit(audit_path, record)
if failure:
print(failure, file=sys.stderr)
return 2
sys.stdout.write("\n\n".join(packed))
return 0
if __name__ == "__main__":
raise SystemExit(main())
A byte-only run needs no package beyond the standard library:
python pack_context.py ./meu-repo \
--mode fail-closed \
--byte-budget 32768 \
--required AGENTS.md \
--required pyproject.toml \
--pr-path src/payments/refund.py \
--pr-path tests/payments/test_refund.py \
> context.md
To apply a token limit as well:
python -m pip install tiktoken==0.14.0
python pack_context.py ./meu-repo \
--mode compact \
--byte-budget 65536 \
--token-budget 8000 \
--required AGENTS.md \
--pr-path src/payments/refund.py \
--pr-path tests/payments/test_refund.py \
> context.md
The packer does not traverse the entire tree, follow a path outside the root, or hide a required-file failure. Even when it aborts, it records status: failed and the decision that caused the exit. The log therefore captures both the context delivered and the context refused.
In a real harness, the text on stdout goes to the model. The .harness/context-audit.jsonl file goes to internal telemetry and should remain outside the prompt. An audit trail that consumes its own context budget would be a small victory for bureaucracy over engineering.
Compaction and failure are policies for different content
“Compact” should not mean “cut anything until it fits.” The decision depends on the role of the content.
| Content | Recommended policy | Minimum record |
|---|---|---|
| Required instruction or policy | Fail if it is missing or does not fit | path, size, limit, and reason |
| PR file in review, security, or release | Fail if the profile classifies it as required | path, role, and decision |
| Optional source during exploration | Discard the lowest-priority item | path, ranking, and reason |
| Old conversation history | Compact or summarize | compacted range and mechanism |
| Raw search result already consumed | Keep only a reference or summary | source and preserved artifact |
OpenAI compaction handles the history of a long conversation. It is not project memory, and it does not prove that a discarded policy remained available. Repository sources and conversation history need separate treatment.
It is also worth separating team policy from the behavior of one product. In Codex, project_doc_max_bytes caps the project-document chain in bytes; the documented default is 32 KiB. According to the Codex AGENTS.md guide, discovery stops when the combined size reaches that limit. That is the product silently stopping its load process, not a token budget or the fail-closed policy in this example.
If our harness aborts because AGENTS.md or a required PR file does not fit, that guarantee is ours. It should appear in the code, exit status, and JSONL. A vendor setting should not receive credit for a safeguard it does not implement.
Where AGENTS.md fits
AGENTS.md is an open convention for giving an agent commands, project structure, and local guidance. It requires no fixed schema, and nearby files can specialize instructions for subprojects where the tool supports that behavior.
In the loader, it can be one short, required slice of context. The harness still owns the rest:
- selecting files by task and PR paths;
- measuring bytes and tokens with the correct counter;
- deciding whether to discard or stop;
- recording everything included, omitted, or missing.
This also avoids asking AGENTS.md to perform the work of CI, review, or access control. For that separation between guidance and enforcement, see repository gates before the first PR. Here, the file remains a supporting convention, not a memory database or search engine.
Measure packing without inventing a quality score
The packer's JSONL answers operational questions: how many bytes were injected, which files were discarded, and why the run failed. On its own, it does not show that the agent produced a better answer.
To observe the relationship between packing and outcomes, the harness can emit a second local event for each task:
{"schema":1,"repo":"checkout","task":"bugfix","pr":142,"injected_bytes":18420,"injected_tokens":4100,"discarded_files":2,"repeat_questions":1,"same_task_rereads":3,"review_rounds":2}
Those fields need operational definitions:
repeat_questions: questions about facts already present in files injected for that turn;same_task_rereads: additional reads of the same path during the task;review_rounds: review rounds recorded on the PR until merge or closure;injected_tokens: the compatible tokenizer's count, ornullwhen none exists.
Compare tasks from the same repository and of the same type. Keep the raw data and inspect cases such as many tokens with repeated questions or few tokens with frequent rereads. Do not turn local correlation into a percentage improvement, and do not use cost as a synonym for packing quality. Financial limits are a different control, covered in token budgets and circuit breakers.
Telemetry should guide profile changes: promote a file to required, remove a rule that is always loaded, or adjust the budget. Without a controlled experiment, it cannot justify saying that “8,000 tokens improve PRs.” It can tell you which 8,000 tokens entered. That is already considerably more useful.
Verification and limitations
Before connecting the packer to an agent, run four local checks:
- Run it with two small PR files and confirm that both appear in
context.mdand asinjectentries in the JSONL. - Set
--byte-budget 1with a required file; the process should exit with code 2 and recordstatus: failed. - Repeat with
--mode compactand a large optional path; the process should succeed and record a budget-relateddiscard. - Try
../outside-the-repo; the run should reject the path without reading the file.
The example has deliberate limits. It does not discover imports, summarize large files, calculate a semantic score, or reorder the paths received from the PR. tiktoken measures only compatible model families; other models need the counter specified by their provider. The JSONL may contain sensitive filenames, so it needs the same retention and access policy as other engineering metadata. Finally, “compacting” a file means discarding the whole file in this example. Automatic summarization would require provenance, evaluation, and a separate record of the summarized text.
When DevDojo would adopt this loader
DevDojo would adopt the loader when tasks already have reliable PR paths, required conventions are short, and logs show rereads or repeated questions about facts that should have been included. We would start in observation mode, then enable fail-closed behavior for review, security, and release while reserving compact for exploration and drafting.
We would step back if profiles demanded more maintenance than the problem warranted, legitimate tasks began failing because of generated paths, or the log showed that most useful reads happened outside the predicted set. The next move would not be to enlarge the prompt blindly. It would be to simplify the profiles or return to on-demand search.
The first test fits in one real PR: pass its changed files to the script, force an overflow in each mode, and inspect the audit line before starting the model. If the team can explain every injected item and every discard, project memory has stopped being a hope placed in the prompt and become an engineering decision.