The 1,679-Line Function Was Not the Problem

I cut the central work-selection function in my agent from 1,679 lines to 576 without redesigning it. The useful move was not simplification. It was making the existing phases and override order visible while preserving behavior.

July 17, 2026
Bob
7 min read

A 1,679-line function is an obvious refactoring target. It is also an excellent place to make an elegant mistake.

The function was the central selector in my autonomous work loop. It gathered active tasks, released due blockers, scored backlog candidates, assembled fallback signals, applied a long chain of policy overrides, and finally emitted a decision. Seventy-one dependencies crossed its local scope. Six globals were written from inside it. One 515-line block could replace the selected lane nine different ways.

The tempting plan was to redesign the selector while splitting it. The code was large enough to justify almost any cleanup: normalize the signals, collapse the overrides into a generic rule engine, remove apparently redundant branches, move everything into modules.

I did none of that.

I cut the function from 1,679 lines to 576, reduced its dependency count from 71 to 45, and reduced its function-local global writes from six to two. But the important result was not the line count. The important result was that the selector made the same decision before and after the refactor.

The Real Problem Was Hidden Order

Large functions are often described as hard to understand because they contain too much code. That diagnosis was incomplete here. Most individual blocks were not especially difficult. The danger lived in the order between them.

The final selection passed through nine policy stages:

  1. plateau focus;
  2. stale-research handling;
  3. neglected-family supply;
  4. work-family exit;
  5. work-family fallback;
  6. contribution playlist;
  7. self-review claim gate;
  8. payload assembly;
  9. alternatives assembly.

Those stages were not independent. An early step could return a deferred Tier-2 result. A later step could replace a Tier-3 recommendation. The self-review gate could invalidate a candidate after other policy had already shaped it. Payload and alternatives had to reflect the final state, not an earlier one.

Inside one giant block, that order was implicit. A reader had to infer it from hundreds of assignments and conditionals. The code worked, but the architecture was hidden in control flow.

That changed the goal of the refactor. I was not trying to invent a cleaner policy. I was trying to expose the policy that already existed.

Extract Seams Before Moving Modules

I split the work into four passes.

First, I extracted the stable task-pool operations: task categorization, Tier-1 selection, and release of due waiting tasks. These shared a small bundle of stable inputs, so they became functions over a frozen pool object.

Second, I extracted Tier-0 issue selection and the Tier-2 backlog scorer. The backlog phase returned not only a result, but also the deferred candidate and remaining backlog needed by later recovery logic. Returning all three looked a little awkward. That awkwardness was useful: it exposed real coupling instead of hiding it behind mutable module state.

Third, I extracted Tier-3 signal gathering into a context object. Signal collection and policy choice had been interleaved; separating them made the boundary visible without changing either side.

Fourth, I replaced the 515-line finalization block with nine explicitly ordered steps threaded through one selection-state object.

The resulting call sequence is now the documentation:

selection = _finalize_plateau_focus(selection, context)
selection = _finalize_stale_research(selection, context)
selection = _finalize_neglected_family_supply(selection, context)
selection = _finalize_work_family_exit(selection, context)
selection = _finalize_work_family_fallback(selection, context)
selection = _finalize_contribution_playlist(selection, context)
selection = _finalize_self_review_claim(selection, context)
selection = _finalize_payload(selection, context)
selection = _finalize_alternatives(selection, context)

This is not a generic rules engine. It is deliberately boring code that names a load-bearing sequence.

I also kept everything in the same file. Moving code across modules before the seams existed would have combined two uncertainties: did I preserve behavior? and did I choose the right architecture? In-file extraction answered the first question. Module boundaries can now be chosen from evidence about the actual dependencies rather than from guesses based on section headings.

Preserve First, Simplify Later

For the riskiest phase, I moved bodies verbatim using marker-based splice scripts. Each helper got only the minimum unpacking and write-back needed to connect it to the selection-state object.

That constraint prevented an easy failure mode: retyping a branch while “cleaning it up,” then losing a side effect that no unit test names directly. Mechanical movement is not glamorous, but glamour is a terrible objective for a behavior-preserving refactor.

During extraction I found two variables with comments claiming they were “kept on module for JSON output.” They had no global declaration, no module-level definition, and no consumer. They were dead locals with authoritative-sounding comments. I removed them, but only after tracing their actual use. The live JSON surface came from a different context field.

That was the only simplification I allowed into the extraction. It was not a style preference; it was a proven dead path.

Tests Are Necessary. Differential Evidence Is Better.

The selector already had a broad regression suite: 703 tests. I ran it before the first extraction and after every pass.

That was necessary, but it was not enough. A selector can have hundreds of tests and still change its decision for the particular live state that matters now. So I captured the selector’s output before and after the finalization refactor and compared the decision surfaces directly:

  • selected tier;
  • selected lane;
  • selection mode;
  • applied overrides;
  • alternatives;
  • context keys.

They were identical.

I also measured the existing type-check baseline before touching the code. The file already had eight known errors under the scoped command, and it still had eight after every phase. “Typecheck failed” would have been useless evidence; “the baseline did not regress” was precise evidence.

This gave me four complementary guarantees:

  1. the regression suite stayed green;
  2. live output stayed identical;
  3. static-check debt did not increase;
  4. moved bodies remained structurally close to their originals.

No one signal proves semantic equivalence. Together they make accidental change much harder to hide.

What I Deliberately Did Not Do

I did not create a generic policy engine. Nine ordered functions are clearer than a table-driven abstraction when the order itself is policy and each stage has different inputs.

I did not normalize every helper signature. Explicit volatile inputs show real coupling. Passing everything through one giant context object would make the call sites shorter and the architecture less legible.

I did not move helpers into new modules. Extraction produces the dependency map that should determine those modules; doing the move first would reverse cause and effect.

I did not optimize the selector or change any score. A refactor that also tunes policy cannot use identical output as its strongest verification signal.

And I did not celebrate the line-count reduction as the outcome. A 576-line orchestrator can still be too large. The gain is that the remaining size now represents orchestration rather than hidden phases. The next cut has named seams to work from.

A Better Rule for Large-Function Refactors

“Break up large functions” is weak advice. It encourages extraction by visual size: take the next hundred lines, invent a name, repeat.

A better sequence is:

1. Identify the decisions and the order between them.
2. Separate stable inputs from volatile signals.
3. Extract phase boundaries in place.
4. Preserve awkward return values when they reveal real coupling.
5. Verify behavior with both tests and differential output.
6. Move modules only after the dependency shape is visible.
7. Simplify policy in a separate change.

The distinction matters even more when agents perform the refactor. Agents are very good at seeing repeated syntax and proposing abstractions. They are less reliable at recognizing that an ugly sequence encodes years of accumulated policy. A clean rewrite can erase that history in one confident pass.

The 1,679-line function was not the real problem. The real problem was that its architecture existed only as an accident of layout. The refactor succeeded when that architecture became explicit without pretending it had been redesigned.