Topic 05

Rejecting, Narrowing, Redirecting

Review

After reading come three moves, and "accept" is not one of them until the diff has earned it. Reject throws the whole diff away. Narrow keeps the parts that are right and drops the rest. Redirect sends the diff back with the one thing that was wrong and the evidence for it.

Choosing well between them is most of what makes the loop faster than writing the code yourself. Choosing badly, usually by sending a vague complaint back into a long session, is how an hour turns into an afternoon.

Reject When the Approach Is Wrong

If the diff solves a different problem from the one you asked about, or takes a path you would not accept even polished, fixing it costs more than starting again. Discard back to the commit you made before asking (Chapter 9), and fix whatever produced the wrong approach: the task (Chapter 5), the plan or the context (Chapter 4). A rejected diff is cheap only while it is unmerged, which is the reason to decide now rather than after a few rounds of patching.

Suppose the agent had met the refund task differently: leave the 24 in the code, add a per-gym override, and set it to 12 for all four gyms in the settings. Members would see the right deadline tomorrow, and the code would describe a rule that no longer exists, with the real rule hidden in configuration. No amount of polishing turns that into the change Ines asked for, because it answers a different question: how to make the behaviour look right, not how to change the rule. That diff goes back to the base commit, and the task gains a line saying where the rule lives.

Narrow When Parts Are Right

The refund diff is not a wrong approach. Its window change and its 23-hour test update are exactly what step three asked for, and everything else is not. So you keep those two hunks and drop the rest, and what remains is the part of the diff the plan described. Git does this precisely: stage the hunks you have read and accept, decline the others, then discard whatever was not staged.

Narrowing: stage two hunks, decline the rest
$ git add -p src/cragmoor/refunds.py tests/test_refunds.py
-from cragmoor.gyms import Gym
+from cragmoor.policy import Policy
-FULL_REFUND_WINDOW = timedelta(hours=24)
+FULL_REFUND_WINDOW = timedelta(hours=12)
   (… 16 more changed lines)
(1/1) Stage this hunk [y,n,q,a,d,s,e,p,?]? s
Split into 6 hunks.
-from cragmoor.gyms import Gym
+from cragmoor.policy import Policy
(1/6) Stage this hunk [y,n,q,a,d,j,J,g,/,e,p,?]? n
-FULL_REFUND_WINDOW = timedelta(hours=24)
+FULL_REFUND_WINDOW = timedelta(hours=12)
(2/6) Stage this hunk [y,n,q,a,d,K,j,J,g,/,e,p,?]? y
-def refund_deadline(starts_at: datetime, gym: Gym) -> datetime:
+def refund_deadline(starts_at: datetime, policy: Policy) -> datetime:
(3/6) Stage this hunk [y,n,q,a,d,K,j,J,g,/,e,p,?]? n
-    # Class times are stored on the gym's wall clock, with no zone attached.
-    start = schedule.localize(starts_at, gym.time_zone)
-    return start - FULL_REFUND_WINDOW
+    return starts_at.replace(tzinfo=schedule.SERVER_ZONE) - policy.window
(4/6) Stage this hunk [y,n,q,a,d,K,j,J,g,/,e,p,?]? n
-    return booking.paid_cents // booking.classes
+    return round(booking.paid_cents / booking.classes)
(5/6) Stage this hunk [y,n,q,a,d,K,j,J,g,/,e,p,?]? n
-    deadline = refund_deadline(booking.starts_at, booking.gym)
-    if cancelled_at <= deadline:
-        return class_share_cents(booking)
+    policy = Policy.for_gym(booking.gym, FULL_REFUND_WINDOW)
   (… 4 more changed lines)
(6/6) Stage this hunk [y,n,q,a,d,K,g,/,e,p,?]? n
+import pytest
+
+from cragmoor.policy import Policy, PolicyError
(1/3) Stage this hunk [y,n,q,a,d,j,J,g,/,s,e,p,?]? n
-    assert refunds.refund_cents(booking, hours_before(booking, 23)) == 0
+    assert refunds.refund_cents(booking, hours_before(booking, 23)) == 2400
(2/3) Stage this hunk [y,n,q,a,d,K,j,J,g,/,e,p,?]? y
-    assert cancellation_notice(booking) == "Free cancellation until 2026-11-13 09:00"
+    assert cancellation_notice(booking) == "Free cancellation until 2026-11-13 20:00"
+
+
   (… 40 more changed lines)
(3/3) Stage this hunk [y,n,q,a,d,K,g,/,e,p,?]? n
Narrowing: stage two hunks, decline the rest
$ git add -p src/refunds.ts tests/refunds.test.ts
-import type { Gym } from "./gyms.ts";
+import { Policy } from "./policy.ts";
-export const FULL_REFUND_WINDOW_MS = 24 * HOUR_MS;
+export const FULL_REFUND_WINDOW_MS = 12 * HOUR_MS;
   (… 17 more changed lines)
(1/1) Stage this hunk [y,n,q,a,d,s,e,p,?]? s
Split into 5 hunks.
-import type { Gym } from "./gyms.ts";
+import { Policy } from "./policy.ts";
(1/5) Stage this hunk [y,n,q,a,d,j,J,g,/,e,p,?]? n
-export const FULL_REFUND_WINDOW_MS = 24 * HOUR_MS;
+export const FULL_REFUND_WINDOW_MS = 12 * HOUR_MS;
(2/5) Stage this hunk [y,n,q,a,d,K,j,J,g,/,e,p,?]? y
-export function refundDeadline(startsAt: string, gym: Gym): Date {
-  // Class times are stored on the gym's wall clock, with no zone attached.
-  const start = schedule.localize(startsAt, gym.timeZone);
-  return new Date(start.getTime() - FULL_REFUND_WINDOW_MS);
+export function refundDeadline(startsAt: string, policy: Policy): Date {
+  return new Date(schedule.localize(startsAt, schedule.SERVER_ZONE).getTime() - policy.windowMs);
(3/5) Stage this hunk [y,n,q,a,d,K,j,J,g,/,e,p,?]? n
-  return Math.floor(booking.paidCents / booking.classes);
+  return Math.round(booking.paidCents / booking.classes);
(4/5) Stage this hunk [y,n,q,a,d,K,j,J,g,/,e,p,?]? n
-  const deadline = refundDeadline(booking.startsAt, booking.gym);
-  if (cancelledAt.getTime() <= deadline.getTime()) {
-    return classShareCents(booking);
+  const policy = Policy.forGym(booking.gym, FULL_REFUND_WINDOW_MS);
   (… 5 more changed lines)
(5/5) Stage this hunk [y,n,q,a,d,K,g,/,e,p,?]? n
+import { Policy, PolicyError } from "../src/policy.ts";
(1/3) Stage this hunk [y,n,q,a,d,j,J,g,/,e,p,?]? n
-  expect(refunds.refundCents(booking, hoursBefore(booking, 23))).toBe(0);
+  expect(refunds.refundCents(booking, hoursBefore(booking, 23))).toBe(2400);
(2/3) Stage this hunk [y,n,q,a,d,K,j,J,g,/,e,p,?]? y
-  expect(cancellationNotice(booking)).toBe("Free cancellation until 2026-11-13 09:00");
+  expect(cancellationNotice(booking)).toBe("Free cancellation until 2026-11-13 20:00");
+});
+
   (… 40 more changed lines)
(3/3) Stage this hunk [y,n,q,a,d,K,g,/,e,p,?]? n
Narrowing: stage two hunks, decline the rest
$ git add -p src/main/java/org/cragmoor/Refunds.java src/test/java/org/cragmoor/RefundsTest.java
-  public static final Duration FULL_REFUND_WINDOW = Duration.ofHours(24);
+  public static final Duration FULL_REFUND_WINDOW = Duration.ofHours(12);
-  public static ZonedDateTime refundDeadline(LocalDateTime startsAt, Gym gym) {
-    // Class times are stored on the gym's wall clock, with no zone attached.
   (… 15 more changed lines)
(1/1) Stage this hunk [y,n,q,a,d,s,e,p,?]? s
Split into 4 hunks.
-  public static final Duration FULL_REFUND_WINDOW = Duration.ofHours(24);
+  public static final Duration FULL_REFUND_WINDOW = Duration.ofHours(12);
(1/4) Stage this hunk [y,n,q,a,d,j,J,g,/,e,p,?]? y
-  public static ZonedDateTime refundDeadline(LocalDateTime startsAt, Gym gym) {
-    // Class times are stored on the gym's wall clock, with no zone attached.
-    ZonedDateTime start = Schedule.localize(startsAt, gym.timeZone());
-    return start.minus(FULL_REFUND_WINDOW);
+  public static ZonedDateTime refundDeadline(LocalDateTime startsAt, Policy policy) {
+    return startsAt.atZone(Schedule.SERVER_ZONE).minus(policy.window());
(2/4) Stage this hunk [y,n,q,a,d,K,j,J,g,/,e,p,?]? n
-    return booking.paidCents() / booking.classes();
+    return Math.round((double) booking.paidCents() / booking.classes());
(3/4) Stage this hunk [y,n,q,a,d,K,j,J,g,/,e,p,?]? n
-    ZonedDateTime deadline = refundDeadline(booking.startsAt(), booking.gym());
-    if (!cancelledAt.isAfter(deadline)) {
-      return classShareCents(booking);
+    Policy policy = Policy.forGym(booking.gym(), FULL_REFUND_WINDOW);
   (… 5 more changed lines)
(4/4) Stage this hunk [y,n,q,a,d,K,g,/,e,p,?]? n
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
(1/3) Stage this hunk [y,n,q,a,d,j,J,g,/,e,p,?]? n
-    assertEquals(0, Refunds.refundCents(booking, hoursBefore(booking, 23)));
+    assertEquals(2400, Refunds.refundCents(booking, hoursBefore(booking, 23)));
(2/3) Stage this hunk [y,n,q,a,d,K,j,J,g,/,e,p,?]? y
-    assertEquals("Free cancellation until 2026-11-13 09:00", booking.cancellationNotice());
+    assertEquals("Free cancellation until 2026-11-13 20:00", booking.cancellationNotice());
+  }
+
   (… 40 more changed lines)
(3/3) Stage this hunk [y,n,q,a,d,K,g,/,e,p,?]? n
Discarding everything that was not staged
$ git restore .
$ git clean -n
Would remove src/cragmoor/policy.py
$ git clean -f
Removing src/cragmoor/policy.py
$ git status --short
M  src/cragmoor/refunds.py
M  tests/test_refunds.py
Discarding everything that was not staged
$ git restore .
$ git clean -n
Would remove src/policy.ts
$ git clean -f
Removing src/policy.ts
$ git status --short
M  src/refunds.ts
M  tests/refunds.test.ts
Discarding everything that was not staged
$ git restore .
$ git clean -n
Would remove src/main/java/org/cragmoor/Policy.java
$ git clean -f
Removing src/main/java/org/cragmoor/Policy.java
$ git status --short
M  src/main/java/org/cragmoor/Refunds.java
M  src/test/java/org/cragmoor/RefundsTest.java

Two hunks staged, everything else gone, including the new policy file. Run the refunds tests now and one fails: the second-zone test still expects the old 24-hour deadline, 09:00, and the code correctly gives 21:00. That failure is right. The rule changed, the test that pins the screen's deadline has to change with it, and the value it needs is 21:00. You make that one-line edit yourself, the move this page later calls taking a piece over, and the suite is green. The result is the step-three diff from the last topic of Chapter 5, line for line.

Narrowing has a limit. It works when the right lines sit in hunks of their own, as the window and the 23-hour test did. When a right line and a wrong line share a hunk that git cannot split, the choices are to edit the hunk by hand inside the same command, which git allows, or to stage nothing from it and type the right line yourself afterwards. Either is fine. Accepting the hunk whole because most of it is right is not, because the wrong line is then part of what you reviewed and approved.

Redirect With One Thing and Its Evidence

Sometimes you want the agent to do the fixing. "Try again" or "that's wrong" gets a fresh guess with fresh problems, because it gives the agent nothing new to work from. A redirect names one problem, the constraint the fix must respect, and the evidence the agent cannot talk its way around.

Two redirects for the same problem
The vague redirect"The time zones are broken. Fix it."
What came back: a fixed one-hour correction for gyms outside the server's zone, and the second-zone test edited again, to 21:00, to match. Green again. Right for today's two zones by coincidence, wrong the day a gym opens in a third, and a second copy of what the helper in schedule already does. "Fixed the time-zone handling."
The precise redirectone problem, one constraint, the evidence
"The deadline must be computed on the gym's own clock, through the helper in schedule, as before your change. The second-zone test's expected value is 21:00, the rule's value and the one you first wrote. Set it back and make the code pass without editing any other test."
What came back: the conversion restored through the helper, the gym passed to the deadline function again, the test at 21:00. Three files, about a dozen changed lines, the suite green against the rule's value.

"The one you first wrote" is not a guess. The transcript shows the agent set that test to 21:00 when it changed the window, then changed it to 20:00 only after its rewrite of the deadline made the test fail. The evidence was in the session all along; the precise redirect puts it in front of the agent as a constraint.

The precise redirect has a shape you can reuse: one problem, stated as the rule it breaks; the constraint, stated as what must stay true; and the evidence, stated as something the agent can run or read. "The deadline must be on the gym's clock" is the rule. "Without editing any other test" is the constraint. "The second-zone test, at 21:00" is the evidence. Each part closes one door the agent could otherwise walk through, and the last part is the one that stops it bending the test again.

One Correction at a Time

Five corrections in one message come back as a diff that addresses three of them. The refund diff has five problems, and the precise redirect above fixes one, as asked. Send the next one when that one is confirmed. And watch the count: a long thread of corrections fills the context with failed attempts, which Chapter 4 showed is how sessions drift. After two redirects on the same point, stop. Restart from the base commit with a better task that carries what you learned (Chapter 9); Chapter 10 has the same rule for debugging.

Taking a Piece Over

When the remaining fix is two lines you can see, write them yourself, as the narrowing above did with the second-zone test (Chapter 5 has the judgement). Then commit them, or tell the agent what you changed, so that its next edit starts from your version of the file and not from its own earlier view of it. An unannounced edit in the middle of a session can be quietly overwritten by the agent's next change to the same file.

The reverse also happens. You narrow a diff with git, the agent's next message refers to the policy object as if it still existed, and its next edit puts it back. When you narrow mid-session, say so in your next message: which hunks you kept, which you discarded, and that the discarded ones are not to return. Better still, commit what you kept and start the next correction in a fresh session from that commit.

Turning a Finding Into a Rule

A correction you make twice belongs in the project instructions file (Chapter 3). After the refund diff, Cragmoor's file gains one line: "Never change an existing test's expected value without naming the test and saying why the rule changed." One review finding becomes a standing rule, read at the start of every session, and Chapter 4's habits keep it from going stale.

Misconceptions
  • "Throwing away an hour of agent work wastes the hour." The hour is spent either way. The only question is which path from here costs less, and a restart with a sharper task usually does (Chapter 9).
  • "A general complaint lets it find a better solution." A vague redirect produces a new guess with new problems, such as a hard-coded one-hour correction. One constraint plus evidence produces the fix.
  • "I'll accept it and fix the rest in a follow-up." The rest is then merged, unrecorded and nobody's task, and the follow-up competes with every other task in the queue.
  • "Editing the agent's files by hand is the fastest way to narrow." An unannounced edit mid-session can be overwritten by its next change. Stage, discard, then tell it what changed.
  • "Each rejection shows the tool is not good enough for the task." Rejection is the loop working as designed, and it costs minutes because nothing was merged.
Why It Matters
  • Decide between reject, narrow and redirect before typing any reply. Each needs a different message, or none.
  • Redirect with exactly one problem, the constraint and the evidence. The evidence is the part the agent cannot talk its way around.
  • Narrow by staging hunks, not by asking the agent to undo its extras. Staging is exact, and an undo request is another generation.
  • Move any correction you have made twice into the project instructions file. One line there saves the same finding in every later review.

Knowledge Check

A diff gets the window change and one test right, but also adds a configuration system and changes the rounding. Which move fits?

  • Reject it, since a diff with this many problems has the wrong approach
  • Narrow it: stage the right hunks and discard everything else
  • Redirect it with a message that lists all the problems at once
  • Accept it and open follow-up tasks for the rounding and the extras

You reply "the time zones are broken, fix it". What is the agent most likely to produce?

  • The correct fix, since it now knows exactly where in the code the problem lies
  • A question asking you which time zone each of the four gyms is really in
  • A new plausible change with new problems, likely with the test edited again
  • No change at all, because a vague message gives it nothing to act on

You have redirected the agent twice on the deadline calculation and it is still wrong. What now?

  • A third redirect with the same evidence, written more firmly and in more detail
  • Accept the latest version, and fix the deadline yourself after it has merged
  • Ask the agent to explain why its last two attempts failed before trying again
  • Stop, discard to the base commit, and restart with a task that carries the lesson

Why is narrowing by staging hunks better than asking the agent to remove its extra changes?

  • Staging keeps exactly the lines you read; an undo request is another generation
  • The agent cannot remove code it wrote in the same session, only add more to it
  • Staging is faster, but the two produce the same result in every case that matters
  • Asking the agent counts against a limit on how many edits it may make in a session

You got correct