| name | code-structure-cleanup |
|---|---|
| description | Guides a service-layer extraction pass after an AI-built feature ships. Use when feature code works but contains duplicated mechanics, repeated API calls, or copy-pasted logic across multiple callers. Use when deciding what belongs in shared services vs. domain-specific actions. |
| hints | - Run cleanup AFTER the feature works, never during feature development. - Extract only logic repeated across 2+ callers — never abstract singletons. - Keep domain rules (auth, error classification, status transitions) in actions. - Replace one caller first, verify, then migrate the rest. - Keep the diff small and focused on the feature area only. |
AI agents often take the easiest path: they create new functions instead of reusing existing ones. A feature can work while still leaving behind duplicated logic, inconsistent validation, and repeated API calls that future agents will struggle to debug.
The fix is a two-layer architecture: actions orchestrate domain rules (the "why/when"), while a service layer centralizes reusable mechanics (the "how"). Run the cleanup pass after the feature works — not before, and not during.
- A feature works but the code has duplicated mechanics across multiple files.
- Multiple callers perform the same low-level operation (email sending, sandbox creation, API calls, data parsing).
- The agent created similar helper functions in different files.
- A bug fix in one flow was not propagated to other flows doing the same thing.
When NOT to use: Logic used by only one caller — extracting it is over-abstraction. Do not use this as permission to redesign the whole app.
Orchestration Layer (Actions) Service Layer (Shared Mechanics)
├── owns business rules ├── owns reusable operations
├── owns state transitions ├── owns provider/SDK interactions
├── owns auth/ownership checks ├── owns command execution details
├── owns failure classification ├── owns health checks / readiness
├── owns retries / user-facing errors └── returns structured results
└── calls service functions
Decision rule:
- "What this product flow means" → keep in actions
- "How to do this operation reliably" → move to service layer
Design as capability blocks, not monoliths. Each function should:
- Accept all required data as explicit parameters (no hidden global state)
- Return structured outputs — e.g.,
{ ready, previewUrl, proxyPort } - Never reach into the database or domain state directly
- Make failure explicit (structured results, not swallowed errors)
// Good: composable blocks — each caller picks what it needs
createManagedSandbox(...)
prepareRepo(...)
detectPackageManager(...)
installDependencies(...)
runBuildCommand(...)
// Bad: one god function that hides all control flow
doEverythingForSandbox(...)The feature is working. Now do a code-structure cleanup pass.
Goal:
- Find duplicated runtime mechanics, repeated API calls, repeated parsing, repeated validation.
- Move repeated mechanics into reusable service-layer functions/modules.
- Keep domain policy (auth, status transitions, error classification) in the calling route/action.
- Do not change user-facing behavior.
- Keep the diff small.
Process:
1. Inspect the files touched by the feature.
2. Identify repeated logic and name the duplication clearly.
3. Propose the smallest service-layer extraction.
4. Implement it.
5. Run the relevant tests/typechecks.
6. Summarize exactly what got simpler.- Write the flow in action code first — establish clear behavior.
- Mark repeated operational chunks across callers.
- Extract only repeated, non-domain chunks to a service function.
- Replace one caller first → verify tests pass → replace remaining callers.
- Run typecheck, lint, and confirm all flows still work.
// emailService.ts — shared mechanics (the "how")
export async function sendWelcomeEmail(params: { to: string; name: string }) {
const html = `<h1>Welcome ${params.name}</h1>`;
await emailProvider.send(params.to, "Welcome", html);
}
// userSignup.ts — orchestration (the "when" — different business rule)
if (user.marketingOptIn) {
await sendWelcomeEmail({ to: user.email, name: user.name });
}
// adminInvite.ts — orchestration (same mechanic, different domain rule)
await sendWelcomeEmail({ to: invitee.email, name: invitee.name });| Rationalization | Reality |
|---|---|
| "I'll clean it up later" | Later never comes. AI agents copy existing patterns — duplicated code compounds with every new feature. |
| "It's only in two places" | Two callers is exactly when to extract. Three callers means the window for clean extraction is closing. |
| "Cleanup will change behavior" | It should not. Behavior-changing cleanup is a bug fix or refactor — do it separately. |
| "I'll just refactor the whole module while I'm here" | Unscoped cleanup creates risky diffs and noisy PRs. Stay focused on the feature area. |
| "The service should handle business logic too" | Services own mechanics, not decisions. Auth, error classification, and policy stay in actions. |
- A bug fix applied in one flow was not applied to other flows doing the same thing.
- Helper functions with the same logic scattered across action files.
- Service functions that directly mutate database tables or domain state.
- A single "do-everything" service function hiding all control flow.
- Cleanup and feature development mixed in the same PR.
- Extracting logic used by only one caller (premature abstraction).
After completing the cleanup pass, confirm:
- User-facing behavior is unchanged (existing tests still pass).
- Duplicated mechanics were reduced — callers now share the extracted function.
- Calling files became simpler, not more complex.
- Domain policy (auth, transitions, error classification) remained in actions.
- Typecheck and lint passed.
- Diff is focused on the feature area — no unrelated changes mixed in.