Skip to content
Goatfied

deployment

Agentic planning for cross-package refactors

Learn how to design AI agents that handle cross-package refactors by sequencing edits correctly while staying within token budgets and maintaining compilability.

2026-07-1012 min readBy Goatfied
Agentic planning for cross-package refactors

Cross-package refactors expose the central tension in AI coding systems: agents excel at local transformations but collapse under dependency-aware orchestration. When you rename a function used across twelve packages, the mechanical edits are trivial—update imports, adjust call sites, regenerate mocks. The hard part is sequencing those edits so each intermediate state compiles, determining which packages can change in parallel versus which must wait for upstream changes, and surfacing integration breaks before they reach production. This guide walks through the design space for planning large refactors under compile-first constraints that actually catch breaks before they ship.

The token budget wall in workspace-wide planning

An agent that loads every file across ten packages into context to build a refactor plan burns 50K+ tokens before writing a single line of code. Even if your LLM supports that window, plan quality degrades: the model confuses which interface belongs to which package, proposes conflicting changes, or generates a 47-step plan where steps 12–31 are all variations of "update imports."

Worse, when any single step fails—a type error in package C—the agent must choose: re-plan the entire workspace (another 50K tokens) or patch locally and hope the rest of the plan holds. Teams see expensive replan cycles or PRs that break tests in packages the agent never revisited.

The boundary is real: cross-package refactors hit the point where upfront global planning costs more in tokens and fragility than incremental planning costs in backtracking. The fix is to move from monolithic plans to dependency-graph-aware chunking.

Dependency graph traversal as a planning primitive

Instead of loading all files upfront, parse package manifests (`package.json`, `go.mod`, `Cargo.toml`) to identify which packages import the target module. Load only the import statements and exported signatures from each dependent package. This keeps planning token cost proportional to the number of package boundaries crossed, not total lines of code.

Generate a two-phase plan: refactor the source package first (updating exports, tests, docs), then iterate through dependents in topological order—leaf packages before their consumers. For example, renaming `logger.info()` to `logger.log()`:

1. Update `@core/logger` exports and unit tests

2. Update packages that only consume the logger: `@api/handlers`, `@worker/jobs`

3. Update packages that re-export logger utilities: `@internal-sdk`

This surfaces a key tradeoff: topological ordering gives predictable progress but delays feedback if a downstream package has an unexpected usage pattern. If package F uses your utility in a macro that breaks the new signature, you won't discover that until phase two.

Goatfied's validate-then-retry loop inverts this: refactor the source package, run global tests, see failures in package F, and *then* load F's implementation. This finds edge cases earlier but risks ping-ponging between packages. The cost is more compile/test cycles; the payoff is never accumulating a dozen broken packages before you notice.

Constraint checkpoints as backpressure

Cross-package refactors have a deceptive failure mode: the agent completes phase one, all tests pass, commits, moves to phase two, breaks package D, and now needs to revisit phase one because the real signature should have been different. Without checkpoints, you're doing archaeology to figure out which of the 19 files needs to change.

Explicit checkpoints—compile, lint, and test runs after each package-level edit—create backpressure that surfaces mismatches early:


# Example constraint gate in Goatfied config

constraints:

  - stage: per_package

    run: |

      npm run build

      npm run lint

      npm test -- --coverage=false

    on_fail: expand_context_and_retry

When package B fails to compile against the new package A interface, the agent gets immediate signal. It can load more of B's implementation, adjust A's signature, or mark a dependency as "needs manual review" if the constraint can't be automatically satisfied. Each package's success becomes a verified invariant before the agent moves on, not a hopeful assumption.

This is slower in the happy path where the initial plan was perfect. It's radically faster in the common path where the plan was 80% correct and needs local corrections.

The edit-validate gap and false-green builds

Single-package refactors have natural boundaries: propose changes, run tests, see failures, retry. Cross-package work breaks this. Consider `packages/core`, `packages/api`, `packages/worker`. An agent plans to extract shared types from `api` into `core`, then update both consumers. If it validates only after all edits, it discovers circular dependencies too late. If it validates after each package, `api` breaks between steps because `core` changes aren't atomic with consumer updates.

A subtler trap: agents generate plans in topological order but execute them in parallel queue order. Thread A starts updating `api/handlers.ts`, thread B starts updating `core/types.ts`. Thread A's validation passes because old types are still present. Thread B commits type changes. Thread A commits using stale types. CI was green at each commit, but the combined result is broken.

The fix: model dependencies explicitly in the constraint system. In Goatfied's architecture:


constraints:

  - name: type-migrations

    depends_on: [core/**/*.ts]

    blocks: [api/**/*.ts, worker/**/*.ts]

The agent can parallelize freely until it touches `core`, at which point consuming packages wait. The tradeoff: slightly slower execution for correctness guarantees that matter in production.

Partial vs. atomic merge strategies

Should the agent open one PR with all packages updated, or a chain of PRs where each package is individually mergeable?

**Atomic PRs** (one PR, all changes) keep the codebase consistent at every commit and avoid in-between states where half the packages use the old API. They're easier to reason about in review. The cost: if package G has a subtle issue, the entire PR is blocked. You can't merge the successful refactor of packages A–F while you fix G.

**Partial PRs** (one PR per package, with dependency ordering) let you merge validated changes incrementally. Package A gets merged, package B depends on merged A and gets merged next. If package G breaks, the blast radius is contained. The cost: you must maintain backward compatibility at each step or explicitly mark some packages as "transitional" and accept that main is broken until the chain completes. Most teams avoid the broken-main path, which means partial strategies force you to design refactors that can land in stages.

Goatfied's small-diff bias pushes toward partial PRs. The agent naturally produces a series of focused changes, each with its own validate gate. If your refactor truly can't be staged (a breaking protocol change across tightly coupled packages), acknowledge that upfront and design a feature flag or compatibility shim, not a perfect 3,000-line atomic PR.

Handling breaking changes with phased migrations

The hardest refactors involve breaking changes—removing a deprecated API, tightening a type, changing a return value. If downstream packages haven't updated, they break. The standard answer is two-phase migration: introduce the new API alongside the old, update consumers, then remove the old.

Agentic planning makes this tractable:

1. **Phase 1 (additive)**: Agent adds the new API to the core package, marks the old one deprecated, ships it. No downstream breakage yet.

2. **Phase 2 (migration)**: Agent updates each consuming package to use the new API. Because the old API still exists, each update compiles independently. These can happen in parallel.

3. **Phase 3 (removal)**: Once all consumers migrate, agent removes the deprecated API. The validation gate confirms zero usage before proceeding.

This requires the agent to understand "safe intermediate states." Goatfied's plan-then-validate loop checks each intermediate commit against the full test suite, not just the final state. If phase 2 breaks a consumer, the retry logic adjusts the migration PR before you see a red CI badge.

Integration test boundaries and contract validation

Agents often validate changes by running each package's test suite in isolation. This catches unit test failures but misses integration contract breaks. A real pattern: an agent renamed a REST endpoint path in `api` and updated all call sites in `worker`. Both test suites passed. The staging deploy broke because a third service—external to the monorepo—hit the old path.

The failure: isolated validation scope. The agent's mental model stops at the monorepo boundary. For cross-package refactors, you need:

  • Contract tests that verify interface stability (OpenAPI specs, proto definitions, exported type signatures)
  • Explicit markers for public API surfaces vs. internal implementation
  • A staging environment where integration tests run against the composed system

Goatfied's validation gates let you inject custom scripts: `validate: integration-tests.sh` that spins up all services and runs end-to-end flows. The agent doesn't need to understand your entire architecture; it just needs to know "this validation must pass before merge" and interpret failures.

Your CI must run the full test suite for every downstream package that imports the changed code—not just the package you edited, but everything that transitively depends on it:


# Example GitHub Actions step

- name: Run affected tests

  run: |

    npx nx affected:test --base=main --head=HEAD

Wire this into the validate step. After the agent proposes an edit, the validation pass should fail immediately if downstream tests break. Don't wait for CI. Goatfied runs lint, type check, and affected tests before marking an edit complete. The agent sees failures and retries with a narrower change, catching "I updated the interface but forgot a call site in package B" before you push.

Writing constraint manifests upfront

Before any code changes, document what must remain true throughout the refactor. These are compile-time and runtime gates that will block merges if violated:


constraints:

  - type: no_breaking_changes_without_deprecation

    packages: ["@company/api-client", "@company/internal-sdk"]

  - type: test_coverage_threshold

    minimum: 85

    scope: changed_files

  - type: dependency_graph_acyclic

    root: "packages/"

When Goatfied's agent loop plans edits, these constraints become hard requirements. The planner knows it can't remove a method without first adding a deprecated wrapper. It won't propose changes that drop test coverage. You're trading flexibility for the ability to actually land the refactor.

Run a constraint collection pass before prompting any agent. Walk your repository structure and output a dependency manifest that answers:

  • If I change a symbol in package A, which packages import it?
  • Does this symbol appear in serialization boundaries (JSON schemas, API specs)?
  • Are there generated artifacts that derive from this package's types?

You don't need perfect precision. A conservative overapproximation—flagging any file that references the symbol string—is sufficient. The goal is to prevent the agent from missing an entire subsystem.

Bounding diff size for reviewability

Even with correct ordering, a cross-package refactor can produce diffs too large to review. Hard limit: 150–300 lines of functional changes per pull request. More than that and review quality collapses.

For agentic workflows, partition the plan into chunks that each touch ≤3 packages and stay under the line limit. Goatfied's approach: the agent proposes a plan with explicit breakpoints. You see "Phase 1: add new API methods to `core-utils` (estimated 120 LOC). Phase 2: migrate `auth-service` callers (estimated 280 LOC)." Each phase maps to one PR.

Small diffs get merged in hours, not days. Mistakes are cheap to roll back. Set `max_diff_lines: 150` in your config; the agent automatically splits planned work into multiple branches, each with its own compile/lint/test cycle.

Handling cross-repo boundaries

The hardest case is when your refactor spans repositories: internal package consumers, mobile apps that vendor your SDK, infrastructure repos that import your service's protobuf definitions.

**Option 1: Compatibility shim**. Keep the old API surface as a thin wrapper during transition. Add both `validateToken()` and `verify()`, mark the former deprecated, ship the change, then remove it once consumers migrate.

**Option 2: Coordinated multi-PR release**. Update all repos simultaneously. The agent produces multiple PRs—one per repo—then orchestrates merge order. Structure the plan as a DAG: nodes are PRs, edges are "must merge before" dependencies. If repo B depends on repo A's new export, B's PR stays in draft until A merges and publishes.

For self-hosted Goatfied deployments, configure the planner with external dependency metadata—simple JSON mapping package names to Slack channels for approval workflows. The agent knows to file a tracking issue and pause rather than assuming it can migrate everything.

When to fall back to human-guided decomposition

Agentic planning breaks down when the refactor requires architectural judgment that isn't expressible as a local constraint. Splitting a monolithic package into three new packages, each with different dependency rules, needs a human to draw the boundaries. The agent can execute the split once you've defined the module map, but it won't invent a coherent decomposition from first principles.

Similarly, refactors that change concurrency models, swap out entire libraries, or involve performance tradeoffs need human decomposition. Signals to escalate:

  • The dependency graph shows cycles (mutual imports between packages that both need updates)
  • Generated code is hand-edited downstream (your API client was templated but then customized)
  • No automated tests cover the integration boundary
  • The refactor requires semantic changes, not just renames (splitting one function into two with different error handling)

The practical pattern: human defines the phase boundaries and invariants, agent executes each phase under compile/lint/test gates. Don't ask the agent to be a software architect. Ask it to be a meticulous, fast implementer that never forgets to update an import.

In these cases, have the agent produce a draft PR with detailed comments explaining what it couldn't safely complete. A human takes over with 80% of the mechanical work done and a clear map of remaining risk areas.

Operational discipline: rollback and staging

Every PR in the refactor sequence needs a one-command rollback plan documented in the PR description before merge:


## Rollback

1. Revert to `@company/core-utils@1.4.2`

2. Redeploy `auth-service` and `api-gateway`

3. Clear Redis cache for keys matching `auth:token:*`

If something breaks in production, you don't debug which of eight PRs caused it. You revert the most recent phase, verify health, then investigate. Goatfied's small-diff approach makes this tractable—each PR is reversible because it changed a narrow slice of the system.

Deploy to staging with selective package rollouts. Don't update all twelve packages in a single deployment. Use feature flags or canary deploys to route a small percentage of traffic through new code paths. Monitor error rates, latency, domain-specific metrics. For self-hosted Goatfied, the agent audit log traces which specific AI-generated edits correspond to performance regressions.

Practical rollout: start narrow, expand gradually

If adopting agentic planning today, don't start with cross-repo refactors. Begin with:

1. **Single-package renames**: stay within one `package.json` boundary, let the agent learn your test patterns

2. **Add constraint validation**: wire up compile/lint/test gates so failures surface immediately

3. **Gradually expand scope**: multi-package within the same monorepo, then coordinated cross-repo

Track how often the agent succeeds on first attempt versus requires retries. If retry rates stay high, your constraints are either too loose (agent makes invalid edits) or your tests don't cover integration points (agent produces diffs that break things downstream but pass local checks).

The theme across every failure pattern: missing or mismatched constraints. Agents need explicit rules about what must validate together (dependency clusters), what can run in parallel (independent subtrees), what needs human review (public API changes), and what counts as success (compile + lint + unit tests versus integration tests). In Goatfied's compile-first model, you define these upfront. The agent's plan phase generates a DAG of edits, the constrain phase prunes invalid paths, and the validate phase runs the right scope of checks. This doesn't eliminate failures—it shifts them left to plan time where they're cheaper to fix.

  • [Branch-aware context retrieval pipelines](/blog/branch-aware-context-retrieval-pipelines)
  • [Completion ranking with repository signals](/blog/completion-ranking-with-repository-signals)
  • [Context window budgeting under token pressure](/blog/context-window-budgeting-under-token-pressure)
  • [Cursor, Copilot, Continue, Cody, Windsurf, Codeium comparison framework](/blog/cursor-copilot-continue-cody-windsurf-codeium-comparison-framework)

Related posts

Agentic planning for cross-package refactors | Goatfied Blog