Give Your Coding Agent Eyes
Every coding-agent session starts blind: your agent rediscovers how your features work, and sometimes guesses wrong. AGENTS.md is the usual fix, but it loads every fact into every session - too thin to answer real questions, or too fat to afford. So give the agent a catalog instead. Each feature in your project gets one small note. At startup the agent loads only a table of paths and descriptions - no content - and reads a note when the description matches the task. You regenerate the catalog from your code every 20 commits or at the end of each sprint, so it never drifts from what the code does today. The menu, not the meal Here's what the agent sees at startup: If the current task requires knowledge from a file below, call the read tool on it before proceeding. Skip files whose description does not match the task. /app/.pi/notes/back.settings.md Load when working on per-organization settings for notification rules and other global defaults. /app/.pi/notes/back.notification-engine.md Load when working on how queued notifications are batched, retried, and dispatched. Paths and descriptions. No content. Twenty features cost me about 300 tokens - roughly one mid-sized note's worth - and every feature in the project is reachable. When I ask for a change to the settings page, the agent reads back.settings.md and nothing else. When I ask about retry behavior, it reads the notification engine note. The LLM is the relevance filter, and it turns out to be a very good one - provided the descriptions are written as triggers. That's the whole idea: the agent starts with a table of contents for the codebase, and pulls the chapters it needs. How the block gets built This is a pi extension, pi-context . It hooks before_agent_start : pi.on("before_agent_start", async (event, ctx) => { const rootFiles = await walkUpContextFiles(fs, cwd); if (rootFiles.length === 0) return; const linked = await collectConfigDirFiles(fs, projectRoot, visited); const block = formatContextFilesBlock(linked); if (!block) return; return { systemPrompt: ${event.systemPrompt}\n\n${block} }; }); Four steps: walk up from cwd to find the project root via AGENTS.md / CLAUDE.md , recursively scan .pi/ , .claude/ , and .agents/ for .md files, keep only the ones with a description frontmatter field, inject the block. It runs over SSH too, against the remote filesystem, which is why the fs is abstracted. Note the filter. A markdown file without a description is invisible. That's the opt-in - your READMEs and scratch notes don't leak into the system prompt. The description is the router This is the one thing that decides whether the whole system works, so it gets its own rule: the description is a trigger condition, not a summary. # Bad - describes the file description: This file covers the settings feature. # Good - describes when to read it description: Load when working on per-organization settings for notification rules and other global defaults. The agent never sees the content when it makes the decision. It sees one sentence and has to answer "does this match my task?" A summary makes that a guess. A trigger makes it a lookup. Every description starts with "Load when" or "Use when" - the constraint is enforced by the skill that writes them. Notes are grouped by feature The notes live in .pi/notes/ , one file per feature, flat, prefixed by layer: .pi/notes/ โโโ _index.md โโโ .last-sync โ a88fd19e683fa8d75... โโโ back.settings.md โโโ back.notification-engine.md โโโ back.auth-tenancy.md โโโ back.prisma-schema.md โโโ ops.dev-workflow.md โโโ test.harness.md One topic per file, hard-capped around 5 KB. The feature is the unit of perception: it's the granularity at which you actually ask for changes, so it's the granularity at which the agent should be able to load knowledge. A 40 KB architecture doc is useless here - it can't be selectively read. A real note looks like this, in full: --- description: "Load when working on per-organization settings for" notification rules and other global defaults. --- # Settings Feature The settings row is a per-org singleton reached at /api/v1/settings; GET returns the org's settings, PATCH creates or updates them. GET /api/v1/settings returns the current notification configuration for the active org. Defaults are notificationsEnabled = true and notificationRules = null when no row exists yet. The first GET for a tenant without settings still returns a usable config. PATCH only accepts notificationsEnabled and notificationRules; the use case upserts the singleton for the active org. PATCH /api/v1/settings { notificationsEnabled: false } disables global notifications. The notification engine reads these rules when a job opts into useGlobalRules. Global notifications disabled means the engine drops every queued job silently. See [[back.notification-engine]]. Settings are tenant-scoped by organizationId. GET returns defaults when no row exists yet. The engine, not the settings feature, decides how rules apply. Roughly 1.4 KB. Every line is a fact an agent would otherwise have to infer from three files, and the under each requirement pins down the ambiguous ones. Notes cross-link with [[wiki-style]] references, so an agent reading about settings knows where the consuming behavior lives. Bootstrapping: one cold start Nobody hand-writes twenty of these. The doc-routing skill (doc-skills) does a cold start on a repo with no notes: Cold start detected - no previous sync found. Root: /app Skipping: **/.lock, /dist/, **/.generated.*, /build/ Files in scope: ~137 files (will be grouped into topics) Proceed? [yes / cancel] It runs git ls-files , filters the noise, groups the rest by feature, and splits any group bigger than three files into sub-topics - recursively, until every topic is small enough that one note can honestly cover it. Then it reads the source files for each topic and writes a note. That last part matters. It does not read the git log. It reads the code. Keeping the eyes open Cold start is the expensive run. After that there's .last-sync - a single commit hash sitting in the notes directory. I type sync notes and get: Last sync: a88fd19 - 2026-08-16 - feat: add rule editor Range: a88fd19..HEAD Proposed sync - 14 commits: ยท 3f2b1ac refactor: extract dispatch queue ยท 91cd004 fix: tenant scoping on PATCH ... Confirm? [yes / provide different range] Confirm, and it groups the changed files by feature, classifies each group as UPDATE, CREATE, or SKIP, rewrites only the affected sections, and stores the new hash. Cost is proportional to what changed, not to repo size. Cadence: every ~20 commits, or at the close of a sprint. That's not arbitrary - the workflow warns you above 20 and tells you to batch into 10-20 commits per run. Below that you're syncing noise. Far above it, the diff spans so many features that grouping gets sloppy and you're really doing a cold start again. A sprint boundary happens to land almost exactly in that window, which makes it a natural ritual: merge the sprint, run one sync, review the diff on the notes, commit. It costs a couple of minutes. What you get back is that every session for the next two weeks starts with accurate knowledge of every feature in the project. Behavior, not history The one rule that makes this a perception layer instead of a changelog: The doc must describe what the code currently does - not what changed. Diffs and commit messages are explicitly banned as note content. The sync uses the diff only to decide which notes to touch - then it reads the current source and writes the current truth. The extraction guide draws the line sharply: | Keep (behavioral) | Drop (structural) | |---|---| | "Returns 401 when the token is missing." | "Has a token validation function." | | "MAX_RETRIES=3 - requests fail hard after 3 attempts." | "PORT=3000 - default HTTP port." | | "// FIXME: rate limiter not enforced for admin tokens." | "// TODO: add pagination." | A changelog tells an agent what moved. Eyes tell it what is. An agent handed a history has to replay it to learn the present state; an agent handed the present state can just work. Same reason you don't onboard a new teammate by having them read six months of PRs. The writing itself A third piece, doc-writing , owns how each note is written: format selection (ordered steps get numbered blocks, keyword routing gets a trigger table, hard constraints go before soft ones), a token budget of 10-15 lines per section, and the description: contract. It's what keeps twenty notes written by twenty different sessions looking like one system. Try it pi install git:github.com/zeflq/pi-context Add the doc-skills skills, then in your repo: sync notes No .last-sync , no notes โ it offers the cold start. After that, run it every sprint. The setup is maybe twenty minutes on a real codebase. The change is hard to go back from: the agent stops asking you where things live. Top comments (0)
Comments
No comments yet. Start the discussion.