Topic 05

When to Write It Yourself

Specification

Some changes are quicker to type than to describe. Some code has to pass through your own head to be understood. And some skills only grow when you do the work yourself. For those, the agent is the slower or the worse tool, even when it would produce exactly the same lines.

This page is the judgement for a single task, the one in front of you today. Chapter 14 asks the same question of whole kinds of work.

Smaller Than Its Explanation

Renaming a variable in one function, fixing a typo in an error message, adding the argument the type checker just named: for these, the instruction, the wait and the reading cost more than the edit. Every agent change has a floor under it, the time to specify it plus the time to read the result, and no model speed lowers that floor, because both halves are yours.

At Cragmoor the list of such changes is long and ordinary: the typo in the schedule comment, a log message that names the wrong module, a test whose name no longer matches what it checks, an argument added after a type error that names it exactly. Each takes less than a minute by hand. Each, sent to the agent, takes a minute to describe, a wait and a diff to read, and the diff sometimes comes back with something extra in it.

Typing it yourself against asking for it, for two sizes of change (illustrative times)
A 3-line changerename, typo, one argument
Type it: about 1 minute.
Ask for it: describe, 1 minute; wait, under a minute; read the diff, 1 minute.
The floor alone is longer than the edit.
A 300-line changea new endpoint with its tests
Type it: most of an afternoon.
Ask for it: specify, 10 minutes; wait, a few minutes; read and correct, 30 to 40 minutes.
Checking is much faster than writing, and the agent earns its keep.

The numbers in the figure are illustrations, not measurements. The shape is what matters: the fixed costs of asking are roughly the same at any size, so they dominate small changes and vanish into large ones.

Code You Need to Understand Deeply

The waitlist's offer logic is the Cragmoor example. Since the double offer of Chapter 2, whose cause Chapter 10 finds, it is known to be code where two things happen at once and one ordering of them is the bug. Reading a correct-looking diff of it is weaker than writing it, because writing forces you through every interleaving, one decision at a time, and reading lets you agree with each line without ever asking what happens if the other request arrives between this line and the next.

That does not put the agent out of the room. It can map the callers before you start, write tests that try to break your version, run them a thousand times, and review what you wrote. You write the core and own the reasoning; the agent attacks it.

The test of whether a piece of code belongs here is simple: if it broke at three in the morning, would you be able to reason about it without the agent's help? For the waitlist offer logic, Cragmoor's team decided the answer had to be yes, after the double offer of Chapter 2. Money and access checks fall under the same test, and Chapter 14 widens it from single tasks to whole areas of the codebase.

The Part You Are Learning

If the language's concurrency tools or the test runner's fixtures are new to you, the agent will produce working code and you will learn to accept it. Accepting is not learning. Write the first few yourself, slowly, with the documentation open, then let the agent write the next ones while you review them against what you now know. Chapter 14 takes this further, into which skills fade when you stop exercising them.

At Cragmoor the case came up in your second week. The booking service's tests use the test runner's fixtures heavily, and you had never written one. The agent could produce a fixture for the refunds tests in seconds, and it would have worked. You wrote the first two yourself, with the runner's documentation open, got the scope of one of them wrong, and found out why it mattered when a test leaked state into the next one. After that, reviewing the agent's fixtures took a minute each, because you knew what to look for.

When the Agent Is Stuck

After the same wrong fix twice, the third attempt is usually the same guess in different words (Chapter 10 has the full pattern). The fastest route is often to write the fix yourself, using what the failed attempts showed about the problem. Two failed attempts are not wasted: each one ruled out an approach and usually exposed the line where the real difficulty sits.

A Quick Test Before You Delegate

Three questions settle most cases in under a minute. Would describing the change take longer than typing it? Do you need to understand this code well enough to reason about it alone later? Are you still learning the thing the change exercises? A yes to any of them is a reason to type it yourself, or at least the core of it. Three noes, and the change is the kind the agent does well: bounded, checkable, and faster to read than to write.

Writing It Yourself Is Still the Loop

Skipping the agent never means skipping the stages from Chapter 2. You still specify, even if only in your head. You read your own diff before committing it, with the same questions Chapter 6 asks of the agent's. The tests decide whether it works, and you commit or discard. Your own code earns no exemption from review: you wrote it with intent, but intent is exactly what makes your own mistakes hard to see.

The Refund Change, Honestly

After the plan from earlier in this chapter, step three is one changed value and the tests that move with it. It is a few lines either way. One engineer on Cragmoor's team types it, another asks the agent for it, and both are fine, as long as the diff is read and the tests are the table's. Here is what the step looks like typed by hand, against the commit that ended step two.

Step three, typed by hand: git diff
diff --git a/src/cragmoor/refunds.py b/src/cragmoor/refunds.py
--- a/src/cragmoor/refunds.py
+++ b/src/cragmoor/refunds.py
@@ -13,7 +13,7 @@ if TYPE_CHECKING:
 
 # Full refund for a cancellation at least this long before the class.
 # The cancellation screen in booking shows the same deadline.
-FULL_REFUND_WINDOW = timedelta(hours=24)
+FULL_REFUND_WINDOW = timedelta(hours=12)
 
 
 def refund_deadline(starts_at: datetime, gym: Gym) -> datetime:
diff --git a/tests/test_refunds.py b/tests/test_refunds.py
--- a/tests/test_refunds.py
+++ b/tests/test_refunds.py
@@ -32,7 +32,7 @@ def test_cancel_25_hours_ahead():
 
 def test_cancel_23_hours_ahead():
     booking = drop_in()
-    assert refunds.refund_cents(booking, hours_before(booking, 23)) == 0
+    assert refunds.refund_cents(booking, hours_before(booking, 23)) == 2400
 
 
 def test_cancel_one_hour_ahead_is_not_refunded():
@@ -42,4 +42,4 @@ def test_cancel_one_hour_ahead_is_not_refunded():
 
 def test_deadline_shown_at_second_zone_gym():
     booking = drop_in(SECOND_ZONE)
-    assert cancellation_notice(booking) == "Free cancellation until 2026-11-13 09:00"
+    assert cancellation_notice(booking) == "Free cancellation until 2026-11-13 21:00"
Step three, typed by hand: git diff
diff --git a/src/refunds.ts b/src/refunds.ts
--- a/src/refunds.ts
+++ b/src/refunds.ts
@@ -7,7 +7,7 @@ const HOUR_MS = 60 * 60 * 1000;
 
 // Full refund for a cancellation at least this long before the class.
 // The cancellation screen in booking shows the same deadline.
-export const FULL_REFUND_WINDOW_MS = 24 * HOUR_MS;
+export const FULL_REFUND_WINDOW_MS = 12 * HOUR_MS;
 
 /** The last moment a cancellation still gets a full refund. */
 export function refundDeadline(startsAt: string, gym: Gym): Date {
diff --git a/tests/refunds.test.ts b/tests/refunds.test.ts
--- a/tests/refunds.test.ts
+++ b/tests/refunds.test.ts
@@ -31,7 +31,7 @@ test("cancel 25 hours ahead", () => {
 
 test("cancel 23 hours ahead", () => {
   const booking = dropIn();
-  expect(refunds.refundCents(booking, hoursBefore(booking, 23))).toBe(0);
+  expect(refunds.refundCents(booking, hoursBefore(booking, 23))).toBe(2400);
 });
 
 test("cancel one hour ahead is not refunded", () => {
@@ -41,5 +41,5 @@ test("cancel one hour ahead is not refunded", () => {
 
 test("deadline shown at second-zone gym", () => {
   const booking = dropIn(SECOND_ZONE);
-  expect(cancellationNotice(booking)).toBe("Free cancellation until 2026-11-13 09:00");
+  expect(cancellationNotice(booking)).toBe("Free cancellation until 2026-11-13 21:00");
 });
Step three, typed by hand: git diff
diff --git a/src/main/java/org/cragmoor/Refunds.java b/src/main/java/org/cragmoor/Refunds.java
--- a/src/main/java/org/cragmoor/Refunds.java
+++ b/src/main/java/org/cragmoor/Refunds.java
@@ -8,7 +8,7 @@ import java.time.ZonedDateTime;
 public final class Refunds {
   // Full refund for a cancellation at least this long before the class.
   // The cancellation screen in Booking shows the same deadline.
-  public static final Duration FULL_REFUND_WINDOW = Duration.ofHours(24);
+  public static final Duration FULL_REFUND_WINDOW = Duration.ofHours(12);
 
   private Refunds() {}
 
diff --git a/src/test/java/org/cragmoor/RefundsTest.java b/src/test/java/org/cragmoor/RefundsTest.java
--- a/src/test/java/org/cragmoor/RefundsTest.java
+++ b/src/test/java/org/cragmoor/RefundsTest.java
@@ -35,7 +35,7 @@ class RefundsTest {
   @Test
   void cancel23HoursAhead() {
     Booking booking = dropIn(FIRST_ZONE);
-    assertEquals(0, Refunds.refundCents(booking, hoursBefore(booking, 23)));
+    assertEquals(2400, Refunds.refundCents(booking, hoursBefore(booking, 23)));
   }
 
   @Test
@@ -47,6 +47,6 @@ class RefundsTest {
   @Test
   void deadlineShownAtSecondZoneGym() {
     Booking booking = dropIn(SECOND_ZONE);
-    assertEquals("Free cancellation until 2026-11-13 09:00", booking.cancellationNotice());
+    assertEquals("Free cancellation until 2026-11-13 21:00", booking.cancellationNotice());
   }
 }

Two files and six changed lines in each language, three out and three in. The window becomes 12, the 23-hour test now expects a full refund, and the second-zone deadline moves from 09:00 to 21:00, which is 12 hours before a 09:00 class. The example table's three rows at the new line join it as tests; Chapter 8 writes them before the code, and shows why that order matters. Chapter 6 shows the same step done by the agent, and what came back.

Misconceptions
  • "Using the agent for everything is the efficient default." On small edits the fixed costs, describing, waiting and reading, dominate, and typing is faster.
  • "Reading code teaches me as much as writing it." Reading a correct solution feels like understanding. Writing exposes the decisions you would have got wrong, which is where the learning is.
  • "If I write it myself I can skip review." Your own diff gets the same reading before commit and the same tests as the agent's.
  • "Critical code is too important to use AI near it at all." The agent can still help around it: tests that try to break it, a review of your version, a map of its callers. You write the core and own the reasoning.
  • "If I write it myself I'm wasting the tool." The tool earns its keep where checking is faster than writing. Telling those tasks apart is part of using it well.
Why It Matters
  • Type any change shorter than its own description. The agent's floor is describing plus reading.
  • Write concurrency, security and money logic you must reason about yourself, and use the agent to attack it with tests. Understanding comes from writing, assurance from trying to break it.
  • Write the first instances of anything you are learning before you delegate the rest. You then review from knowledge, not from fluency.
  • Take over after the second failed attempt at the same problem. A third attempt is usually the same guess.

Knowledge Check

Which of these changes is quicker to type than to ask an agent for?

  • Adding the one argument the type checker just named in its error message
  • A new report endpoint with validation, a query and its tests, about 300 lines
  • Characterization tests for an untested module of a few hundred lines of code
  • A mechanical rename across forty files that a script cannot safely do alone

Why does writing the waitlist's offer logic yourself build understanding that reading the agent's version does not?

  • The agent's version would be wrong, so reading it would only teach you the wrong lessons
  • Writing forces a decision at every interleaving; reading lets you agree with each line
  • Code written by hand runs faster, so you can see how the real timing of offers behaves
  • Reading code is slower than writing it, for logic where two things can happen at once

You write the core of the double-offer fix yourself. What is the best use of the agent around it?

  • None; the agent should stay out of code this sensitive, from start to finish
  • Ask it to rewrite your version in a cleaner style once your tests pass
  • Let it write the fix as well, then pick whichever of the two versions looks better
  • Have it write tests that try to break your version, and review what you wrote

You typed step three of the refund change yourself. What still has to happen before you commit it?

  • Nothing extra, because you understood every line as you typed it in
  • An agent review of the diff, since a person should not approve their own work
  • Read your own diff and run the tests, the same as for an agent's change
  • A second engineer types the same change, and the two versions are compared

You got correct