[All writing]

Scoping Agent Rules Like Routes, Not Like Prompts

Jeff Liu//5 min read

Most agent instruction systems start as one growing file. Every new edge case gets appended. Every session then pays the token cost of every past edge case, whether or not it applies to the current task. That approach breaks well before it feels broken. The failure mode is silent, the agent still runs, it just gets worse at following the parts buried on page four.

The fix that actually held up came from treating rules like routes, not like one long prompt.

Glob-scoped rules, not one flat file

Each rule file carries a YAML frontmatter block declaring which file paths it applies to. A rule about database migrations only loads when the current task touches something under a schema directory. A rule about test placement only loads for files matching a test-adjacent path. The agent's context at any moment holds only the rules relevant to what it is actually doing. Not the full accumulated history of every edge case anyone has ever hit.

The consolidation happened in one commit, April 8, dissolving three separate rule monoliths, a Linear-workflow file, a feature-package-standards file, and an anti-patterns file, into nine distinct micro-rules with YAML glob boundaries attached to each. Three weeks later, on April 30, a second pass merged overlapping groups further. Forty-seven separate rule files became thirty-one, a 34% reduction, and almost none of that was deleted content. Most of it was finding the same guidance stated three different ways in three different files, then merging it into one, scoped correctly.

One routing table, not a routing table per caller

A related problem showed up in how the agent called external tools. Two separate MCP servers handled Linear operations, and not by choice. The third-party Linear server did not support project updates or milestone creation, so I reverse-engineered the API routes and built those two into my own server. The split was real and undocumented, until an April 8 commit wrote it down as a rule that loads only when the agent is touching a plan or a task file.

markdown
---
description: "Resolving MCP Tool Splits across Linear Workflows"
globs: ["**/implementation_plan.md", "**/task.md", "**/walkthrough.md"]
---

# Linear Workflow: Hybrid MCP Server Matrix

Linear operations are split across TWO distinct MCP servers (`ts-core` and
`linear-mcp-server`). Do NOT consolidate to one — each handles items the other cannot.

## 1. The TS-Core Matrix
These two specific execution vectors MUST be handled by `ts-core` because the
third-party linear server does not support them:
- **Project updates:** `linear_create_project_update`
- **Milestone creation:** `linear_create_milestone`
*Hallucination Trap:* Do not invent ts-core tool names like `linear_create_issue`.

That last line is the one that matters. It is a rule written to stop the model inventing a tool name, which is the same problem the gate below solves, eight months earlier and with prose instead of code.

If you have built a task queue with more than one worker type, you recognize the shape. You do not want dispatch logic re-derived at every call site. You want it declared once, in a place every caller reads from. A routing change becomes one edit instead of a grep-and-pray across the codebase.

An artifact with a lifecycle, not a scratch file

The part built specifically to survive context loss between sessions is a pair of small markdown templates, task.md and implementation_plan.md. They get treated as artifacts with an actual lifecycle, not disposable notes, a rule shipped February 11 and hardened again February 28, the same day as an unrelated directory rename. A task file opens with a progress header. Every item is a checkbox. A rule blocks overwriting either file without first checking whether the existing content is already marked complete. That check is the real mechanism, not a convention people are trusted to remember.

markdown
> **Progress: 0/N complete** | Context: 🟢
> ✅ Done | 🔄 Current | ⬜ Remaining

- [ ] Task 1
- [ ] Task 2

When a session ends mid-task, it writes a handoff block to the bottom of the same file before stopping.

markdown
## Session Pickup (for next agent)
- **Last completed:** [specific item]
- **In progress:** [what is mid-flight]
- **First action next session:** [exact tool call or command to run]
- **Uncertain about:** [questions/assumptions to verify]
- **Key files touched:** [list of modified files]

The next session, whether that is you or a different agent instance, reads that block first. State does not live in a chat transcript that eventually scrolls out of context. It lives in a file, checked into version control, that any future session can open cold.

A Definition of Done gate that requires evidence, not a claim

The last piece closes the gap between an agent saying a task is finished and a task actually being finished. Before any ticket gets marked done, a rule requires running a real tool call, a grep or a file read, to verify each item in that ticket's Definition of Done against the current state of the code. If an item cannot be verified that way, the ticket does not move to done. It does not matter what the agent's own summary claims. The most recent piece of this system, shipped August 29, extends the same discipline one step further, a gate that records every commit against its tracked ticket automatically, so work that ships straight to the main branch still leaves a trail.

This exists because the failure it prevents is common. An agent reports success based on what it intended to do, not what it actually verified. Nothing downstream catches the gap until you build on top of the missing piece. Requiring tool-call evidence before a status change turns "I did this" into a claim that has to survive a second, independent check before it counts.

None of this is exotic, and none of it happened in one sitting. It spans February through August. Five real commits. Each solved one specific failure the previous one exposed. It is the same discipline any infrastructure team applies to a routing layer or a queue. Scope what loads. Centralize what dispatches. Persist state outside the ephemeral request. Verify before you mark something complete. The only real difference is that the requests here are conversations, and the workers are language models instead of processes.