banner
约 9000 字
30 分钟

Harnessing AI Agents P2: I stopped guessing what my AI agent should know

2026年2月15日
2026年2月17日

文章摘要

TL;DR: Your codebase already contains a feature list, scattered across routes, services, schema, tests, and issues. I'll show you how to extract those into a structured JSON inventory that anchors your AI agent and makes every task scoped and verifiable. All demonstrated on a real Cloudflare Workers project.

This post assumes you have an existing project and want to use AI coding agents (like Claude Code, Cursor, Copilot) more effectively. If you've read my previous post on building an AI agent workflow from scratch, this is the practical "how-to" for one of its core pieces.

The problem: why "just read my repo" doesn't work

I pointed Claude Code at mogpersonalweb (full-stack blog CMS, runs on Cloudflare Workers, uses Hono and Drizzle ORM and, uh, TanStack Start for the SSR bits) and asked it to add newsletter subscriptions.

It wandered. Spent tokens poking around the codebase like a new hire on day one with no onboarding doc, opening files at random, reading things that didn't matter, skipping things that did. Missed the patterns I'd spent weeks establishing. My Result<T, E> error type, the middleware chain, cache versioning. And built something that technically worked but felt like it belonged in a completely different project. The kind of output where you look at it and go, yeah, this is correct, but also, have you seen the rest of my codebase?

My first reaction was to blame context length. Or the model. But honestly the real problem was embarrassingly simple. I'd handed over code without ever telling it what any of that code was for.

And that's a distinction that took me way too long to see. Raw code tells an agent what exists. It says nothing about what actually matters. Or what you've already covered. And definitely nothing about when you're actually done with a feature. Without that structure, what you get is improvisation. And agent improvisation is just hallucination wearing a trench coat.

What I needed: some kind of anchor. A feature inventory that's structured (machine-readable, ideally JSON instead of just prose) something that actually pins down where the agent looks instead of letting it wander. Not documentation for humans to skim over coffee. More like a file where each entry says "here's a capability this app has, here's its current status, and here are the steps to prove it works." Intent, basically. The stuff that lives in your head but never makes it into the source files.

Once the agent has that, it stops guessing and starts working within boundaries. Still makes mistakes, yeah. But there's a world of difference between "guess and hope" and "work within constraints."

Great. So I sat down to write this inventory from scratch. Opened src/. Stared at it.

...where do you even start?

From memory, apparently. I tried. Got to about 8 items before everything started blurring together, descriptions getting vaguer with each one. By item 6 I was writing stuff like "comment system" with zero specifics, which, as we'll see later, is basically useless.

So I tried what felt like the obvious shortcut. (Obvious to me, anyway. Maybe nobody else would've done this.) I asked the agent itself to generate the feature list from the codebase. It came back with 24 items in clean JSON. Looked great, looked professional, looked like exactly the kind of thing you'd put in a README to impress someone. Then I checked them against the actual code:

JSON
{
  "id": "newsletter-subscribe",
  "description": "User can subscribe to email newsletter for new post notifications"
}

This feature didn't exist. Neither did "dark mode toggle" or "post bookmarking." Three hallucinated features, conjured from whatever statistical soup of blog CMS patterns the model had ingested during training. And on the flip side, it had listed internal plumbing like "database migration system" and "middleware chain composition" as if they were product features a user would care about. Those things matter, but not as user-observable capabilities. (They do belong in a feature list, actually. Just framed differently. More on that in Rule 1.)

That's when it clicked, and I felt a little stupid for not seeing it earlier. The agent that builds features can't also be the one deciding what "done" means. That's writing your own exam and grading it too. What I needed was systematic extraction, starting from what the code actually does, filtered through a human who knows what matters versus what's noise.


The mental model: your code already knows what it does

Once I stopped trying to conjure the list from thin air and just, you know, looked at my project, things got easier fast. I opened the folder structure. Hono puts routes here. Drizzle puts schema there. Services live under each feature module. The shape of the inventory was already right there in front of me, I'd just been trying to write it from the wrong direction.

Every codebase, if you squint at it right, has five layers where features leave traces:

Source

What it reveals

Example

Routes

What users can reach

/search, /admin/posts/edit.$id

Service methods

What the app can do

moderateComment(), submitFriendLink()

Schema / Models

What data the app manages

posts, tags, friend_links tables

Tests

What behavior has been verified

"should return tags sorted by name"

Issues / TODOs

What's broken or missing

"Search results have no highlighting"

What about UI components? Fair question. Complex client-side interactions (drag-and-drop, live validation, modals) live in src/features/*/components/. I fold these into service or route items when I find them, since they almost always map to an existing capability. But if your app is heavily client-side (SPA, canvas-based, rich editor), components probably deserve their own pass.

This isn't a mechanical process where you run a script and get a feature list out the other end, though. I want to be clear about that because the table above makes it look cleaner than it is. Each source gives you raw material, and you still have to curate. Toss the internal helpers nobody cares about, merge items that describe the same thing from different angles, rewrite implementation details as capabilities someone could actually observe. Extraction plus editorial judgment. Not copy-paste.

The order that felt most natural to me: Routes first (broadest view), then Schema (fills structural gaps), Services (adds depth), Tests (validates what's been verified), Issues (captures what's missing). Wide to narrow, each pass filling holes the previous one left. But I could see someone starting with schema if that's how they think about their app. There's no magic to the ordering.


The 5 sources: how I extracted ~22 feature items from mogpersonalweb

For each source I'll walk through what I looked at, what I pulled out, and what caught me off guard. Every extracted item uses the same JSON schema (this is the actual format of the feature list file):

JSON
[
  {
    "id": "search-posts",
    "category": "public",
    "description": "User can search posts by keyword",
    "status": "passing",
    "verification_steps": [
      "Navigate to /search",
      "Enter a keyword",
      "Confirm matching results appear"
    ]
  },
  {
    "id": "search-highlight",
    "category": "functional",
    "description": "Search results highlight matching keywords",
    "status": "failing",
    "verification_steps": [
      "Navigate to /search",
      "Enter a keyword that matches known posts",
      "Confirm matched terms are visually highlighted in results"
    ]
  },
  {
    "id": "cache-versioned-invalidation",
    "category": "technical",
    "description": "Cache keys use version-based invalidation via CacheService.bumpVersion(); stale entries are purged in background via waitUntil()",
    "status": "passing",
    "verification_steps": [
      "Update a published post",
      "Confirm KV cache version increments",
      "Confirm subsequent public reads return fresh data within 5 seconds"
    ]
  }
]

Three examples, three categories. The categories will make more sense as we go.

Status values: each item has one of four statuses:

Status

Meaning

pending

Planned but no code exists yet. A slot in the backlog.

failing

Code exists but the feature is broken, incomplete, or doesn't pass its verification steps.

passing

Works correctly. Verification steps succeed.

deprecated

Was passing, now intentionally removed or superseded. Kept in the list so the agent doesn't accidentally rebuild it.

And the transitions between them:

text
pending → failing    (work begins, first implementation lands)
failing → passing    (verification steps all pass)
passing → failing    (regression discovered, or requirements changed)
passing → deprecated (feature intentionally removed or replaced)

Most items start as failing. You extracted them from existing code that works, so they go straight to passing, or you noted something broken, so it's failing. pending is for features you want but haven't started. deprecated is rare but prevents a specific failure mode: without it, the agent sees a capability that used to exist, finds no code for it, and helpfully rebuilds the thing you just spent an afternoon removing.


Source 1: Routes (user-reachable functionality)

If a user can navigate to it, it's a feature. Hard to argue with that, so I started here.

text
src/routes/
├── _auth/
│   ├── login.tsx
│   ├── register.tsx
│   ├── forgot-password.tsx
│   ├── reset-link.tsx
│   └── verify-email.tsx
├── _public/
│   ├── index.tsx
│   ├── posts.tsx
│   ├── search.tsx
│   ├── friend-links.tsx
│   ├── unsubscribe.tsx
│   └── post/$slug.tsx
├── _user/
│   ├── profile.tsx
│   └── submit-friend-link.tsx
├── admin/
│   ├── index.tsx
│   ├── posts/edit.$id.tsx
│   ├── comments/index.tsx
│   ├── friend-links/index.tsx
│   ├── media/index.tsx
│   ├── tags/index.tsx
│   └── settings/index.tsx
├── robots[.]txt.ts
├── rss[.]xml.ts
└── sitemap[.]xml.ts

The layout groups basically handed me a role taxonomy for free: public/ is anyone, auth/ is authentication flows, _user/ is logged in, admin/ is admin only. That mapping went straight into each item's category field without me having to think about it, which was nice, because the next part required a lot more thinking.

For features that cross roles or describe system-level behavior (AI moderation, post tagging, search highlighting), I used "functional". And later I added "technical" for infrastructure items, but that whole distinction really belongs in the Rule 1 discussion, so I'll come back to it.

I poked through page components too, looking for interactions the route path alone doesn't reveal. Inline editing, keyboard shortcuts, that sort of thing. Short list, as it turned out. Folded them into existing service items and moved on.

JSON
{
  "id": "search-posts",
  "category": "public",
  "description": "User can search posts by keyword with relevant results",
  "status": "passing",
  "verification_steps": [
    "Navigate to /search",
    "Enter a keyword that matches a known post title",
    "Confirm the matching post appears in results"
  ]
}
JSON
{
  "id": "submit-friend-link",
  "category": "user",
  "description": "Logged-in user can submit a friend link request for admin review",
  "status": "passing",
  "verification_steps": [
    "Log in with GitHub OAuth",
    "Navigate to /submit-friend-link",
    "Fill in site name and URL",
    "Submit the form",
    "Confirm success message appears"
  ]
}
JSON
{
  "id": "rss-feed",
  "category": "public",
  "description": "Blog provides an RSS feed at /rss.xml with published posts",
  "status": "passing",
  "verification_steps": [
    "Navigate to /rss.xml",
    "Confirm valid XML is returned",
    "Confirm published posts appear in the feed"
  ]
}

I nearly skipped the SEO routes. rss.xml, sitemap.xml, robots.txt. No UI, no React component, not even something you'd demo to anyone. But they're endpoints a user (or a bot, or a feed reader) can hit, with behavior you can verify, and that makes them features whether they render pixels or not. Funny how the least glamorous routes were the ones I almost left out.

Routes produced 7 candidates. After de-duplication with services (coming up next), 5 survived as route-only items.


Source 2: Service methods (business capabilities)

Routes gave me the surface. Services were where I found the depth, and also where the de-duplication headaches started.

A single route like /post/$slug has a whole cascade happening behind it. Fetching the post, checking permissions, loading comments, incrementing view counts, maybe triggering a cache warm. Each public method on a *.service.ts file is one business capability, and I had 12 service files, north of 80 public methods total. posts.service.ts, comments.service.ts, tags.service.ts, friend-links.service.ts, search.service.ts, media.service.ts, cache.service.ts, ai.service.ts, and a handful more I'm probably forgetting.

TypeScript
// src/features/comments/comments.service.ts (simplified)
export async function createComment(context: AuthContext, data: CreateCommentInput)
export async function deleteComment(context: AuthContext, data: DeleteCommentInput)
export async function moderateComment(context: DbContext, data: ModerateCommentInput)
export async function getMyComments(context: AuthContext, data: GetMyCommentsInput)
export async function getAllComments(context: DbContext, data: GetAllCommentsInput)
JSON
{
  "id": "admin-moderate-comment",
  "category": "admin",
  "description": "Admin can approve or reject a pending comment",
  "status": "passing",
  "verification_steps": [
    "Log in as admin",
    "Navigate to comment management page",
    "Find a pending comment",
    "Click approve or reject",
    "Confirm comment status changes accordingly"
  ]
}
JSON
{
  "id": "ai-comment-moderation",
  "category": "functional",
  "description": "New comments are automatically screened by AI moderation before publishing",
  "status": "passing",
  "verification_steps": [
    "Submit a comment on any post",
    "Confirm the comment enters 'pending' status",
    "Confirm AI moderation workflow triggers",
    "Check that appropriate comments are auto-approved or flagged"
  ]
}
JSON
{
  "id": "media-upload",
  "category": "admin",
  "description": "Admin can upload images to R2 storage and manage them in the media library",
  "status": "passing",
  "verification_steps": [
    "Log in as admin",
    "Navigate to media management",
    "Upload an image file",
    "Confirm the image appears in the media list",
    "Confirm the image is accessible via its URL"
  ]
}

Now, the de-duplication problem. POST /api/comments and createComment() are the same capability described twice. Lots of that. My rule ended up being: if the route is just an HTTP wrapper around a service call, kill the route item, keep the service one. Service names describe the business action (moderateComment) rather than the HTTP plumbing (POST /api/comments/moderate), and that's what you want in a feature description. I only kept route-only items for stuff that's purely routing. Redirects, permission gates, SEO endpoints with no service behind them.

This killed about a third of my initial list, which honestly felt great. Less noise in the JSON, fewer tokens wasted when the agent loads it. Services produced 10 items; combined with routes after the cull, 12 total.


Source 3: Schema / Models (data the app manages)

Almost skipped this one. Would've been a massive blind spot, in retrospect, because schema files contain information about your app that literally nothing else does.

Drizzle schema is basically a map of what the app cares enough about to persist. And what I found when I went through the tables was that some of them implied features I'd completely forgotten I'd built.

TypeScript
// src/lib/db/schema/
// posts.table.ts   → PostsTable, TagsTable, PostTagsTable
// friend-links.table.ts → FriendLinksTable
// comments.table.ts → CommentsTable, EmailUnsubscriptionsTable
// media.table.ts    → MediaTable, PostMediaTable
// config.table.ts   → SystemConfigTable
// auth.table.ts     → user, session, account, verification
JSON
{
  "id": "post-tagging",
  "category": "functional",
  "description": "Posts can be assigned multiple tags via a many-to-many relationship",
  "status": "passing",
  "verification_steps": [
    "Open post editor",
    "Add two or more tags to a post",
    "Save the post",
    "Reload the post",
    "Confirm all assigned tags are still present"
  ]
}
JSON
{
  "id": "email-unsubscribe",
  "category": "public",
  "description": "Users can unsubscribe from email notifications via a link",
  "status": "passing",
  "verification_steps": [
    "Receive a notification email",
    "Click the unsubscribe link",
    "Confirm unsubscription success page appears",
    "Confirm no further emails are sent"
  ]
}

The friend_links table was the big one. It exposed an entire approval workflow (admin approve/reject, notification emails on status change) that had been invisible during route scanning because it was tucked behind a generic admin layout. I'd looked at admin/friend-links/index.tsx and thought "yeah, friend link management, already got that." But the approval flow was a separate feature with separate behavior, and I'd missed it.

And then there was email_unsubscriptions. I had to go re-read my own migration file to remember what that table was even for. (It handles opt-outs from comment notification emails. Past me apparently built that at some point.) Schema filled in 3 items that neither routes nor services had turned up. Not bad for a source I almost didn't check.


Source 4: Tests (verified behavior)

Test descriptions are feature descriptions in their pajamas. Rough around the edges, written for a different audience, but they contain the actual behavioral intent in a way that service signatures and route paths don't.

TypeScript
// From tags.service.test.ts
it("should return tags sorted by name", ...)
it("should return tags with post counts", ...)
it("should cache public tags list", ...)

// From friend-links.service.test.ts
it("should submit a friend link with pending status", ...)
it("should send admin notification email on submission", ...)
it("should reject duplicate URL submission", ...)

// From media.service.test.ts
it("should upload file and create DB record", ...)
it("should rollback R2 upload when DB insert fails", ...)
JSON
{
  "id": "post-list-pagination",
  "category": "public",
  "description": "Post list displays in reverse chronological order with cursor-based pagination",
  "status": "passing",
  "verification_steps": [
    "Navigate to the blog home page",
    "Confirm posts are ordered by publish date descending",
    "Scroll to the bottom",
    "Click next page",
    "Confirm older posts appear"
  ]
}
JSON
{
  "id": "friend-link-duplicate-guard",
  "category": "functional",
  "description": "System rejects duplicate friend link URL submissions but allows resubmission after rejection",
  "status": "passing",
  "verification_steps": [
    "Submit a friend link with a URL",
    "Try submitting the same URL again",
    "Confirm rejection with duplicate error",
    "Have admin reject the first submission",
    "Resubmit the same URL",
    "Confirm resubmission succeeds"
  ]
}

But here's the thing: what the tests didn't cover ended up being more useful than what they did. I had 50+ descriptions across tags, friend links, media, rate limiting, search. Nothing for comment creation. Nothing for post publishing. Nothing for the entire auth flow. Those gaps were a signal. They told me which features to give a mental asterisk, which areas to point the agent at first when I wanted better coverage. Tests confirmed 2 existing items and turned up 2 behavioral items I'd missed, but the real value was the negative space.


Source 5: Issues / TODOs (broken or missing features)

The first four sources extract what exists. This one gets at the stuff that isn't there yet, or that's there and broken.

I grep'd for TODO and FIXME markers, checked GitHub Issues. Codebase was clean. No outstanding TODOs in source files. I'm honestly not sure if that reflects discipline or if I just never write them. But I knew from actually using the app that certain things were busted or half-done. The kind of stuff you notice every time, wince at, and then go do something else because fixing it isn't urgent enough to bump whatever you're actually working on:

JSON
{
  "id": "search-highlight",
  "category": "functional",
  "description": "Search results highlight matching keywords in the result snippets",
  "status": "failing",
  "verification_steps": [
    "Navigate to /search",
    "Enter a keyword that matches known posts",
    "Confirm matched terms are visually highlighted in results"
  ]
}
JSON
{
  "id": "comment-notification",
  "category": "functional",
  "description": "When a new comment is submitted on a post, the post author receives an email notification",
  "status": "failing",
  "verification_steps": [
    "Submit a comment on any post as a logged-in user",
    "Check admin email inbox",
    "Confirm notification email arrived with comment content and post link"
  ]
}

By far the easiest items to write. Past me had already done the hard work (noticing the problem, getting annoyed enough about it to remember). The agent could pick these up with zero ambiguity because the annoyance had already crystallized into specifics. 3 items, all "status": "failing".


After all five passes: 22 items. I felt good about the coverage, felt like I'd been thorough, felt like I could hand this to the agent and things would go well.

They did not go well. Half the items produced garbage. The features I'd extracted were real, the categories made sense, the coverage was solid. But the descriptions (the way I'd worded them) were leading the agent astray in ways I hadn't anticipated at all.


The 3 description rules: making extractions agent-ready

Extraction gives you the raw material. These rules are what make it actually usable, and I arrived at them the hard way: by writing bad descriptions, watching the agent misinterpret them, getting frustrated, and iterating until the failure modes stopped recurring. I'll show the failures alongside the fixes because the failures are where the learning is.

After my first pass I had 30+ items, and I'll be honest, most of them were junk. Too vague, too focused on implementation guts, or worded in a way that made verification impossible. I threw out a third entirely. Rewrote the rest.


Rule 1: Write what the user does, not what the code does

My gut was to write items that read like Jira tickets:

JSON
{
  "id": "newsletter-endpoint",
  "category": "functional",
  "description": "Implement POST /api/newsletter/subscribe endpoint",
  "status": "failing",
  "verification_steps": [
    "Send POST request to /api/newsletter/subscribe",
    "Confirm 200 response"
  ]
}

The agent built the endpoint. Just the endpoint. No form. No validation. No success message. No error states. It did exactly what I asked for, which (and I cannot stress this enough) was precisely the problem.

What happened concretely: it created a Hono route handler, wrote the database insert, returned a 200 JSON response, and called it done. No React component. No Zod schema on the input. No success or error UI. It had built the back half of a feature because the description only described the back half. I stared at the diff for a while before I realized the bug was in my spec, not its implementation.

Here's the rewrite, framed around what a user actually experiences:

JSON
{
  "id": "newsletter-subscribe",
  "category": "public",
  "description": "User can enter their email on the blog homepage to subscribe to new post notifications",
  "status": "failing",
  "verification_steps": [
    "Navigate to the blog homepage",
    "Find the newsletter subscription form",
    "Enter a valid email address",
    "Submit the form",
    "Confirm a success message appears",
    "Enter an invalid email address",
    "Submit the form",
    "Confirm a validation error appears"
  ]
}

Night and day. Implementation-focused descriptions let the agent stop at the code boundary and feel satisfied. User-intent descriptions drag it through the whole picture: form, validation, feedback, error states, the full loop.

Note: The newsletter feature doesn't exist yet in mogpersonalweb. This is an example of a "failing" item I'd add for future work.

When Rule 1 doesn't apply: technical features

So Rule 1 is great when a human is the actor. But not everything agents touch is user-facing, and this is where I got bitten in a completely different way.

I asked the agent to "refactor the cache invalidation layer to use version-based keys." My feature list was all user-observable stuff ("user can search posts," "admin can upload images") and it had nothing to say about how caching was supposed to work under the hood.

The agent went ahead and refactored caching. It "worked." Requests returned data. But it had silently dropped my waitUntil() pattern for background invalidation and swapped out versioned keys for timestamp-based expiry. A totally different architecture, one that would quietly break under load when stale cache entries piled up. The feature list had told the agent nothing about the intended design, so it designed its own, and the fact that it happened to return correct responses during testing was just luck.

For infrastructure (caching strategies, middleware chains, rate limiting, migration patterns), flip Rule 1. Write what the system does, not what the user sees. These get category: "technical":

JSON
{
  "id": "cache-versioned-invalidation",
  "category": "technical",
  "description": "Cache keys use version-based invalidation via CacheService.bumpVersion(); stale entries are purged in background via waitUntil()",
  "status": "passing",
  "verification_steps": [
    "Update a published post",
    "Confirm KV cache version increments",
    "Confirm subsequent public reads return fresh data within 5 seconds"
  ]
}
JSON
{
  "id": "rate-limiting-token-bucket",
  "category": "technical",
  "description": "API endpoints are rate-limited using a token bucket algorithm backed by Durable Objects, with per-IP tracking",
  "status": "passing",
  "verification_steps": [
    "Send requests exceeding the rate limit from a single IP",
    "Confirm 429 response after threshold",
    "Wait for token replenishment period",
    "Confirm requests succeed again"
  ]
}

Verification steps are still concrete, still things you could automate. They just test system behavior instead of UI flows. And the Rule 3 heuristic still holds: if you can't turn the steps into an automated test, you haven't finished writing them.

Quick gut check I use for categorization: would I explain this feature to a product manager? Then it's "functional". Would I only bring it up in a sprint retro or a pull request description? Probably "technical".


Rule 2: One observable behavior per item

Too broad:

JSON
{
  "id": "comment-system",
  "category": "functional",
  "description": "Complete comment system functionality",
  "status": "failing",
  "verification_steps": [
    "Verify comments work"
  ]
}

When does "complete" happen? When can the agent mark this "passing"? It can't. Nobody can. The word "complete" is doing no work in that sentence.

I watched what the agent did with this one and it was kind of painful. Tried to build everything at once (creation, threading, moderation, notifications, deletion) in a single session. Around the third sub-feature it started losing the thread, referencing variables from thousands of tokens ago, duplicating logic it'd already written. End result: creation worked, moderation was half-broken, notifications never materialized. Context-window overreach, which, if you've spent any time with these agents, you know is one of the most common failure modes. The description invited it.

Too narrow:

JSON
{
  "id": "comment-email-field",
  "category": "functional",
  "description": "Add email column to comments table in schema.ts",
  "status": "failing",
  "verification_steps": [
    "Check that email column exists in comments table"
  ]
}

That's not a feature. That's a line of code cosplaying as a feature. You're micromanaging, doing the agent's job for it, and in the process stripping it of the context it would need to make good decisions about how to implement that column.

Right-sized:

JSON
{
  "id": "comment-notification",
  "category": "functional",
  "description": "When a new comment is submitted, admin receives an email notification via Resend",
  "status": "failing",
  "verification_steps": [
    "Submit a comment on any post as a logged-in user",
    "Check admin email inbox",
    "Confirm notification email arrived with comment content and post link"
  ]
}

One behavior. Observable outcome. The definition of "done" fits in your head without squinting. The agent knows when to stop, which, I've learned, is half the battle.


Rule 3: Verification steps must walk the real UI

My early attempts:

JSON
"verification_steps": [
  "Run bun test",
  "Confirm all tests pass"
]

Useless. "Tests pass" is a side effect, not proof that the feature works. It's like verifying a building is structurally sound by checking that the lights turn on.

What actually happened when I gave the agent this: it built search, wrote one unit test for the happy path, watched it go green, and moved on. The search page did render results. It just had no empty state when nothing matched, no partial-match handling, no edge cases at all. The agent had satisfied "tests pass" without ever getting close to "search works for a real person typing real queries."

Here's what useful verification looks like:

JSON
{
  "id": "search-posts",
  "category": "public",
  "description": "User can search posts by keyword with relevant results",
  "status": "passing",
  "verification_steps": [
    "Navigate to /search",
    "Enter a keyword that matches a known post title",
    "Confirm the matching post appears in results",
    "Enter a keyword with no matches",
    "Confirm an empty state message appears",
    "Enter a partial word",
    "Confirm results still match"
  ]
}

Do a thing, expect a thing. Do another thing, expect another thing. An agent can walk through it. A human can walk through it. A Playwright script can walk through it. That's the bar: if you can't turn the steps into a Playwright test, you aren't done writing them.


Trade-offs and common mistakes

This system isn't perfect. I've messed it up in every way you'd expect and a few you probably wouldn't. Here's the damage, in roughly the order I inflicted it on myself.


I went too big too fast. 35 items on day one because I was being "thorough." Also completely overwhelming, for me and for the agent. 35 feature items as context is a lot of tokens and a lot of diluted focus. The agent kept referencing the wrong items, confusing similar-sounding features, generally thrashing. Scaled back to 10-15 and things smoothed out immediately. You can always add more once the first batch is stable and you've calibrated on what the right granularity feels like for your project.

Then I mixed sizes wildly. Some items were 10-minute tasks. Others would've taken a week. When "add alt text to images" sits next to "implement full-text search with relevance ranking" in the same list, the agent can't gauge what kind of effort it's signing up for. It treats them the same, which means it either over-engineers the small one or under-engineers the big one. If something feels 5x bigger than its neighbors, that's a strong signal it should be broken down.

Worse, I let the agent write its own descriptions. God, this was tempting. "Hey, you already know the codebase, just generate the feature list." And it did! And the list was wrong in ways that didn't become obvious until I'd already started using it. The agent that builds features should not define what "done" means. Writing your own exam and grading it. Descriptions come from you, or from a separate planning session with a different hat on. The coding agent's only job is flipping "status": "failing" to "status": "passing".

The one that actually hurt, though, was not maintaining the list. I refactored three modules, renamed files, changed function signatures. Forgot to update the feature list. Next session, the agent spent 20 minutes (real minutes, real tokens, real context window) trying to import from files I'd deleted, calling functions with signatures I'd changed. It was faithfully following instructions that were now lies. All because I couldn't spend two minutes updating a JSON file. A stale feature list is worse than no feature list. At least without one the agent starts fresh instead of building on top of bad information.


When to skip all of this: not every project needs a feature list. Prototyping something, hacking on a weekend project, poking at an unfamiliar API? Just talk to the agent. This system earns its keep in a pretty specific set of circumstances:

  • Your codebase has patterns the agent should respect, not reinvent from scratch

  • You're tracking features across multiple sessions, conversations that don't share context

  • You care about actually verifying that work is correct, not just that it compiles and doesn't throw

If none of that rings true, don't bother. You can always bolt on a feature list later when the project outgrows the ad-hoc approach.

One more scenario worth mentioning: greenfield projects. No existing code, no extraction to do. The feature list is the spec. Write it first, build from it. Simpler in some ways, because you're defining intent from scratch rather than reconstructing it from code that already exists. Everything in this post is specifically for the messier case where you've got a codebase and need to build the inventory retroactively.


The list is the contract

The Agent runtime environment (the "harness") might contain auto-startup scripts and progress tracking and whatever else. But the feature list is what actually steers the agent. Not documentation. A control mechanism. It decides where the agent looks and what it considers done.

Here's what actually changed once I started doing this: I stopped re-explaining my codebase at the top of every session. Stopped reviewing diffs where the agent had solved some adjacent problem I never asked about. Stopped squinting at output trying to figure out if the agent actually finished or just got 80% there and moved on, because now there were verification steps, and either they passed or they didn't.

5 sources. 3 rules. ~22 items. One JSON file.

Nobody's reading this JSON file for fun. It's a contract between you and your agent. Each item says: this is what the app does, this is how we prove it, and this is where it stands right now. Product features anchor the user-facing work. Technical features anchor refactoring, migrations, and performance. Between them they cover basically everything an agent might touch. I load the file as context when I start a session, and the agent works within those boundaries instead of improvising its own.

Quick reference if you're building your own:

Status values: pending (planned, no code yet), failing (exists but broken or incomplete), passing (works, verification steps succeed), deprecated (intentionally removed, kept so the agent doesn't rebuild it).

Categories: public, user, admin (role-based, who can access it), functional (crosses roles or doesn't belong to a single actor), technical (infrastructure, not directly user-observable).

The follow-up post will cover how the feature list drives multi-session workflows. Assigning items across sessions, tracking progress between them, wiring verification steps into E2E tests that actually run. The list stops being a one-time exercise and becomes the spine of an ongoing development loop. Once you have this extracted map, the question becomes how to make it move, and that means building an automated harness that lets the agent auto-load the feature list, run verification steps, and update status as it goes.

But honestly, if you want to try this, just go open your project's route directory. Pick 5 routes. Write 5 feature items in the JSON format above. 15 minutes, maybe. You'll know pretty quickly whether this approach makes sense for your project.

I'm still figuring this out as I go. If you've tried something like this (or tried it and bailed) I'd genuinely like to hear what happened.


Further reading

You are reading the second part of the 5-part series on Harnessing AI Agents. Here are the other parts and relevant resources:

END
© 2026 MoGHenry. All Rights Reserved. / RSS / Sitemap
Powered by Tanstack Start & Flare Stack Blog