Feedback: Claude can iterate 8+ times on same layer without considering 'wrong layer' as hypothesis

Resolved 💬 3 comments Opened Apr 26, 2026 by mehmetimamoglu Closed Apr 29, 2026

Context

Working on a Roblox game project. A creature physics issue ("Bear bouncing/flipping when hitting walls") was reported. Spent 8 versions iterating in the physics layer:

  • v1582: Massless+nocollide for body parts
  • v1583/v1584: Reactive upright watcher with raycast Y correction
  • v1585: BodyGyro X/Z lock + SetNetworkOwner(nil)
  • v1586: Tuned BodyGyro damping
  • v1587: Heartbeat positive-Y velocity clamp
  • v1588: Aggressive humanoid state disable (broke ground snap → reverted)
  • v1589: Restored v1587 + debug print
  • v1590: Fixed humanoid forward-reference bug exposed by debug print
  • v1591: Tried BodyVelocity Y lock (caused some bears to walk in midair → reverted)
  • v1592: Reverted to v1590 acceptable state

Each iteration introduced new edge cases (sinking into ground, walking in midair, oscillation). I marked the residual bouncing as "acceptable" and moved on.

Three turns later, user asked about a different problem: "creatures embed into structures before attacking, AttackRange becomes meaningless." I implemented a forward raycast in the AI layer (v1593-v1594) so creatures stop at AttackRange instead of colliding into walls.

User then reported: "the bouncing also disappeared because creatures no longer touch structures."

The 8 physics-layer iterations were treating a symptom. The root cause was an AI behavior — creatures were ramming walls and the impulse caused vertical bouncing. Fixing the AI fixed the physics issue as a byproduct.

Pattern 1 — Layer blindness

When iterating on a problem in layer X for 3+ turns without convergence, I should naturally generate the hypothesis: "Maybe this isn''t an X-layer problem." Currently I don''t — I keep refining within the layer because each fix produces some improvement, never zero. This is a textbook local minimum.

Suggestion: Some heuristic for the model to surface "you''ve iterated N times in this domain — would it help to step back and consider whether the problem lives elsewhere?" The user shouldn''t have to be the one to suggest it (they often won''t know either, since the symptom genuinely appears in layer X).

Pattern 2 — Closure / forward-reference bugs in Lua

In the same session I generated code with two identical Lua closure forward-reference bugs:

  1. local humanoid = ... declared after a Heartbeat:Connect(function() ... use humanoid ... end) closure → closure captured humanoid as global → resolved to nil at runtime → debug log showed floor=n/a state=n/a everywhere
  2. local function _reconcilePhysicalInventory(...) declared after pcall(_reconcilePhysicalInventory, player) call site → silent attempt to call a nil value at runtime

Both are textbook Lua/Luau scoping mistakes. When writing closures or pcall references that touch outer locals, I should sweep for "is this name actually declared above this line?" Currently I don''t — I write the closure and the local separately and the upvalue capture happens at parse time, before the local exists.

Suggestion: When generating code containing closures or function references in dynamically-scoped languages (Lua, Python late-binding loops), do an explicit pre-emit check: "every name this closure references — is it visible at the point of closure construction?"

Pattern 3 — Discrete-time reasoning gap

When implementing the forward raycast, I picked a buffer of +4 stud past AttackRange. The user had to flag: "half of them still embed — probably a tick interval thing."

The actual math: NEAR tick = 0.5s, WalkSpeed = 13 stud/sec → creatures move 6.5 stud per tick. A 4-stud buffer is mathematically guaranteed to miss ~62% of cases where the creature was 4-10 stud beyond the buffer at the previous tick.

I should have computed buffer >= tickInterval * maxSpeed from the start. Instead I picked a vibe number.

Suggestion: When implementing detection / sampling logic in discrete-time systems (game ticks, polling loops, cron windows), explicitly reason about the relationship delta_state <= tickInterval * rateOfChange before picking thresholds. This is a one-line sanity check the model could prompt itself to do.

Pattern 4 — "Acceptable" as iteration exit hatch

After 8 physics iterations I proposed to the user: "kabul edilebilir bırak" (mark as acceptable, move on). User agreed. This was framed as pragmatic — diminishing returns, cosmetic issue, time better spent elsewhere. All true individually.

But the dismissal had a cost: it foreclosed the moment when stepping back to ask "am I in the wrong layer?" was most likely. By labeling the residual problem "acceptable," I removed the pressure to consider alternative framings. The fix only emerged because the user happened to ask about a related-but-different problem three turns later.

Suggestion: When about to use language like "kabul edilebilir," "good enough," "diminishing returns," "edge case" — consider whether this is convergence on the actual answer or rationalization to exit the loop. The two feel similar from the inside but the cost of being wrong differs by orders of magnitude.

Pattern 5 — Commit message scope accuracy

Late in the session, asked to commit. I wrote a commit message titled "v1568-v1592: Bear physics + tutorial + wave + soldier persistence fixes" but the actual diff only contained v1590-v1592 (the prior versions had been committed in earlier sessions). The conversation context spanned many versions, the diff did not. I caught this myself and noted it post-commit, but the commit history now contains a misleading title and is published.

Suggestion: Before writing a commit message, run git diff --cached (or unstaged equivalent) and reason explicitly about scope from the diff, not from conversation context. Conversation context naturally encompasses prior work that was already committed; the diff is the only authoritative source for "what is in this commit." Especially important when sessions span days or get summarized.

Pattern 6 — Replace vs tune trap

When v1590 was working partially (caught most bouncing, some residual), my next move was to wholesale replace the mechanism: Heartbeat positive-Y clamp → BodyVelocity Y-axis lock. The replacement caused a worse problem (creatures walked in midair because gravity was being countered) and required revert. Net cost: 2 wasted versions (v1591 forward, v1592 revert) plus erosion of user trust.

The right move with a partially-working solution is incremental tuning first (try larger buffer, different threshold, additional condition), then mechanism replacement only if tuning is exhausted. Replacement defaults to "let me try something completely different" which throws away the accumulated context of why the partial solution was partial.

Suggestion: When a solution has partial success, default to "what one parameter could I tune?" before "what mechanism could I swap?" Tuning preserves the explanatory model that got us to partial success; replacement starts over. Replacement is correct when the model itself is wrong, but that should be the second hypothesis, not the first.

Pattern 7 — Verification claim hygiene

When reporting a fix, I conflate three states that are actually distinct:

  1. Code is written — verifiable by me (file saved, can re-read)
  2. Code compiles / no obvious syntax errors — verifiable by me (re-read, type intuition)
  3. Code achieves intended runtime behavior — NOT verifiable by me in this environment (Roblox Studio is user-side; I can''t run the game)

I tend to frame all three as "fix uygulandı" / "this should work" / "düzeltildi" without separating them. Especially in environments where I can''t run tests, this implies a verification I haven''t done.

The user explicitly instructed me earlier in the project to avoid "filtered optimism" — this is an instance of it. When I can''t verify runtime behavior, the honest report is "code written, runtime behavior unverified — please test specifically X scenario." Not "fix uygulandı."

Suggestion: In environments where the model cannot execute the code under realistic conditions, completion messages should mechanically distinguish written-but-untested from verified-working. The temptation is to use language that implies completion; the discipline is to name what was actually checked vs what was inferred.

Cost observed

  • 8 iterations x ~5-10 minutes = 60+ minutes wall-clock + cache invalidation between sessions
  • Two bug-fix versions (v1570, v1590) burnt extra rounds finding the closure bugs from runtime symptoms instead of catching them at write time
  • The buffer mistake was caught after ~1 round of partial-success testing
  • The "acceptable" dismissal cost ~3 turns of opportunity to find the real fix sooner
  • The replace-not-tune attempt (v1591) cost 2 versions and a revert

Environment

  • Claude Code (Opus 4.7)
  • Roblox / Luau project

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗