In this post I’ll describe an improved method to guide the output of code-assist tools towards more maintainable code. In a previous post, I described a refactor approach where periodically I would command the tool to break down files into smaller modules. However knowing (or remembering) when to run it, and having it based on some arbitrary line count was not ideal. That earlier approach has now been superseded by a complexity-based approach that runs as a part of the normal verify workflow.

Other posts in this series

  1. Steering the Vibe: Commits
  2. Steering the Vibe: Verify
  3. Steering the Vibe: Refactor
  4. Steering the Vibe: Review
  5. Steering the Vibe: Complexity (this post)
  6. Steering the Vibe: Permissions

My goto for code-assist is Claude Code and Opus 4.6, so I'll use terminology that relates to these tools, but the concepts within potentially apply to all.

Maintainability Index

On its own, lines of code don’t necessarily correlate to complexity. We can improve the approach by using known complexity metrics including:

  • Cyclomatic complexity - which measures the number of linearly independent paths through a program’s source code and;
  • Halstead complexity - which measures the complexity of a program based on the number of operators and operands in the code.

Combining these two metrics with lines of code allows us to calculate a Maintainability Index between 0 and 100, where a higher score indicates more maintainable code. We can choose some minimum threshold for this score and have the code-assist tool refactor files it has changed or created until the threshold is met.

Adding complexity tools to assist

To implement this I’ve extended my code-assist workflow cli to include a complexity command that exposes the various metrics and maintainability index. Thankfully I didn’t need to implement these calculations myself as ts-complex already has these. The assist tool can now report on these metrics directly:

# Analyze a file (all metrics if single match, maintainability if multiple)
assist complexity <pattern>
# Calculate cyclomatic complexity per function
assist complexity cyclomatic [pattern]
# Calculate Halstead metrics per function
assist complexity halstead [pattern]
# Calculate maintainability index per file
assist complexity maintainability [pattern]
# Count source lines of code per file
assist complexity sloc [pattern]

The maintainability command is the one we’ll use in verify and assist also uses the same itself:

"verify:maintainability": "assist complexity maintainability ./src --threshold 70"

Choosing a threshold and applying changes

The size of an existing codebase will influence what threshold is chosen. For a new codebase it’s easy to set a high threshold (like 70) as code that is added will be refactored until it meets that threshold. For existing codebases however, unless low complexity was a focus, there may be a lot of code that needs refactoring to reach even thresholds like 30. Large refactors burn tokens quickly - files must be constantly read, refactored, and re-analyzed until the threshold is met. For existing codebases, start low, running assist complexity maintainability ./src --threshold 20 yourself in the terminal to gauge the amount of files outside the threshold. As you increase the threshold, more files will appear.

Once you’ve chosen a threshold, calling /verify from within Claude Code will trigger the agent to refactor until the threshold is met. In my experience the agent preferred to work on files in parallel, but this is suboptimal as context window limits are easily reached and the codebase is left in a half-refactored state. To solve for this issue, a failed maintainability check will advise the agent to work in sequence. The agent also tends to want to investigate singular complexity metrics for a given file, so the output also advises how to do this:

Fail: 2 file(s) below threshold 50. Maintainability index (0–100) is derived from Halstead volume, cyclomatic complexity, and lines of code.

⚠️ Diagnose and fix one file at a time — do not investigate or fix multiple files in parallel. Run ‘assist complexity ’ to see all metrics. For larger files, start by extracting responsibilities into smaller files.

Silent configuration

Once a threshold has been met, subsequent modifications to the codebase via code-assist tools will automatically be refactored until new/changed code also meets the threshold. This is via verify configuration, which requires package.json edits, but what if we don’t want that configuration to be present in the repo? assist already supported a run command for calling arbitrary commands with arguments but now that configuration (which can be in (ignored) ./claude/assist.yml or (global) ~/.assist.yml locations) can also be used to configure verify.

In fact, because the configuration no longer requires javascript’s package.json, it can be used to trigger verification tools targeting anything. Here’s an example of how to configure assist verify (in claude /verify) to check rust formatting via a docker container:

assist run add verify:fmt docker compose run --rm lint cargo fmt --check

And the currently configured run commands can be listed via assist run list, or only the relevant verify commands via assist verify list:

# example from https://github.com/staff0rd/project-switch
assist verify list
verify:machete: docker compose run --rm lint
verify:fmt: docker compose run --rm lint cargo fmt --check
verify:clippy: docker compose run --rm lint cargo clippy -- -D warnings
verify:audit: docker compose run --rm lint cargo audit
verify:duplicate-code: npx jscpd --exitCode 1 -r consoleFull --pattern **/*.rs .

Keys that start with verify: will be collected along with those in package.json (if any) and run whenever /verify is called from Claude Code or whenever the user calls assist verify from the terminal. This allows for a lot of flexibility in how verification steps are configured and also allows for configuration to be kept out of the repo if desired.