Project Context
Your harness is generic. Every project it touches is not.
One project uses bun test. The next uses vitest. The third uses npm run check for a typecheck-and-lint combo that doesn't match any convention you've baked into the prompt. The agent shouldn't have to guess, and you shouldn't have to teach it project by project.
The trick most production harnesses use is the same one teams use for their human contributors: drop a markdown file in the repo that explains how the project works. The agent reads it and adjusts.
Outcome
If an AGENTS.md file exists in the working directory, the harness reads it and injects its contents into the system prompt as a "Project Instructions" section. With no file, the prompt falls back to the base instructions only.
Fast Track
- Check for
AGENTS.mdin the working directory - If present, read it into a string
- Pass it to
buildSystemPromptasprojectContext
Hands-on Exercise 3.4
Discover and inject AGENTS.md from the working directory.
Requirements:
- In
index.ts, check whethercwd/AGENTS.mdexists - If it does, read it as UTF-8
- Pass the contents (or
undefined) tobuildSystemPromptasprojectContext - Confirm the prompt builder's existing
projectContexthandling adds the section when present and omits it when absent
Implementation hints:
existsSyncfromnode:fsis fine here. The check happens once at startup, not in a hot loop- Use
path.join(cwd, "AGENTS.md"), not template literals, to keep path handling consistent - Don't add monorepo directory walking yet. The challenge below covers that, and Module 4's sandbox abstraction will change how file discovery works anyway
The discovery
The whole feature is a few lines:
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
const agentsPath = join(cwd, "AGENTS.md");
const projectContext = existsSync(agentsPath)
? readFileSync(agentsPath, "utf-8")
: undefined;Pass it into the builder:
const instructions = buildSystemPrompt({
workingDirectory: cwd,
sandboxType: "local",
toolNames: Object.keys(tools),
projectContext,
});That's all of it. Convention over configuration. No plugin system, no registration, no event bus. A file in a known location.
What goes in AGENTS.md
The file is for project-specific facts the harness can't infer from the code itself:
# Project Instructions
## Commands
- `bun test` runs the test suite
- `bun run build` builds for production
- `bun run lint` checks code style
## Architecture
- Monorepo, packages live in `packages/`
- Each package has its own `tsconfig.json`
- Shared types in `packages/shared/`
## Style
- Functional components, no classes
- Named exports, not default
- Error messages must be user-facing
## Lessons learned
- Auth middleware must run before rate limiting
- Don't modify migration files directly, generate new onesThe agent reads this and adjusts. It knows the commands. It knows the architecture. It knows the project's recurring mistakes. The same harness now behaves like a React-project agent in a React project and like a CLI-project agent in a CLI project, because the project itself told the agent what kind of project it is.
This is the same trick Cursor uses with .cursorrules, Codex with AGENTS.md, Claude Code with CLAUDE.md, and pi with its own convention. The file name varies. The pattern doesn't. Discover a markdown file, inject as instructions.
Why one file is enough for now
A real harness walks parent directories looking for AGENTS.md files in a monorepo. It merges them. It might also support .cursorrules, .github/copilot-instructions.md, or ~/.agents/default.md.
We're not building any of that yet. One file in the working directory covers the teaching point: project context comes from a file, the harness discovers it at startup, the prompt absorbs it. The walking and merging stack on top of that without changing the shape.
Try It
Drop an AGENTS.md into the working directory with one specific instruction the agent couldn't infer from the code:
# Project Instructions
- All commits must use the format `feat(scope): message`
- The verification step is `bun test`, not `npm test`Run the agent:
bun run index.ts . "What command do I use to run the tests in this project?"The agent should answer bun test because the file told it. Remove AGENTS.md and run the same prompt. The agent will guess (probably npm test).
npx tsc --noEmitCommit
git add index.ts
git commit -m "feat(prompt): inject AGENTS.md as project context"Done-When
index.tschecks forAGENTS.mdin the working directory- If present, its contents are passed to
buildSystemPromptasprojectContext - With
AGENTS.mdpresent, the agent answers project-specific questions from the file - Without
AGENTS.md, the harness runs without error using base instructions only npx tsc --noEmitpasses
A real monorepo has an AGENTS.md at the root and another inside each package. Starting from cwd, walk up to the repo root (look for .git) and collect every AGENTS.md along the way. Merge them, with the deepest file overriding or extending the root. Now think about conflicts: what happens when the root says use npm and the package says use pnpm? Different harnesses solve this differently. pi merges everything found. Cursor uses the deepest only. Codex concatenates root plus cwd. Try one strategy and notice where it breaks.
Solution
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { buildSystemPrompt } from "./src/system";
const cwd = resolve(process.argv[2] || process.cwd());
const agentsPath = join(cwd, "AGENTS.md");
const projectContext = existsSync(agentsPath)
? readFileSync(agentsPath, "utf-8")
: undefined;
const tools = { read, grep, bash };
const agent = new ToolLoopAgent({
model: "anthropic/claude-haiku-4-5",
instructions: buildSystemPrompt({
workingDirectory: cwd,
sandboxType: "local",
toolNames: Object.keys(tools),
projectContext,
}),
tools,
stopWhen: stepCountIs(10),
});Was this helpful?