Post 07

Project Context Is A Tool

A coding agent should not begin every task as if it has never seen the repository or your project before. It needs a map of the project, and that map should be generated, inspectable, and loaded on purpose. That's what we will learn about Today.

Hand-drawn bottle diagram showing project context as something gathered and poured into the agent loop.

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.