Releases: cortexkit/aft
v0.41.0
v0.41.0: search and navigation at any repository size
This release removes the size limits that used to disable AFT's search and call-graph features on large codebases, and extends semantic search to eight more languages. The headline change is structural: the trigram index that powers grep and aft_search is now disk-backed, so it holds a fixed, small amount of memory no matter how large the repository is. On Chromium (176,000 source files) the index dropped from ~14 GB of RAM to under 800 MB — roughly 18x less — with no change to results and no measurable slowdown on normal repositories.
Because memory is no longer the constraint, the file-count caps that existed only to bound it are gone. Search and call-graph navigation now work on repositories of any size, where before they silently switched off above a threshold.
Search works on repositories of any size
The trigram index used to be held entirely in memory, which forced a 20,000-file ceiling: above it, AFT disabled the index and grep/aft_search fell back to a slower full scan. That ceiling is removed.
- The index is now disk-backed. Posting lists live on disk and are read on demand per query; only a small directory and the per-session edits stay in memory. Peak memory is now bounded by the index structure, not the repository size.
- No 20,000-file cap. Monorepo-scale repositories (Chromium, large Java estates, and similar) keep full indexed
grep/aft_searchinstead of dropping to fallback. - Results are unchanged. Output is byte-identical to the previous in-memory index, verified against the full search test suite. On a 1,200-file repository query latency is identical; at 160,000 files it is within single-digit percent — a small cost for working at a scale that previously had no index at all.
- Builds stay incremental. The index builds in the background and updates only changed files, as before.
Call-graph navigation has no file-count limit
aft_callgraph operations (callers, call_tree, impact, trace_to, trace_to_symbol, trace_data) and cross-file symbol moves (aft_refactor move) were capped at 5,000 source files; above that they returned a "project too large" error. That cap is removed. These operations are served from the persisted call-graph store, which scales to large repositories, so they now work regardless of project size.
Semantic search covers more languages
aft_search's semantic lane now indexes eight additional languages that AFT already parsed for outlines and navigation but did not embed for meaning-based search: Java, Kotlin, Ruby, Swift, Scala, Lua, Perl, and R. (PHP was already covered.) Semantic search now spans every language AFT extracts code symbols for; only plain-document formats (Markdown, HTML, JSON) remain text-only. Existing indexes are not rebuilt — the new languages are picked up as files are indexed.
v0.40.3
v0.40.3
read now hands images and PDFs to the model.
Images and PDFs are read as attachments again
When you read an image (PNG, JPEG, GIF, WebP), AFT now passes it to the model as a real visual attachment instead of a "binary file" placeholder — so a vision-capable model actually sees the picture. Images are decoded and downsized (longest side capped at 1024px) so they stay light in context. On OpenCode, read also hands PDFs to the model as attachments.
Non-vision models are handled gracefully: the image is omitted with a short note rather than breaking the read. Text files, directories, and non-media binaries behave exactly as before.
This restores behavior the host's built-in read provides; AFT's read had been returning a placeholder instead.
No action needed.
v0.40.2
v0.40.2
A patch release that finishes the v0.40.0 config consolidation for the CLI.
aft doctor / setup now read config from the right place
v0.40.0 moved AFT config to the shared CortexKit location (~/.config/cortexkit/aft.jsonc), but the aft CLI (doctor, setup, doctor lsp) was still looking in the old per-harness directories (~/.config/opencode, ~/.pi/agent). Since the migration renames the legacy file aside, aft doctor reported your config as "(not set)" even when you had a valid one, and aft setup wrote $schema to the dead path.
The CLI now resolves AFT config from the same canonical path the plugin uses, so the two always agree. This also fixes a fresh-install case where aft setup could create aft.json (which the plugin doesn't read) instead of aft.jsonc.
No action needed. Your harness config (opencode.jsonc) and tui.jsonc are unaffected.
v0.40.1
v0.40.1: TUI loads on OpenCode 1.17.10
A patch fix for the AFT sidebar and /aft-status failing to load on OpenCode 1.17.10.
Fix
OpenCode 1.17.10 bumped its bundled OpenTUI to 0.4.2. AFT's TUI entry is raw TSX that needs @opentui/solid and solid-js at import, and the plugin had been relying on the host providing them. On 1.17.10 that copy became unreachable from the plugin, so the sidebar and /aft-status failed to load with Cannot find module '@opentui/solid/jsx-dev-runtime'.
The plugin now declares @opentui/core, @opentui/solid, and solid-js as its own dependencies, matching the host's pinned versions, so the TUI resolves its rendering runtime regardless of how the host bundles it. A new test imports the TUI entry the way OpenCode loads it, so this class of breakage is caught in CI going forward.
If you updated to OpenCode 1.17.10 and lost the AFT sidebar or /aft-status, this restores them.
Questions or feedback? Join us on Discord.
v0.40.0
v0.40.0: groundwork for Subconscious
This release lays the foundation for AFT to run under Subconscious, the CortexKit daemon: a single shared service per machine that hosts code intelligence for every coding agent you use, instead of each agent spawning its own copy. Today AFT runs one process per project per agent, which means duplicate file watchers, indexes, and language servers when you have more than one agent open on the same code. Under the daemon, that becomes one shared index, one watcher, and one config per project.
The daemon integration itself is dormant in this release — nothing changes in how you run AFT today. What lands now is the groundwork: a single harness-agnostic configuration location, the internal substrate the daemon needs (path identity, per-project isolation, concurrent request handling), all behaving exactly as before when AFT runs standalone. Alongside it, this release closes a class of bugs where AFT could turn a real command failure into a clean-looking summary, and makes the top search result a full, ready-to-edit symbol.
Configuration moves under .cortexkit/
The most visible piece of the groundwork. Config moves to one harness-agnostic location:
- Global:
~/.config/cortexkit/aft.jsonc - Project:
<project>/.cortexkit/aft.jsonc
This happens automatically on first launch. AFT moves your existing config (~/.config/opencode/aft.jsonc, ~/.pi/agent/aft.jsonc, <project>/.opencode/aft.jsonc, <project>/.pi/aft.jsonc) to the new path and leaves an aft.jsonc.MOVED_READPLEASE marker at each old location naming the new path, so a later edit to an old file is not silently ignored. If your OpenCode and Pi configs differ, the harness you open first wins and the other is preserved alongside it as aft.jsonc.<harness>_OLD. Your settings are never dropped to defaults, so the semantic index is not rebuilt.
Project-supplied TOML compression filters also move, from <project>/.aft/filters/ to <project>/.cortexkit/aft/filters/. These are not auto-migrated: if you have trusted project filters, relocate them once with git mv .aft/filters .cortexkit/aft/filters. You do not need to re-grant trust (it is keyed by project root, so it carries over). Filters left under .aft/filters/ are no longer read.
Bash output never hides a failure
AFT compresses noisy test/build/lint output to save tokens. A multi-model audit found cases where that compression, or AFT's pre-execution rewriting of piped commands, could hide a real failure, so an agent proceeded on a false "all clear". This release closes that class:
- Piped commands run exactly as written. AFT used to rewrite
cargo test | grep -v failinto a barecargo testbefore running it (to compress the runner's full output), which silently changed the pipeline's exit status and hid failures expressed through the filter. AFT no longer rewrites the command: your pipeline runs verbatim, its real exit status is reported, and AFT's compression applies only to commands without a top-level pipe. - Cancelled work reports as failed. A killed or aborted background task now carries a non-zero exit, so a
cargo testyou stop mid-failure is never compressed to a passing summary. - No clean summary over a failing exit. On a non-zero exit, if the compressed text carries no failure signal AFT falls back to the raw output instead of trusting a tool summary. Compile, linker, and lifecycle errors (Go, Cargo, Bun, pnpm, pytest) are preserved rather than stripped, and
eslint: no issues/biome: no diagnosticscan no longer survive a failing run. - Truncation keeps the error. Output capping now keeps each stream's tail, so a late stdout error after a large stderr is no longer dropped, and oversized output returns head plus tail instead of empty.
Search: the top result is the full symbol
- The top
aft_searchresult now renders the complete symbol (signature, body, leading doc and decorators), zoom-quality, capped at 250 lines, with a note that it is current on disk so you do not need to re-read or zoom before editing. - Test files (
*.test.ts,*_test.rs,__tests__/, and the like) are hidden from results by default. PassincludeTests: trueto include them. - Results carry a compact blast-radius annotation (caller count and nearby callers) so you can gauge a symbol's reach at a glance.
Editing and formatting defaults
format_on_editis now off by default. When you opt in, formatting runs on a 90-second idle debounce in the background rather than synchronously after each edit, so it no longer competes with your turn.- Bash defaults to the foreground. Guidance now runs commands in the foreground (auto-promoted to background after 15 seconds if they run long) instead of steering toward
background: trueplusbash_watch. apply_patchend-of-file hunks anchor at the end only, so an EOF-anchored change can never be applied to an earlier matching block.
/aft-status
The /aft-status slash command no longer leaks an error into the TUI or the plugin log when it opens, and it appears once in the slash menu instead of twice.
Fixes
aft_inspectcalled directly now returns fresh Tier-2 results (a synchronous incremental recompute) instead of a possibly-stale snapshot, while the background status bar stays async.aft_outlineaccepts a JSON-stringified arraytarget, andaft_deletehonors a stringified"true"forrecursive, matching what some hosts and models send.- The code-search nudge no longer fires for
grep/rgthat run outside the project (for example aftercdinto another repository). - Distinct MCP clients that sanitize to the same name no longer share a storage directory.
- A Windows file-lock contention error during concurrent filter-trust writes is now retried instead of failing.
- The file watcher skips high-churn ignored directories (
target/,node_modules/) before resolving paths, cutting a CPU spike during builds.
Contributors
Thanks to @tobwen (#139, #140, #141) for the UTF-8 byte-boundary output reads, pipe-aware compression dispatch, and PTY cursor-report detection across read boundaries, which we built on for the bash-output work above.
Questions or feedback? Join us on Discord.
v0.39.4
v0.39.4: clearer signals, no surprise blocks, tighter listings
A patch focused on closing gaps where AFT did the right thing but withheld the information needed next: a sync wait that turned a busy bridge into a red failure, diagnostics that reported a count without the message, and formatter reflows that left a follow-up edit anchored on stale text. Plus a clean fix for external-path access under restrict_to_project_root, and folded ls/find output that keeps the entry that matters.
bash_watch no longer fails on a busy bridge
A sync bash_watch polls the bridge while it waits. Each poll carried a 30-second transport timeout, so if the bridge was briefly busy (a background scan, a parallel tool call), one poll could time out and abort the whole watch — surfacing a red "bash_status timed out after 30000ms" even though the task was fine and the bridge stayed warm. Re-issuing the watch hit the same window, repeating the failure. A busy-bridge poll timeout now means "retry," not "the watch failed": the wait continues to its own deadline, and only reports a graceful "bridge stayed busy" if it genuinely can't read status in time. A real command failure still surfaces immediately.
aft_inspect shows the diagnostic, not just a count
aft_inspect reported diagnostics: 1 errors but kept the message and location behind the sections parameter, like the other categories. A bare count isn't actionable — there's no fixing "1 error" without knowing what and where. Diagnostics detail (message + file:line) is now always included, capped by topK. It self-suppresses when there are no diagnostics, so a clean inspect costs nothing extra. The other categories (dead code, unused exports, duplicates) stay behind sections — their detail can run to hundreds of rows and a count there is a useful at-a-glance signal.
Edits surface formatter reflows
When the project formatter reflows an edit — rustfmt splitting a one-line call across four lines, Prettier re-wrapping an object — the file on disk no longer matches the submitted text. A follow-up edit anchored on the original text could then fail to match. Edit results now show the reflowed region's on-disk text when the formatter changed the edit, so the next edit can re-anchor on what's actually there. It only appears when a reflow happened; a no-op format adds nothing.
restrict_to_project_root blocks cleanly (#125)
With restrict_to_project_root: true, reading or globbing a path outside the project failed after you granted the permission prompt — the grant never reached the isolation boundary, so access was approved and then refused. restrict_to_project_root is AFT's full-isolation knob and is no longer tied to the host permission system: an out-of-project path is blocked up front with a clear explanation (and a note on how to allow external paths), instead of a prompt that can't be honored. With the setting off (the default), external paths work exactly as before.
ls and find keep the entry that matters
Long ls/find output was compressed by truncation, which dropped entries from the middle of the listing — including the specific file being searched for. Runs of similar names now fold to a single summary line (module_*.ts — 200 files (module_000.ts … module_199.ts)) while any structurally distinct entry stays in the listing verbatim. A 200-file directory collapses to a few lines, and the one distinct outlier survives.
Smaller refinements
aft_zoom suggests close symbol names when a requested symbol isn't found. aft_callgraph marks edges resolved by name alone with a trailing ~, so a possibly-wrong same-name match is distinguishable from an exact one; the marker is explained in the tool description.
v0.39.3
v0.39.3: cleaner bash control, readable callgraph output, Quarto, and config you can trust
A patch with a broad set of UX and correctness improvements: bash.background: false now fully removes the background surface instead of half-disabling it, aft_callgraph returns readable text instead of raw JSON, Quarto/R-Markdown files outline, malformed config no longer fails silently, and pipeline commands ask for permission once.
Bash: background: false now means it
Setting bash.background: false used to only block explicit bash({ background: true }) — long foreground commands still auto-promoted to background, and the bash_status/bash_kill/bash_watch tools stayed registered (#124). Now it fully disables the background surface: those tools aren't registered, the background/pty parameters are removed from the bash tool, and long commands run to completion in the foreground (bounded by timeout, default 30 minutes). If you keep background on (the default), nothing changes.
Bash waiting guidance, rebuilt around interruptible waits
Sync bash_watch is now interruptible — sending a message while it's waiting disengages the wait and converts it to a background notification. The old guidance told agents not to sync-wait on long commands because it "locks the user out"; that's no longer true, and the advice pushed agents into polling bash_status in a loop instead. The tool descriptions and workflow hints now steer agents to block on bash_watch when the result is the next thing they need (it's safe — you can always interrupt), end the turn when there's parallel work, and never loop bash_status to wait. The sync bash_watch timeout cap was raised from 5 minutes to 30 minutes so it can actually wait out a real build.
One permission prompt per pipeline (#123)
A piped bash command (cat x | grep y | wc -l) used to fire a separate permission prompt for each stage. Those are now collapsed into a single prompt. Access to paths outside the project root is still prompted separately.
aft_callgraph returns readable text
aft_callgraph was the last tool still dumping a raw JSON envelope to the agent. It now returns compact, grouped text — caller files with their call sites, call trees, trace paths, and impact sites — matching the format of the other tools. Repeated callers of the same symbol within a file collapse onto one line. Unresolved callees in a call tree are marked [unresolved] so a callsite is never mistaken for a definition.
Lower-memory callgraph cold build (#122)
The callgraph store's cold build now parses files in batches and bounds the method-dispatch pass, reducing peak memory on large repositories. The resulting graph is identical to the unbatched build. Batch size is tunable via callgraph_chunk_size (default 100; set 0 to parse all at once).
Quarto and R-Markdown (#113)
.qmd and .Rmd files now outline and zoom through the Markdown path (ATX and Setext headings). The Markdown heading extractor was reworked along the way for more reliable section ranges.
Config that fails loudly (#120)
A syntax error in your aft.jsonc used to be swallowed — AFT silently fell back to defaults and you only found out via doctor. A config file that exists but can't be parsed now surfaces a visible warning telling you it was ignored and pointing you at doctor. (A missing config file is still silent, as before.) Also: the /status dialog now closes on Enter as well as Esc, matching its footer.
Wider Linux compatibility (#119)
The linux-x64 binary is now built with cross, lowering the glibc floor (older distributions like Ubuntu 18.04/20.04) and bringing it in line with how the arm64 binary is already built.
Thanks to @dragon-Elec for the low-memory callgraph cold-build work (#122/#121) and @RoninZc for the cross linux-x64 build (#119/#118).
Questions or feedback? Join us on Discord.
v0.39.2
v0.39.2: no more "bridge keeps getting killed", configurable timeouts, quieter sandboxes
A patch centered on bridge stability: a passive status poll can no longer kill the bridge out from under your work. Plus the bridge timeout and restart threshold are now tunable for slow filesystems and multi-window setups, and AFT no longer looks broken in restricted sandbox environments.
The sidebar status poll no longer kills a busy bridge (#117)
The TUI sidebar polls the bridge for status roughly every 1.5 seconds. When the bridge was busy with real work — say a background Code Health scan kicked off by your edits — those polls queued behind it and timed out. Two timed-out polls tripped the "bridge looks hung, restart it" rule, which killed and respawned the bridge — aborting the edit or read you were actually waiting on. The agent then saw "the bridge keeps getting killed" and retried straight back into the same trap.
A passive status poll timing out means the bridge is busy, not hung, so it no longer counts toward the restart rule and now falls back to the last cached status immediately instead of blocking. Your real work is never interrupted by a health check again. (Most visible on Windows and large repos, where the background scan runs longer, but it was cross-platform.)
Configurable bridge timeout and hang threshold
Related to the above: on slow filesystems (WSL/DrvFs, NFS, network mounts) the aft binary's cold start and file walks can legitimately exceed the built-in 30-second request timeout, and combined with the fixed "restart after 2 consecutive timeouts" rule this produced a restart loop. The same fixed threshold also bit setups where several editor windows share one bridge process. The #117 fix removes the most common trigger (passive polls); these knobs let you tune the remaining behavior for genuinely slow environments.
Two settings are now configurable in aft.jsonc (defaults unchanged, so nothing changes unless you opt in):
{
"bridge": {
"request_timeout_ms": 30000, // raise on slow filesystems
"hang_threshold": 2 // raise when many windows share one bridge
}
}These are user-level settings (a project config can't change them, since they govern the bridge's restart safety and your machine's transport budget).
Graceful degradation in restricted sandboxes
- cpuinfo noise (#97): in some sandboxes, ONNX Runtime's bundled CPU-detection library logs
Error in cpuinfo: failed to parse processor information from /proc/cpuinfo. That's a benign third-party message, not an AFT failure — AFT never depended on/proc/cpuinfo. It's now filtered so it doesn't surface as an AFT error; genuine errors still show. - watcher unavailable (#111): when the filesystem watcher can't attach (often an inotify limit in sandboxes), AFT already kept working — it just lost live detection of external file changes. It was presented as an alarming error; it now reads as an honest soft degradation: "file watcher unavailable; continuing without live external-change invalidation."
Thanks to @herjarsa for the bridge-timeout investigation and the original implementation it grew from.
Questions or feedback? Join us on Discord.
v0.39.1
v0.39.1: lighter background CPU, sharper Code Health, and a calmer bash watch
A patch focused on what AFT does while you work: the background scans cost far less CPU per edit, Code Health gets more accurate, and a couple of sharp edges are filed off.
Background scans barely touch your CPU now
Tier-2 (dead code, unused exports, duplicates) re-checked file freshness by re-hashing every file in the project on each change. It now compares file stat metadata and only re-reads the file that actually changed — about 10x less work per edit, and the gain grows with repository size. This is the background-CPU cost that large-repo users felt after every save.
A companion fix makes the watcher recover correctly when the OS drops filesystem events under load (a large checkout or branch switch): instead of silently missing changes, it falls back to a single project rescan.
Code Health accuracy
- Modules imported only for their side effects (
import "./setup") no longer make their exports look dead, and reachable code behind them is counted. - After a failed incremental refresh, stale call-graph rows are no longer projected as if fresh — dead-code results report honestly instead of showing wrong counts.
- Editing a
tsconfig/jsconfig(includingextendschains and a package's owntsconfigfield) now correctly invalidates dead-code and unused-export results that depend on path resolution. aft_searchreports files it couldn't read instead of silently counting them as zero matches.
Code Health speed
The call-graph cold build is about 13% faster, and the incremental call-graph reads, oxc reachability pass, and search-index cold build all do less redundant work. Results are unchanged.
Fixes
- bash watch could freeze the host. A sync
bash_watchwith a regex pattern compiled and ran that regex on the host event loop; a pathological pattern over large output could hang the UI. Regex matching now runs in the Rust engine (linear, no catastrophic backtracking), and sync/async watches now share the same line-anchor semantics. - apply_patch checkpoints and diffs. A multi-hunk patch that touched the same file twice could lose the original from the undo checkpoint or report the wrong per-file diff. Checkpoints now snapshot every touched path up front, and metadata is tracked per hunk.
Quality of life
aft_zoomaccepts space-separated symbol names in thesymbolsstring for code files (e.g."foo bar baz"), splitting them into separate lookups.- The "use aft_search" nudge on bash
grepno longer fires when the search targets a path outside the project, whereaft_searchwouldn't help. - The sidebar remembers its collapsed state across restarts, sorts predictably alongside other CortexKit sidebars, and lets you hide individual sections via
tui-preferences.jsonc.
Questions or feedback? Join us on Discord.
v0.39.0
v0.39.0: Code Health that's accurate, incremental, and barely touches your CPU
This release makes Code Health trustworthy and cheap: the dead-code numbers are correct, and the background scans now reparse only the file you changed instead of the whole project on every edit. Plus R language support and pending diffs in edit approval prompts.
Code Health: accurate Rust dead code
The dead-code analyzer was flagging constructors and associated functions as dead even when they had real callers. Type::new() calls resolved through receiver-type inference were being discarded, so anything reached only that way looked unused. On AFT's own source this over-reported by ~80 symbols — the entire ::new false-positive cluster. Those calls now count toward liveness, while looser name-only matches stay excluded to avoid the opposite error.
Code Health: scans no longer rescan the whole project on every edit
Tier-2 scans (dead code, unused exports, duplicates) used to re-parse every file in the project after each change — a CPU burst of several seconds on a mid-sized repo, far worse on large ones, every time you saved. This was the background-CPU complaint behind several reports.
The scan now caches each file's local facts and reparses only the file that actually changed; the cross-file liveness pass runs over cached facts with no re-parsing. On AFT's repo a single-file edit dropped the dead-code parse from re-reading ~1,800 files to exactly one — measured live at ~130ms of scan work where it used to be ~3 seconds. Results are byte-identical to a full cold scan, verified across importer-removal, file-deletion, rename, barrel/pub use re-exports, tsconfig alias changes, cross-file type references, and manifest entry-point edits.
The two earlier per-scan optimizations are still in here too: dead-code edge lookups went from quadratic to grouped (a ~5x cold-scan speedup on this repo), and the call-graph cold build got ~14% faster.
R language support
.R/.r files now work in aft_outline, aft_zoom, and the AST tools (ast_grep_search/ast_grep_replace). Functions (including the name <- function(...) assignment form), top-level assignments, and calls extract with correct ranges. (#112)
Pending diff in edit approval prompts
When OpenCode asked for approval on an AFT edit, the prompt showed No diff provided instead of the pending change — you couldn't review an edit before approving it. AFT now computes the diff before asking: write diffs the new content client-side, edit previews its transform without committing, and apply_patch previews through the same applier it commits with, so the diff you approve is exactly what gets written. (#115)
Fixes
- Code-search guidance now appears for
grep/rganywhere in a command chain, not only when it leads — multi-statement scripts were slipping through. - The Tier-2 staleness ceiling that forces a refresh during continuous editing was raised from 10 to 30 minutes, so a long refactor no longer triggers a mid-session scan of half-applied changes.
- The bundled tokenizer data is regenerated atomically, so a concurrent build can't observe a half-written file.
Questions or feedback? Join us on Discord.