In the last post, we gave the loop a CLI. Now the user can give the agent a task and point it at a workspace.
But a workspace is still only a folder. If the user says "fix this bug," the agent has to orient itself before it can act well.
- What language is this?
- Where is the source code?
- Where are the tests?
- How do I run them?
- Are there docs?
- Are there architecture decisions?
- What should I ignore?
One can hope the model will figure all of that out every time. Sometimes it will. Sometimes it will burn half the context window wandering around the repository, before doing any actual work.
What Is Context?
We often say "the agent needs context" as if context were a vague cloud around the model. In a harness, context is more concrete. It is text that enters the model request.
That means project context should be treated like a tool output. It should be created by code, stored somewhere the user can inspect, and loaded into the conversation only when it is useful.
repository files
-> context builder
-> .loop/PROJECT.md
-> /context load
-> model messages
This keeps the hidden prompt state less hidden. The user can open the context file and see what the agent is being told about the project.
Context Is Selected Input
The model does not "know the project" in some general, magical way. It sees the current request. That request is assembled by the harness from a few different sources.
- system instructions durable behavior rules
- user task what we are doing now
- project context map of the repository
- tool results what the agent observed
- memory facts saved from previous runs
Context management is the work of choosing what belongs in that request right now. This is why it is such a large surface area. The harness is not only moving text around. It is deciding what the model can pay attention to.
Context Management Is A Loop
The first-principles version is a loop:
discover -> select -> load -> use -> update -> compress or forget
The agent discovers files, selects useful context, loads
it into the conversation, uses it to act, updates what
it knows, and eventually has to compress or forget old
details. /init is only the first step in
that larger cycle: discovery into a project map.
Context Fails In Four Ways
Most context problems are variations of four basic failures.
- too little context agent is blind
- too much context agent is distracted
- stale context agent trusts old facts
- wrong context agent optimizes for the wrong task
A project map mostly helps with the first failure. It gives the agent enough orientation to begin. But it also creates new responsibilities: keep the map small, refresh it when the project changes, and avoid loading it when the task does not need it.
The Fresh Session Problem
A good test is to start a fresh session and ask the agent: "What is this project and how do I verify a change?"
If it cannot answer without searching randomly, the project has an orientation problem. If it answers from one clear context file, the workspace is much easier for the agent to use.
Generate A Map
Our very first version does not need embeddings, retrieval, or a model-generated summary, which is typical of more advanced agents. It can be deterministic.
/init
In Loop, /init scans the configured
workspace and writes:
.loop/PROJECT.md
That file becomes a map. It can include the project root, detected shape, important files, source files, test files, documentation previews, and notes for the agent.
# Project Context
Generated by `loop /init`.
## Root
`/Users/example/project`
## Detected Shape
- Python, documented
## Source Files
- `loop_cli/agent.py`
- `loop_cli/tools.py`
## Test Files
- `tests/test_agent_fake_client.py`
This is not perfect understanding. It is orientation. The agent still needs to read the real files before changing them.
A Real Example: CLAUDE.md
If you have used Claude Code, this idea may already feel
familiar. Claude Code uses CLAUDE.md files
to give Claude persistent project instructions, and its
/init command can generate a starting
CLAUDE.md by analyzing the codebase.
That file usually contains things like build commands, test instructions, coding standards, project architecture, and common workflows. In other words: the things you do not want to re-explain every time a fresh session starts.
Our .loop/PROJECT.md is deliberately
simpler. It is not a full instruction system yet. It is
a project map: what files exist, where the tests are,
which docs matter, and what the agent should inspect
first.
- CLAUDE.md persistent instructions for Claude Code
- .loop/PROJECT.md generated orientation map for our tiny loop
Project Context vs Conversation Context
This separation is important.
- project context what exists on disk
- conversation context what is currently in the model request
.loop/PROJECT.md can exist on disk without
being in the model's current context. It means the user
can inspect it, regenerate it, and decide when to load
it.
/context show status
/context show print .loop/PROJECT.md
/context load add it to this session
/refresh rebuild and load it
/context load is the bridge. It takes a
file from the workspace and turns it into model-visible
context.
Why Not Auto-Load Everything?
Context is a budget and is limited. Every token used to describe the project is a token that cannot be used for the current task, recent tool results, or the user's instruction.
The user should be able to see what is loaded and understand why the model knows what it knows.
This Is Active Work
Context management is not a solved problem. It is one of the places where coding agents are actively evolving. Larger context windows help, but they do not remove the design problem. More context can mean more cost, more latency, and more irrelevant detail for the model to sort through.
A recent paper on
repository-level context files
found that context files can make agents explore more
and respect project instructions, but they can also
reduce task success and increase cost when they add
unnecessary requirements. That is a useful warning for
our version:
.loop/PROJECT.md should stay small,
factual, and useful.
Another line of work looks at context compression. The Active Context Compression paper frames long-running software tasks as a context bloat problem: old tool results, failed attempts, and stale details make the next turn more expensive and less focused. This space is one worth watching, but we will keep the application simple: we keep the map, but do not keep every footstep of the way.
What Larger Agents Do
Production coding agents use several layers at once. The details differ by product, but:
- repository instructions AGENTS.md, CLAUDE.md, project rules
- codebase search grep, symbols, embeddings, indexes
- session history recent messages and tool results
- compaction summaries or trimmed history
- external memory durable facts, decisions, preferences
- workspace state files, diffs, tests, command logs
A 2026 architecture study of Claude Code describes a much richer system around the simple model-tool loop, including layered context compaction, session storage, MCP, plugins, skills, hooks, and subagents. We are not building that whole machine here, but have a look for more details.
Skills are the natural next step after a generated
project map. .loop/PROJECT.md tells the
agent what exists. A skill tells it how to behave for a
particular kind of work, and progressive disclosure
keeps those instructions out of context until the task
actually needs them.
What To Skip
A context builder needs a sense of clutter. The agent does not need to index virtual environments, Git internals, caches, generated files, or its own context output.
skip:
.git/
.venv/
__pycache__/
.pytest_cache/
.loop/
memory.json
Just like in the workspace article: the harness should help the model see the project, not add the noise.
The Loop So Far
Here is the first project-context implementation. It is
intentionally deterministic: collect files, skip noise,
detect a rough project shape, preview docs, and render
.loop/PROJECT.md.
from pathlib import Path
PROJECT_CONTEXT_PATH = ".loop/PROJECT.md"
MAX_INIT_FILES = 120
MAX_DOC_PREVIEW_CHARS = 2000
def context_path(workspace: Path) -> Path:
return workspace / PROJECT_CONTEXT_PATH
def should_skip_project_path(workspace: Path, path: Path) -> bool:
relative = path.relative_to(workspace)
parts = set(relative.parts)
if parts & {".git", ".venv", "__pycache__", ".pytest_cache", ".loop"}:
return True
return relative.as_posix() == "memory.json"
def collect_project_files(workspace: Path) -> list[Path]:
files = []
for path in sorted(workspace.rglob("*")):
if not path.is_file() or should_skip_project_path(workspace, path):
continue
files.append(path)
if len(files) >= MAX_INIT_FILES:
break
return files
def read_preview(path: Path) -> str:
try:
content = path.read_text(errors="replace")
except OSError as error:
return f"[could not read: {type(error).__name__}: {error}]"
if len(content) <= MAX_DOC_PREVIEW_CHARS:
return content.strip()
return content[:MAX_DOC_PREVIEW_CHARS].strip() + "\n...[truncated]"
def detect_project_kind(files: list[Path]) -> list[str]:
names = {path.name for path in files}
kinds = []
if "pyproject.toml" in names or "requirements.txt" in names:
kinds.append("Python")
if any(path.suffix == ".py" for path in files):
kinds.append("Python")
if "package.json" in names:
kinds.append("Node.js")
if "Cargo.toml" in names:
kinds.append("Rust")
if "go.mod" in names:
kinds.append("Go")
return sorted(set(kinds)) or ["unknown"]
def render_project_context(workspace: Path, files: list[Path]) -> str:
rel_files = [path.relative_to(workspace).as_posix() for path in files]
docs = [
path
for path in files
if path.name.lower().startswith("readme")
or path.name in {"AGENTS.md", "CONTEXT.md", "requirements.txt", "pyproject.toml"}
or "/docs/adr/" in path.as_posix()
]
source_files = [
rel for rel in rel_files if rel.endswith((".py", ".js", ".ts", ".tsx", ".jsx"))
]
test_files = [
rel for rel in rel_files if "/test_" in f"/{rel}" or rel.startswith("test_")
]
lines = [
"# Project Context",
"",
"Generated by `loop /init`.",
"",
"## Root",
"",
f"`{workspace}`",
"",
"## Detected Shape",
"",
"- " + ", ".join(detect_project_kind(files)),
"",
"## Important Files",
"",
]
lines.extend(f"- `{rel}`" for rel in rel_files[:MAX_INIT_FILES])
lines.extend(["", "## Source Files", ""])
lines.extend(f"- `{rel}`" for rel in source_files[:50] or ["None detected."])
lines.extend(["", "## Test Files", ""])
lines.extend(f"- `{rel}`" for rel in test_files[:50] or ["None detected."])
lines.extend(["", "## Document Previews", ""])
for doc in docs[:10]:
rel = doc.relative_to(workspace).as_posix()
lines.extend([f"### `{rel}`", "", "```text", read_preview(doc), "```", ""])
lines.extend(
[
"## Notes For The Agent",
"",
"- Use this file as orientation, not as ground truth when code has changed.",
"- Re-run `/init` after meaningful structure changes.",
]
)
return "\n".join(lines).rstrip() + "\n"
def initialize_project_context(workspace: Path) -> Path:
files = collect_project_files(workspace)
content = render_project_context(workspace, files)
output = context_path(workspace)
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(content)
return output
The real Loop CLI wires this into /init,
/refresh, and /context. The
important design point is that context generation is not
hidden inside a prompt. It is a visible tool.
Where we are
Project context gives the agent a map. It lowers the cost of orientation, makes hidden prompt state inspectable, and gives the user control over what the model sees.
Next we will look at memory: what should survive beyond a single project scan, what should stay temporary, and why remembering too much can be just as dangerous as remembering too little.