Development WorkflowBranch A

A Role File Is a Class. A Project's Rule File Is an Instance.

Where knowledge lives when three stacks share one command, layer by layer from longest-lived to disposable.

Twelve skills, eighty percent the same file

Our repo is a monorepo, and not of one service. Backend, frontend, a set of services for the edge side, a ring of satellite services, each its own tech stack, with older services still migrating in. Eighty percent of the rules an AI agent needs are the same everywhere. The other twenty are per-project: which command runs the tests, what the coding style looks like, what not to touch.

Generation one of our workflow was a skill per role. Coder, tester, reviewer, team lead. The three stacks needed different knowledge, so every role got a prefix:

.claude/
└── skills/
    ├── be-coder/SKILL.md
    ├── be-tester/SKILL.md
    ├── be-reviewer/SKILL.md
    ├── be-team-lead/SKILL.md
    ├── fe-coder/SKILL.md
    ├── fe-tester/SKILL.md
    ├── fe-reviewer/SKILL.md
    ├── fe-team-lead/SKILL.md
    ├── edge-coder/SKILL.md
    ├── edge-tester/SKILL.md
    ├── edge-reviewer/SKILL.md
    └── edge-team-lead/SKILL.md

Twelve complete files. Each one self-contained: permissions, boundaries, project knowledge, all in the same file. be-tester alone was 400 lines. Lay the three coders side by side and eighty percent matched. Who you are, what you do, how you report back. None of that depends on the stack.

What I reported to the team at the time, roughly:

Each role is bloated and fits exactly one project. Every new project, even a new backend service, needs its own row of skills, and we cannot keep up with that. A feature that touches two projects has no team lead to call, or mixes two and gets imprecise.

Changing how the coder thinks meant editing three files. The twenty percent was mixed into the prose, so even copy and paste didn’t work. Projects kept arriving, and each arrival meant going back through the whole row.

It worked. We couldn’t afford to keep it.

One class, three instances

I was at the gym after work, turning over how to make agents handle per-project rules. Between sets it landed. This is the same object written three times, by copy and paste. So pull out a class and let each project be an instance. Abstract on top, concrete below. First lesson of object-oriented programming, applied to Markdown.

.claude/
├── skills/
│   ├── be-team-lead/SKILL.md
│   ├── fe-team-lead/SKILL.md
│   └── edge-team-lead/SKILL.md
└── agents/
    ├── coder.md
    ├── tester.md
    └── reviewer.md
projects/
├── backend/
│   └── .claude/
│       ├── coder.md
│       ├── tester.md
│       └── reviewer.md
└── frontend/
    └── .claude/
        ├── coder.md
        ├── tester.md
        └── reviewer.md

The message I sent the team, which is still how it works:

The subagent is dispatched as the root abstraction. At that point it knows only its role and the general rules: I am a coder, I write code. When it arrives at a project it reads the implementation rules there: I am in the backend, this is Go, this is the style. A new project merged into the monorepo brings only its own role files. Nothing else changes, and nothing leaks between stacks.

Within a day, a dozen skills came down. The per-stack team leads stayed as entry points for dispatch, and three role files replaced everything else. Every dispatch since has run the same way. The team lead sends a coder with a project path on the work order. The coder reads the root coder.md first: who it is, what it does, and that on arrival it reads the file of the same name. Then the project’s coder.md. The backend’s says wrap every error with context, never a bare panic. The frontend’s says API failures go through the error boundary, no per-call try/catch. Two files stacked make one coder that can start work.

The whole workflow has been torn down and rebuilt more than once since. The role files kept this shape every time. The list of classes hasn’t changed. Their bodies have been trimmed twice. The one pain the cut did not touch, a feature spanning two projects with no obvious team lead to call, went away only when the per-stack leads collapsed into a single /develop.

One thing the cut proves that I didn’t see at the time. Generation one did not die of dividing work into roles. It died of copying the roles per stack. Those are two different questions, and conflating them would have had me throw out the roles along with the duplication.

Knowledge lives at the layer its lifespan earns

The class and instance split was the first move. It took a few more months to see what it was an instance of: knowledge should live at the layer that matches how long it stays true. Longer-lived, higher up. Shorter-lived, closer to disposable. The field names and paths below are examples and will expire. The layering hasn’t.

Permanent principles. The root CLAUDE.md describes only its direct children. A middle layer is an index. The project layer is the actual document. Deduplication is by deletion, never by a pointer to another file. If two files say it, one of them loses the sentence.

The root has reversed its own definition once. It used to require each project’s CLAUDE.md to carry the full subtree, the tech stack and the commands. It now says never a directory tree or a stack list. Two of the trees had gone stale, and a listing the repo can produce on demand is not knowledge, it is a copy that rots. The root’s length is a series, not a constant: 37 lines, up to 42 at the worst, 33 now.

Machine contracts. Anything a machine can decide moved from Markdown into JSON. Each project has a project.json. The verify commands and the test scope live there and nowhere else, and the role files stopped restating them. The agent copies the field and runs it. No amount of prose asking “please run the full suite” matches one field that is actually executed.

Delivery documents. Written for QA, from what was actually built, never from the frozen plan. We used to have a spec-writer role and a tree of specs written before implementation. Both died together. The specs described intent. What QA needs is the delivered behaviour and the acceptance criteria, and those are written at the end.

The one-run baton. PLAN.md is the shortest-lived layer. It is created for one feature and discarded when the run is done. Two disciplines in it have outlasted every rewrite. Ownership is physical: a session writes only its own stack’s block, and the other stacks’ blocks are read-only, even when they look wrong. Flag, don’t fix. And status is evidence: a user saying “looks done” in conversation never flips a phase. A flip needs test output or a diff stat, and a user’s decision is recorded as a verbatim quote with the date.

What moves a rule between layers is who changes it

The frontend facts that sat in root until they went stale moved down to the frontend’s own file. They did not move because the backend was reading them, though it was. They moved because the people changing the frontend code couldn’t see them from where they worked, so nobody updated them.

The reverse happened too. A rule that had lived in one project’s coder file moved up to root when all three stacks ended up wanting the same thing. Same test, opposite direction. Who will change this, and can they see it from where they work.

One place that is explicitly not a layer: local memory. Root says flow knowledge lives only in git-tracked files, never in local memory. A rule on someone’s machine is a rule the rest of the team doesn’t have, and a fresh clone has to carry the whole workflow.

The class is where the rule is enforced

For a long time the role files described responsibilities and that was all. Now four of the seven carry a hook in their frontmatter. The reviewer’s tool list already had no Edit and no Write, so it could never silently fix what it flagged. But Bash can write too. Its Bash is now an allowlist of read-only commands, and anything else, sed -i included, exits with an error.

That changes what a class is. It is not just the description of a role. It is where the rule is executed, and an instance can’t override it. One tension, noted rather than resolved: one hook hardcodes a project’s path into a file in the class layer. Instance knowledge, living upstairs. The layering is a principle, and the repo violates it in at least one place.

Adding a project is new, changing one is editing the instance

Months on, a new project is a directory with a few files. The commit log shows it. A project that had only a coder and a reviewer gained its tester. A test harness got its own CLAUDE.md. Neither touched root or any sibling.

Changing the backend coder’s rules is one file, read only by coders dispatched to the backend. Break it and only the backend breaks. Not through discipline, because no other path reaches it.

Maintenance became one file at a time. Open the project, open the role, write a few lines, done. The agents stay narrow. The human gets to extend freely.


Same class.
Your instance, my instance.

An edge-side colleague and I were arguing about code comments, the oldest argument there is. I think they should barely exist. They think if the AI wants to write one, let it. We never settled it, and we didn’t need to.

I added one rule to my project’s coder file. Their project is entirely theirs. We call the same name every day, and the coder is terse in my project and chatty in theirs.