Teaching the Agent Our Craft: Structured Agentic Development on a Real Codebase

Teaching the Agent Our Craft: Structured Agentic Development on a Real Codebase

Our small dev team went from a blank slate to a platform helping patients overcome chronic pain

Alex Haldeman

July 06, 2026

The Mission

We recently partnered with a startup that had developed a clinically proven approach to alleviating neuroplastic chronic pain. Their program worked: a coach-led model that helped patients ease chronic pain at a lower cost than conventional treatment. The problem was reach. The in-person model could not scale to the demand they were seeing, and there were not enough coaches to close the gap. We were brought in to build a digital platform that could deliver the program to every patient who needed it.

Our Development Philosophy and Inspiration

At 8th Light, we approach agentic development the same way we approach any software engagement: with discipline around test-driven development, clean architecture, and code that is built to embrace change. But agentic development comes with its own failure modes. An agent produces code that covers the happy path and misses critical behaviors. A context window fills with stale reasoning from earlier attempts, and the agent starts working against itself. Without explicit conventions, the output works but looks like nobody on the team wrote it.

Tyler Burleigh's Research-Plan-Implement gave us a useful frame for thinking about this. His core observation: the bottleneck is not code generation, it is ensuring the model understands what to build before it starts building. RPI addresses that by separating research, planning, and implementation into distinct phases, each with a review cycle before the next begins. We adapted that structure into our Claude Code workflow, with the additional goal of keeping product managers and designers genuinely in the loop at each transition, not just developers.

What follows is a description of the harness we built from a Claude Code-specific perspective.

The Harness

Structure

Before walking through the pieces, it helps to see how they fit together. Everything that teaches the agent our craft lives in a handful of files at the project root and inside a single .claude/ directory. None of it is application code. It is the scaffolding that sits alongside the code and shapes how the agent works within it.

The map below ignores the application source entirely and shows only that scaffolding. Each branch is a separate part of the framework, and the sections that follow zoom into them one at a time.

pain-management-app/
├── CLAUDE.md            # the knowledge root: project context, critical rules, naming conventions, architecture
├── .mcp.json            # connections to the outside: Linear (product), Figma (design), internal services
└── .claude/
    ├── rules/           # path-scoped rules that codify how we write code, auto-loaded by file path
    ├── skills/          # the workflow: story-writer → tdd-build → pr-summary → code-review
    ├── agents/          # a test-writer subagent that writes tests with no view of the implementation
    ├── hooks/           # guardrails that enforce the boundaries (e.g. the test-writer touches only test files)
    └── commands/        # small routine helpers for linting, type-checking, and installs

 

The root of our knowledge

Everything starts from a single file. CLAUDE.md is the one place the agent has to find on its own, and it carries the project context that applies everywhere: what we are building, the principles we hold to, the conventions and critical rules that span the whole codebase, and the pointers to everything else the agent should load. From there it reaches the path-scoped rules and the rest of the framework.

The critical rules that live here are short and unambiguous, the kind of thing that is obvious to the team and invisible to a fresh model. They tend to encode the habits you would otherwise repeat in code review:

1. Don't write speculative code. Every function, endpoint, or field
   must be used by something in the same change.
2. Tests come before implementation, and assert behavior, not internals.
3. Reuse an existing component or utility before introducing a new one.
4. Comments explain WHY, not WHAT.

Getting this root right matters more than any single rule beneath it, because every other part of the framework is reached through it.

 

Codifying our approach in a context-aware way

The conventions a senior developer carries in their head, such as how to structure a query, which test selector to reach for first, or where business logic belongs, don't come from a single global rule. They depend on where you are in the codebase. The same is true for the agent. Loading everything the agent might ever need into one file would work, but it wastes context and buries the relevant guidance under rules that don't apply to the task at hand.

Claude Code's path-scoped rules solve this directly. Each rules file declares the paths it governs in its frontmatter, and Claude loads it automatically when the agent works with matching files. The anatomy is simple:

---
paths:
  - "frontend/src/**/__tests__/**/*"
  - "frontend/src/**/*.test.*"
---

# Frontend Testing
... rules for testing go here

A rules file targeting backend source files never loads during a frontend task. That keeps context lean and, more importantly, keeps the guidance specific. The agent working on a backend service sees the Router → Service → Repository architecture rules. The agent writing a frontend test sees the testing conventions. Neither has to wade through the other's rules.

That specificity is what made the rules files pay off on this project. One example: our frontend testing rule makes explicit when to reach for each Testing Library selector.

- `getBy*`: Element is already in DOM (sync, throws if missing).
- `findBy*`: Wait for element to appear (async, throws if missing). Use instead of `waitFor` + `getBy`.
- `queryBy*`: Assert element is NOT present (sync, returns null).

This is the kind of guidance a developer picks up after a few code reviews. Without it, an agent reaching for waitFor + getBy when findBy is the right tool will produce tests that work but don't match how the team writes them. With it, the first draft is already consistent. The same held for our backend architecture rules, our API layer conventions, and our state management patterns. The rules files turned each of those into things the agent just knew. More importantly, when we noticed the agent diverging from our conventions, or spotted a better approach ourselves, we had a feedback loop for folding that lesson back in.

The backend rules carry the same kind of specificity. The architecture rule does not just say "use Router → Service → Repository", it shows the dependency wiring the agent is expected to produce, down to creating the repository inside the DI function:

async def get_playlist_service(
    session: Annotated[AsyncSession, Depends(get_session)],
    catalog_client: Annotated[CatalogClient, Depends(get_catalog_client)],
) -> PlaylistService:
    repository = PlaylistRepository(session)
    return PlaylistService(repository, catalog_client)

PlaylistServiceDep = Annotated[PlaylistService, Depends(get_playlist_service)]

With that in front of it, the agent writes a new service the way the team writes the rest: HTTP concerns in the router, business logic in the service, SQL confined to the repository.

 

Creating collaboration through integration

Engineering knowledge was now codified, but product and design still lived in separate tools the agent could not see. We needed a way to bring them into the same workflow. We used Linear for backlog management and roadmap planning, and Figma for our design system and UI flows. Model Context Protocol (MCP) is an open standard that gives AI agents a uniform way to connect to external tools and services. Rather than copying context from a Linear ticket or a Figma file into a prompt, the agent reads from and acts on those tools directly, the same way a developer tabs between their editor and a browser.

To share these connections across the team, we committed an .mcp.json file to the repository. Any developer who clones the repo picks up the full set of integrations immediately. The file also handles connections that require more care, whether that is a custom setup script for local tooling or an API token that cannot be committed to source control:

{
  "mcpServers": {
    "linear": {
      "type": "http",
      "url": "https://mcp.linear.app/mcp"
    },
    "figma": {
      "type": "http",
      "url": "https://mcp.figma.com/mcp"
    },
    "ehr-service": {
      "command": "node",
      "args": [
        "${EHR_SETUP_PATH}"
      ]
    },
    "cms-service": {
      "type": "http",
      "url": "https://mcp.fake-cms-service.io",
      "headers": {
        "Authorization": "Bearer ${FAKE_CMS_MCP_TOKEN}"
      }
    },
    "some-docs-server": {
      "type": "http",
      "url": "https://some-docs-server.com/api/mcp"
    }
  }
}

MCP servers are not limited to collaboration tools. We connected our EHR and CMS services the same way, which made a noticeable difference in code quality and troubleshooting. One example stands out: a developer connected the Figma MCP, pulled the design system directly into the agent's context, and built a 1-to-1 match using a custom design tokens page backed by real application components. Whenever the design and the code drifted, re-syncing was a single command rather than a manual audit.

 

Building our workflow

Codified knowledge and connected tools still left one thing open: structure for the work itself. We built two skills that map directly onto RPI's phases: /story-writer handles research and planning, /tdd-build handles implementation.

/story-writer

The skill starts at Linear. Give it a ticket ID and it reads the existing description. Give it nothing and it creates a new ticket and works from there. Either way, it then explores the codebase before drafting anything, not because it needs to write code, but so the implementation steps it produces are grounded in the project's actual structure. A step that references the right domain folder or calls out a real constraint is more useful than a generic checklist.

What makes it feel collaborative rather than generative is the interview loop. The agent asks clarifying questions in conversation until the requirements are specific enough to write acceptance criteria against. Once both parties are satisfied, it writes the story in a structured format: an overview, implementation steps pitched at what to build, not how, and acceptance criteria that can be verified in the app. The approved story goes back to Linear and becomes the shared source of truth for the whole team.

The structured format is fixed, which is what makes the output reviewable instead of a wall of prose:

Overview
- Purpose and scope of the story, in a few sentences

Implementation Steps
- Meaningful units of work grouped by concern (Backend, Frontend, Testing),
  pitched at what to build, not a file-by-file how

Acceptance Criteria
- Checks someone can actually perform in the running app

One habit we developed: including the Figma MCP link for the relevant design file directly in the story description. When /tdd-build reads the ticket to start implementation, it can pull the design into its context through the same MCP connection and work with the actual component designs rather than a written description of them.

/tdd-build

The skill reads the Linear ticket, explores the relevant parts of the codebase, then enters plan mode and proposes the implementation as a numbered list of TDD cycles. Each cycle specifies a test file, a behavioral spec, the exact command to run, and a RED gate reason: why the test should fail before any implementation exists.

Cycle 1: GET /playlist/{id} returns playlist with tracks
- Test file: backend/tests/playlists/test_playlist_router.py
- Test spec: GET /playlist/{id} returns 200 with {id, name, tracks[{id, name}]}
- Test command: cd backend && python -m pytest tests/playlists/test_playlist_router.py -v
- RED gate: endpoint does not exist
- Implementation: add GET /playlist/{id} router, service, and repository
- Files: backend/src/playlists/router.py (new), service.py (new), repository.py (new)

Nothing gets written until the developer has reviewed and approved the plan.

Once approved, the build begins. For each cycle, the main agent dispatches the test-writer subagent with the behavioral spec as its only context. The subagent starts from a fresh context window and has no knowledge of the implementation the main agent is about to write. That separation is deliberate. A test written with the implementation already in view tends to describe that implementation, not the behavior we actually care about. Working from a behavioral spec keeps the test focused on what the system should do. The subagent runs the tests, confirms they fail for the right reason, and returns a RED gate report. Only then does the main agent write the minimum code to make them pass.

The fresh context also means the subagent cannot drift toward the implementation. With nothing to glimpse, it has no shortcut to rationalize and no way to shape its tests around how the code happens to be structured.

The subagent is configured by a declarative definition that sets how it behaves and what it can access. Its frontmatter names the only tools it can reach for and registers the hook that will police them:

---
name: test-writer
description: Writes failing tests for a single TDD cycle, isolated from any implementation.
tools: Read, Glob, Grep, Write, Edit, Bash
hooks:
  PreToolUse:
    - matcher: "Edit|Write"
      hooks:
        - type: command
          command: "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/restrict_test_writer_paths.py\""
---

Listing the hook in frontmatter is what wires it in, but the enforcement lives in the script itself. Even with a prompt that explicitly forbids touching implementation files, a sufficiently confused agent might try, so restrict_test_writer_paths.py is the hard backstop. It fires on every Edit and Write the subagent makes and checks the target path against an allowlist:

ALLOWED_PATTERNS = (
    re.compile(r"(?:^|/)backend/tests/"),
    re.compile(r"(?:^|/)__tests__/"),
    re.compile(r"\.test\.[^/]+$"),
    re.compile(r"(?:^|/)frontend/src/testing/"),
)

The matching runs only for the test-writer, so every other agent (including the main conversation) passes through untouched. When a path fails the check, the script hands Claude Code a structured decision before the file is ever touched:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "test-writer may only write test files. Refusing to modify <path>."
  }
}

The reason comes back to the agent so it can report clearly rather than retry. The prompt states the intent, and the hook is what actually enforces it.

 

The results

The clearest outcome was how much a small team could carry. A pair of developers took on a scope that would normally demand more hands: a multi-tenant platform, an AI coaching companion bounded by real safety constraints, and a data architecture built to keep sensitive information out of reach. The harness was what made that possible. It let two people move through that scope quickly without the work looking like two people rushing, because the conventions, the architecture rules, and the test-first cycle were already encoded for the agent to follow.

Speed was not the only thing we watched. Scope stayed under control as the work went on, rather than being quietly dropped to keep up the appearance of speed. That is usually what separates genuine progress from debt you only discover later. The same structure that kept the agent honest, research before planning and a failing test before any implementation, is what kept the codebase coherent under pressure.

The integration paid off just as much away from the keyboard. Wiring Linear and Figma into the agent's workflow through MCP collapsed the lag between a design decision, a product priority, and the code that implemented it. Re-syncing a built component against an updated design became a single command instead of a manual audit, and the feedback loop between engineering and the rest of the team got tighter in a way that had previously been tedious.

This approach was an accelerant to our team's abilities, not a replacement. The harness did not execute everything on its own. Engineers still stepped in to scaffold code when a cleaner structure was needed, or to adjust how a test was written, and then folded that lesson back into the rules so the agent carried it forward. That loop, where engineers kept improving the harness and the harness let them cover more ground, is what made the approach worth keeping. What started as experimentation on one engagement is now a repeatable practice we can bring to other work.

 

How to get started

Sometimes, the biggest challenge to making innovation "stick" is the drag of organizational momentum. Have an idea, workflow, or challenge that has been a blocker for your business? Let's talk.

Spark a conversation >

Alex Haldeman

Lead Engineer

Alex is a full-stack engineer and consultant with over a decade of experience building software across healthcare, finance, government, and defense. His work spans backend systems, frontend applications, and cloud infrastructure, giving him a broad view of how the pieces of a product fit together and where the hard problems tend to surface. In recent engagements he has focused on AI-powered systems and agentic development, exploring how these tools change the way software gets designed, built, and shipped. He works in a test-driven, agile style and places equal value on engineering fundamentals and the newer practices reshaping the field. Across his engagements he has taken projects from early architecture through delivery, often serving as the technical lead on cross-functional teams, and he regularly mentors the engineers he works alongside. He's interested in writing about the craft of building software and what he is learning as the tools and the work continue to evolve.