Making a real repo agent-legible — a before/after

You bought the agent seats. The PRs still get rejected. The problem is not the tool — it is that the repo gives an agent nothing to aim at. Here is what changes when you fix that.

The setup

This is my most substantial codebase: a production mobile-plus-services monorepo, ~118 commits, React Native + Node/Fastify + Postgres. It is a realistic case: no contract layer an agent can lean on, module boundaries that only exist in people’s heads, tests that pass whether or not the change is right. I ran the Repo Readiness Review on it — read access, scored agent-legibility, made one day of harness changes, then had an agent do a real ticket. Below is the before, the changes, the ticket, and the after. The repo itself stays private; every excerpt here is redacted or renamed.

The score — before and after

Scored 1–5 on five dimensions. 1 = an agent is guessing; 3 = written down but not enforced; 5 = drift fails the build.

DimensionBeforeAfter 1 dayWhat changed
Contracts34.5Shared Zod schemas existed; the API contract (error envelope, error-code table, pagination, id/timestamp formats) was only inferable from code. Added a written api-conventions doc. Not 5 — conformance is honoured, not gated.
Module boundaries23Vertical-slice modules, consistent in the tree, nothing documenting or enforcing them. Now documented and cited in the rule file. Still human-review only.
Test guardrails34Real integration suite against live Postgres/Redis in CI. Added a written testing standard plus verification subagents that replay the CI gate locally before commit. Not 5 — no coverage threshold enforced.
Rule files15No CLAUDE.md / AGENTS.md at all. Added one: fixed stack, exact layout, an ordered before-done gate, typed-error and logging rules, plus four scoped subagents and a permissions config for safe autonomy.
CI enforcement33Unchanged. The CI file is byte-identical. The harness added rules and local verification, not a new CI job. Everything past a 3.9 composite is CI making the written rules fail the build.
Composite2.43.9+1.5 in a day, driven almost entirely by the rule file and by writing the conventions down.

The baseline was strong where tooling already existed and blank where an agent needs instructions. The cheap, high-leverage half — write down the stack, the conventions, the contract; wire verification subagents — is a day. Making CI enforce boundaries, coverage, and contract conformance is the next engagement, not the same day. Saying so plainly is part of the point.

What one day of harnessing changed

Redacted and renamed below — proprietary logic kept out.

  1. CLAUDE.mdFixed stack ("do not propose alternatives"), the exact module file pattern cited by line into the conventions doc, an ordered "before done" gate (typecheck && lint && test, then a curl smoke test, tick acceptance criteria, commit on a branch, never push without asking).
  2. conventions.mdSchema library as the source of truth, no enums, vertical slices, a naming table, typed errors from a shared errors module, "don’t write code without writing tests", a "what NOT to do" list.
  3. api-conventions.mdThe contract shape: a standard error envelope with a details payload, a stable error-code table, cursor pagination, ULID ids, ISO-8601 UTC timestamps.
  4. Four scoped subagentsdb-migration-runner, endpoint-smoke, module-verifier, seed-fixtures. Each has hard rules, a narrow tool set, and a right-sized model. module-verifier replays the CI gate locally before a commit.
  5. .claude/settings.jsonacceptEdits with a tight command allow-list and an explicit deny of git push, rm -rf, git reset --hard. Safe autonomy instead of approving every step.

The ticket

Scope given to the agent: a booking stays open for a few minutes past its scheduled time before a sweep expires it. An assignee must not be able to claim a booking whose scheduled time has already passed. Add the guard, with a test. Run cold in both trees — same prompt, same model.

Both produced working code that passed typecheck, lint, and the full suite. The difference was reviewability. Domain terms below are renamed and the diffs trimmed to the illustrative hunks; a shared one-line error-code addition is identical in both trees and omitted.

Before the harnessAfter
Servicelogic inlined into the claim flowextracted as a named guard next to the file’s existing guard helpers
Time sourceDate.now() hardcoded in the branchnow: Date injected — testable, matches the other helpers
Errormessage + code, 2 argsmessage + code + { scheduledDate, scheduledTime } — the details the contract requires
Testasserts the setup booking's status before the real check; generic nameasserts only the behaviour; name follows the convention’s "describe behavior" rule

Before — baseline tree

apps/api/src/modules/bookings/bookings.service.ts
@@ -344,6 +344,14 @@
if (booking.status !== 'open') {
throw new ConflictError('This booking is no longer open', 'BOOKING_NOT_OPEN');
}
+ // A booking can still be `open` for a few minutes past its scheduled time — the
+ // expiry floor (now + MIN_CLAIM_WINDOW) keeps near-term bookings claimable, so
+ // a booking created shortly before its scheduled time outlives that time.
+ // Once that instant has passed a new claim is useless: reject it.
+ const scheduled = localToUtc(booking.scheduledDate, booking.scheduledTime);
+ if (scheduled.getTime() < Date.now()) {
+ throw new ConflictError('This booking is past its scheduled time', 'BOOKING_WINDOW_PASSED');
+ }
const existing = await repo.findClaimByBookingAndAssignee(bookingId, actorId);
apps/api/src/modules/bookings/bookings.test.ts
@@ -270,6 +270,25 @@
expect(dup.json().error.code).toBe('DUPLICATE_CLAIM');
});
 
+ it('claim on a booking whose scheduled time has passed is 409 BOOKING_WINDOW_PASSED', async () => {
+ const { owner, assignee, team } = await setupTeamWithAssignee();
+ // Scheduled a few minutes ago: accepted at creation via the submit grace, and
+ // the expiry floor keeps it `open` — but its scheduled time is already past.
+ const create = await createBookingAs(owner.auth, team.id, localDateTime(-3));
+ expect(create.statusCode).toBe(201);
+ expect(create.json().status).toBe('open');
+ const bookingId = create.json().id;
+
+ const claim = await app.inject({
+ method: 'POST',
+ url: `/api/v1/bookings/${bookingId}/claims`,
+ headers: { authorization: assignee.auth },
+ payload: {},
+ });
+ expect(claim.statusCode).toBe(409);
+ expect(claim.json().error.code).toBe('BOOKING_WINDOW_PASSED');
+ });
+
it('claiming is standalone — no overlap warning even with a clashing assignee schedule', async () => {

After — harnessed tree

apps/api/src/modules/bookings/bookings.service.ts
@@ -139,6 +139,21 @@
return scheduled;
}
 
+ /**
+ * A booking can linger `open` for a few minutes past its scheduled time before
+ * the expiry sweep flips it — don’t let someone claim a booking that has
+ * already passed.
+ */
+ function ensureWindowNotPassed(booking: Booking, now: Date): void {
+ const scheduled = localToUtc(booking.scheduledDate, booking.scheduledTime);
+ if (scheduled.getTime() < now.getTime()) {
+ throw new ConflictError(
+ 'This booking’s scheduled time has already passed',
+ 'BOOKING_WINDOW_PASSED',
+ { scheduledDate: booking.scheduledDate, scheduledTime: booking.scheduledTime },
+ );
+ }
+ }
+
async function ensureNoOwnerOverlap(input: {
@@ -344,6 +359,7 @@
if (booking.status !== 'open') {
throw new ConflictError('This booking is no longer open', 'BOOKING_NOT_OPEN');
}
+ ensureWindowNotPassed(booking, new Date());
const existing = await repo.findClaimByBookingAndAssignee(bookingId, actorId);
apps/api/src/modules/bookings/bookings.test.ts
@@ -270,6 +270,23 @@
expect(dup.json().error.code).toBe('DUPLICATE_CLAIM');
});
 
+ it('rejects a claim once the booking’s scheduled time has passed (409 BOOKING_WINDOW_PASSED)', async () => {
+ const { owner, assignee, team } = await setupTeamWithAssignee();
+ // Scheduled a few minutes in the past: still inside the create-side submit grace, so
+ // the booking is born `open` and lingers open until the expiry sweep — but a claim
+ // must not succeed once its scheduled time has passed.
+ const bookingId = (await createBookingAs(owner.auth, team.id, localDateTime(-3))).json().id;
+
+ const res = await app.inject({
+ method: 'POST',
+ url: `/api/v1/bookings/${bookingId}/claims`,
+ headers: { authorization: assignee.auth },
+ payload: {},
+ });
+ expect(res.statusCode).toBe(409);
+ expect(res.json().error.code).toBe('BOOKING_WINDOW_PASSED');
+ });
+
it('claiming is standalone — no overlap warning even with a clashing assignee schedule', async () => {

What a senior sends back on the before PR: use the guard-helper pattern; inject now; add the details payload; stop asserting setup state. The after PR draws none of those. Same model, same ticket — the repo was the variable.

After — where it lands

Composite 2.4 → 3.9 in a day. The remaining ~1.1 to a 5 is CI-enforcement work — import boundaries, a coverage gate, a response-contract test — which is the Agent Harness Build, not an afternoon.

What this means for your repo

The seats are not the problem, the repo is. This is a 3–5 day review — read access to one or two repos, a scored fix list, and a live demo of an agent doing one of your real tickets before and after. Then the choice to go further is yours.

The full walkthrough — the private repo, the actual rule sets, the CI config — happens on a call.