Releases: cortexkit/aft
v0.35.4
v0.35.4: Large-repo CPU relief and concurrent-work stability
A patch release that stops two high-CPU situations on large repositories and under concurrent activity, fixes a crash on deeply-nested files, and hardens the semantic index, watcher, and background bash against transient failures.
High CPU on large repositories (#86)
On large repositories, aft_inspect's dead-code analysis built a whole-project call graph by re-parsing every file and resolving cross-file edges with no size limit, and it did so proactively shortly after the bridge started. On a repository with tens of thousands of files this pegged most CPU cores for a long time, even when no one asked for an inspection. Reported in #86.
The dead-code call-graph build now respects the same lsp.max_callgraph_files limit (default 5000) that the interactive aft_callgraph tools already use. Above that limit, dead-code analysis reports as unavailable instead of running the unbounded build. Duplicate detection and unused-export analysis are unaffected. Raise lsp.max_callgraph_files if you want dead-code analysis to run on a larger repository. A persisted, incremental call graph that lifts this limit entirely is in progress.
Embedding storm under concurrent activity
When the embedding backend returned a transient error during an incremental index refresh (for example a local LM Studio or Ollama server briefly overloaded by several projects indexing at once), AFT treated it as fatal and fell back to re-embedding the entire corpus. That full rebuild hit the already-busy backend, failed again, and cascaded into a sustained multi-core (and, with a GPU backend, multi-bridge) storm.
A transient refresh failure now keeps the existing index and retries only the changed files later, instead of dropping the cache and rebuilding everything. Only a genuine permanent error (such as an embedding-dimension change) triggers a rebuild.
Crash on deeply-nested files
A follow-on to the v0.35.3 crash fix: the call-extraction walkers used during inspection could still overflow the worker stack on a pathologically deep file (minified bundles, generated code, very long chained expressions), aborting the AFT process. Those walkers are now depth-bounded, so deep files are handled safely.
Resilience fixes
- The semantic index recovers from a brief embedding-backend outage instead of parking in a failed state, and detects the local runtime version more reliably.
- The watcher no longer leaves the index status stuck on "refreshing" after a transient hiccup, and sidebar status updates are pushed promptly.
- Background
bashkeeps its worker bridge alive across a transport timeout rather than tearing it down, so long-running tasks survive.
v0.35.3
v0.35.3: Inspect crash fix, semantic self-heal, Code Health in the sidebar
A patch release fixing a background-scan crash and a stuck semantic index, surfacing codebase health in the TUI sidebar, and fixing a silent LSP auto-install failure.
Inspect background scan no longer crashes on deep files
The duplicate-detection scanner walked each file's syntax tree recursively with no depth limit. A pathologically deep file (a minified bundle, generated code, or a very long operator/promise chain) could overflow the background worker's stack and abort the whole AFT process. The scanner now bounds its recursion and runs on a larger worker stack, so deep files are handled safely.
Semantic index recovers from transient backend blips
If the embedding backend was briefly unavailable when an index build started (for example restarting a local LM Studio or Ollama server mid-build, or a model still loading), the index parked in a permanent failed state and only a restart would retry it. Transient failures, including the HTTP 400 a local backend returns while a model is loading or being unloaded, are now treated as temporary: the index stays in a loading state, retries with backoff, and recovers on its own once the backend answers. A genuine misconfiguration (auth, dimension mismatch) still fails fast with a clear message.
Code Health in the TUI sidebar
The codebase-health counts that the agent sees in its status bar now also appear in the OpenCode TUI sidebar and the /aft-status view: live LSP errors and warnings, plus duplicate and TODO counts. The collapsed sidebar shows them as three at-a-glance traffic lights. Dead-code and unused-export counts are intentionally held back from this view for now while their accuracy on real TS/JS codebases is improved.
LSP servers install reliably again
AFT installs language servers with npm install --no-save into its own cache directory. When that directory has no package.json, npm walks up the tree and, if any parent (for example a package.json in your home directory) has one, installs into that package instead. The cache directory ends up empty while npm still exits successfully, so the install silently failed and you saw a recurring "LSP binary missing" warning even with lsp.auto_install enabled. AFT now writes a small anchoring package.json into the cache directory before installing, so the server lands where AFT expects it. Affects both OpenCode and Pi.
README
Fixed the YAML row in the Supported Languages table (wrong column count) and a missing blank line that made a horizontal rule render as a heading.
Thanks to @rustybret (#92, diagnosing the npm walk-up cause) and @Suknna (#93/#94, the README fix).
Join us on Discord: https://discord.gg/DSa65w8wuf
v0.35.2
v0.35.2 — Leaner edit output, GUI-launch npm fixes, and a background-task leak fix
A focused patch: edit results are now a compact line instead of a JSON blob, auto-update and LSP install work when OpenCode is launched from the dock with a stripped PATH, and background bash no longer leaves zombie processes behind.
Leaner edit output
edit now returns a single compact line — Edited (+3/-1)., Edited (+2/-0, 2 replacements)., or Applied edits to 2 files. — instead of the raw JSON envelope it used to hand the agent. The full diff still renders in the editor/TUI; the agent just gets the summary, which trims roughly 90 tokens off every edit, the most frequent operation. Rollbacks now say so in plain language, and both OpenCode and Pi return the same shape.
Leaner aft_search output
aft_search results are tighter too. The redundant JSON dump that followed the formatted results is gone, results are ordered by relevance, and snippet detail now scales with rank — the top hit shows the most context, lower-ranked hits show less — so a search costs fewer tokens without losing the signal that matters.
GUI-launch npm resolution
Launching OpenCode from the macOS dock (or any GUI) starts it with a stripped PATH that omits version-manager directories like mise, nvm, volta, and asdf. AFT's auto-updater and LSP auto-installer spawned a bare npm, which then failed — so the plugin could silently stay on an old version, or skip installing a language server. AFT now resolves npm from PATH, the active Node install, and the common version-manager locations before spawning. doctor --fix also repairs a plugin that fell behind because of this.
The auto-updater no longer removes the installed package before it has confirmed npm can reinstall it, so an interrupted update can't leave the plugin uninstalled.
Background bash zombie fix
A background bash task that ran short-lived child commands (for example a quick mv) could leave the child as a zombie process after it exited. The task lifecycle now reaps the child on its terminal path, so background commands no longer accumulate defunct processes.
Other fixes
- File tools (
read,write,edit,aft_outline,aft_zoom) now expand a leading~in paths, matching the search tools —~/Work/...resolves to your home directory instead of being treated as a literal folder. - The TUI sidebar now updates while a semantic index is building instead of appearing stuck, showing progress as it goes.
Join us on Discord: https://discord.gg/DSa65w8wuf
v0.35.1
v0.35.1
A patch release fixing three setup and configuration bugs.
Fixes
- Config files with comments no longer silently disable all settings. A
//or/* */comment placed inside a nested object inaft.jsonc(for example insidelsp.servers) could throw during validation and make the entire config fall back to defaults, silently turning off search index, semantic search, and everything else with no error shown. JSONC comments are now stripped before validation, so commented config loads correctly. Reported in #88. npx @cortexkit/aft setup/doctor/--versionno longer spawn runaway processes. When the nativeaftbinary was not yet cached (a fresh install, or mid auto-download), the CLI's binary lookup could resolveafton PATH to its own npx script shim and re-invoke itself, spawningopencode --versionandpi --versionin a loop. The resolver now accepts only real native executables from a PATH lookup, and the host version probes have a timeout, so this cannot happen.aft setupnow detects OpenCode Desktop. Detection previously required theopencodeCLI on PATH, so users running only OpenCode Desktop (which does not add a CLI to PATH) saw "no supported harness detected" and setup bailed. Setup now recognizes OpenCode from its config directory and app bundle, not just the CLI.
Join us on Discord: https://discord.gg/DSa65w8wuf
v0.35.0
v0.35.0 — Leaner semantic indexing, agent status bar, sharper aft_inspect
This release substantially cuts the CPU and memory that local semantic indexing uses, adds an IDE-style status bar that surfaces codebase health on tool results without the agent asking, makes aft_inspect substantially more accurate (dead code, duplicates) and keeps its background scans fresh during long sessions, and makes the TUI sidebar collapsible. It also adds npx @cortexkit/aft --version, session-scoped doctor --issue, and an actionable hint when a rust-analyzer-style toolchain component is missing.
Leaner semantic indexing
Local semantic indexing now uses roughly half the CPU and a fraction of the peak memory, with no change to search results and no re-index required.
- CPU is bounded. Embedding previously ran on every core, which pegged the machine and competed with the agent for the duration of a build. It now caps its worker threads at half the available cores. In our measurements that is about 40% less CPU and about 24% faster, because oversubscribing the cores was hurting throughput as well — on an 18-core machine the indexer now uses ~9 cores instead of all 18.
- Memory is bounded. A token budget now bounds each embedding pass so a batch that happens to contain very long symbols can no longer spike memory. On a 20,000-file repository the worst-case peak dropped from ~4.9 GB to ~1.3 GB, which keeps large-repo indexing safe on 8–16 GB machines.
- Existing indexes keep working. The reworked embedder produces vectors that match the previous backend, so your current semantic index continues to serve queries unchanged — no rebuild and no quality change.
Faster startup on large repositories
Files that contain no top-level symbols were being re-parsed on every bridge startup — visible in logs as a recurring "N new files" churn on large repositories. They are now cached like any other file and skipped on subsequent starts, reducing repeated startup work.
Agent status bar
Tool results now end with a compact one-line health bar — an IDE status-bar analog the agent sees as it works, instead of having to call a tool to check:
[AFT E0 W0 | D333 U221 C1160 | T8]
E/W are live LSP errors and warnings for files touched this session — a universal compile-error signal across every language with a language server, not just tsc/cargo. D/U/C are the aft_inspect Tier-2 counts (dead code, unused exports, clone/duplicate groups) and T is the TODO count. A ~ before D marks the Tier-2 counts as predating the latest edit, with current numbers a single aft_inspect call away.
The bar is emitted on change rather than on every call, so the transcript doesn't accumulate identical lines, and the latest bar always reflects current health. The legend is taught once in the system prompt, so each appearance costs only the compact values. Available on both OpenCode and Pi.
aft_inspect is more accurate and stays fresh
Dead-code accuracy. Reachability now propagates liveness through methods reached only by dispatch and through type references, recognizes real entry points from Cargo.toml and package.json (binaries, main/module/exports, script targets) instead of a filename heuristic, and resolves dist/ public-API entries and NodeNext .js import specifiers back to their src/ TypeScript sources. Together these remove large clusters of false positives — command handlers, barrel re-exports, and dispatch-live helpers that earlier versions flagged as dead.
Duplicate collapse. Overlapping and nested duplicate fragments now collapse to their maximal outer span instead of each sub-fragment counting as its own group, so the duplicate count reflects distinct duplicated regions rather than every nested slice.
Ranked drill-down. Findings are ranked by signal tier — product code before tests and tooling — and the summary now carries a cost-ranked top-N preview so the highest-value items surface first.
Background freshness. A watcher-driven refresh scheduler now keeps the Tier-2 scans current during a working session — debounced after edits, with a maximum-staleness ceiling so continuous activity still triggers a refresh — rather than waiting for an idle window that a busy session never reaches. This runs in the binary, so it works the same on OpenCode and Pi.
Collapsible sidebar (OpenCode TUI)
The AFT sidebar panel is now collapsible. Click the ▼ AFT header to collapse it to a condensed view — Search Index and Semantic Index status dots plus a compact compression line (7.7M / 64%) — and click ▶ AFT to expand it again.
Languages
- SCSS (
.scss) is now a recognized language for outline, symbol zoom, and AST search. .incfiles are treated as PHP, so the common PHP include extension gets full parsing, outline, and import support.
CLI
npx @cortexkit/aft --versionreports the CLI, binary, and per-harness host and plugin versions in one command.doctor --issuecan scope a report to a specific session. It lists your recent OpenCode/Pi sessions; choosing one filters the bundled logs to that session, while "General" keeps everything. Secret and path redaction still apply.doctor --fixadds the config$schemaURL when it is missing, so editor validation and autocomplete light up.- The harness picker for
doctoris now single-select;setupstays multi-select.
Fixes
- Deleted files no longer leave phantom diagnostics. When a file is removed (
aft_delete,rm, a branch switch, a failed rebase), its cached LSP errors and warnings are cleared instead of lingering in the warm set and inflating the status bar andaft_inspectcounts. - Missing toolchain language servers now get an actionable hint. When a server on
PATHis a toolchain proxy whose component is not installed — most commonlyrust-analyzer— AFT reportsrustup component add rust-analyzerinstead of the rawUnknown binaryerror. - Sidebar no longer flickers blank. A transient pre-initialized status snapshot during a binary swap or session-directory change no longer overwrites a good one and blanks the panel; the last good snapshot is kept until real data returns.
- Oversized YAML/Kubernetes manifests no longer break semantic indexing. A manifest packing a large inline script (a CronJob/Job
command:body) into one symbol could exceed the embedding backend's per-request token limit and abort the entire index build, silently degrading every search to lexical-only. Embedding text is now clamped so a single oversized symbol can't take down the build. editno longer drops a trailing newline on a fuzzy match. When a whitespace-tolerant match landed on the last lines of a file and the replacement text had no trailing newline, the final replaced line could be joined to the following line. The trailing newline is now preserved.- Partial overrides of a built-in LSP server work again. Setting
lsp.servers.<id>to tweak a single field of a built-in server (for example pointingrustat a custom binary) no longer requires re-specifyingextensionsandbinary; the built-in's values are inherited, and the wholelspconfig section is no longer silently dropped when they are omitted. - Call-graph queries are now timed in the logs.
aft_callgraph callerson a large repository logs how long parsing and edge resolution took, so a slow first call (or aproject_too_largerejection) is attributable instead of appearing as an opaque stall.
Pi
- The
aft_conflictsandgrepbash-output hints — which nudge towardaft_conflictsafter a failed merge and toward the indexedgreptool — now fire on Pi as well as OpenCode.
Thanks to @M0Rf30 (#79, semantic-indexing fix for oversized YAML/Kubernetes manifests) for the contribution to this release.
Join us on Discord: https://discord.gg/DSa65w8wuf
v0.34.0
v0.34.0 — Imports for 12 more languages, YAML/Kubernetes, cross-repo conflicts
This release extends aft_import from 5 languages to 17, adds YAML and Kubernetes/CRD-aware code intelligence, lets aft_conflicts inspect any repository or worktree, adds an .aftignore file and ripgrep-style explicit-file grep, makes aft_zoom call-graph annotations opt-in, and turns two previously-hardcoded limits into configuration.
aft_import now supports 12 more languages
aft_import (add / remove / organize) previously covered TypeScript, JavaScript, Python, Rust, and Go. It now also handles Solidity, Java, C#, PHP, Kotlin, Scala, Swift, Ruby, Lua, Perl, C/C++, and Vue — 17 languages total.
Each engine understands its language's real import shape rather than treating imports as plain text:
- Require-style languages (Ruby
require/require_relative, Luarequire(...)) preserve binding and call style. - Swift supports
@testableand kind-imports (import struct Foo.Bar); Java supportsstaticimports; Solidity supports named, namespace (* as), and aliased forms. - Scala 2 wildcard (
_) and rename (=>) syntax is preserved verbatim when organizing, so organize never rewrites valid imports into invalid ones. - C/C++
#includegroups system (<...>) before local ("...") and accepts modules with or without delimiters. - Vue Single-File Components rewrite imports inside the
<script>block, leaving<template>and<style>untouched.
New optional parameters cover the forms these languages need: namespace, alias, modifiers, and importKind.
Adding an import to a file that has a header but no existing imports now inserts after the header — after a Go package, a Solidity SPDX/pragma, a PHP <?php, or a Java/Kotlin package declaration — instead of at the top of the file. For a PHP file wrapped in a braced namespace Foo { ... }, the import is placed inside the namespace body, and a script starting with a #! shebang keeps the shebang on the first line.
YAML and Kubernetes/CRD support
aft_outline, aft_zoom, aft_search, and ast_grep_* now understand YAML (.yaml / .yml), and YAML files are embedded in the semantic index. Kubernetes and CRD manifests — any document carrying apiVersion + kind — become a single rich symbol named <namespace>/<Kind>/<name>, with high-signal spec fields folded into the embedding so intent queries match: container images and ports, CPU/memory limits, volume mounts, replicas, env var names, RBAC verbs/resources, and Argo Workflow entrypoint/templates/schedule. Plain YAML (docker-compose, CI config, Helm values.yaml) surfaces its top-level keys as symbols, and multi-document streams are handled per document.
aft_conflicts can inspect any repository or worktree
aft_conflicts now takes an optional path parameter, so you can point it at the repository or git worktree where a rebase or merge is actually running instead of only the session's project root. Discovery is anchored at that path's git top level (so it works from any subdirectory), and it unions unmerged-index files with a working-tree scan for conflict markers — so a file that was git added but still carries markers is reported instead of silently missed. The output now names the checked repository root, so a "No merge conflicts found" result is never ambiguous about which repository it actually examined.
.aftignore
AFT now honors an optional .aftignore file, layered on top of .gitignore. It uses the same syntax, is hierarchical, and works in non-git projects.
Use it to exclude paths from every AFT walk — trigram index, semantic index, call graph, aft_inspect, scoped diagnostics, and the ripgrep fallback — that you can't put in .gitignore, most commonly git submodules you don't want indexed. Edits under an .aftignored path also stop triggering reindexing, and editing the .aftignore file itself refreshes the indexes so newly-ignored files drop out without a restart.
grep searches explicitly-named files even when ignored
Naming a single file in grep (for example path: "captures/log.txt") now searches that file even when it is gitignored or .aftignored, matching ripgrep. Previously an explicitly-named ignored file returned 0 matches across 0 files, which read as "no matches" rather than "never searched". Directory searches still honor .gitignore and .aftignore as before.
aft_zoom call-graph annotations are now opt-in
aft_zoom no longer appends call-graph annotations (calls-out / called-by) by default. The default output is now just the symbol source, keeping zoom focused and token-light. Pass callgraph: true to include the annotations. For full cross-file call relationships, use aft_callgraph.
Configurable limits
semantic.max_files(default20000, raised from a hardcoded10000) caps how many files the semantic index embeds. Semantic search now works on monorepos that previously hit the 10k limit. Raise it further when using a remote embedding backend that embeds server-side, or lower it on memory-constrained machines.bash.foreground_wait_window_ms(default8000, minimum5000) controls how long a foregroundbashcommand runs before it is promoted to a background task. The previous window was a fixed 5 seconds.
Fixes
- C/C++ includes added with a delimiter (
#include <string>) are no longer double-wrapped into invalid#include <<string>>. - Import failures that roll back now report the failure instead of returning a success with no change.
- Backup loading no longer logs a per-entry warning for every legacy backup directory on startup; stale directories from older path-hash schemes are skipped quietly.
aft_inspectdead-code analysis resolves cross-file, barrel re-export, and namespace-import (import * as ns) liveness more accurately, reducing false positives.aft_importrejects module paths with..traversal segments or absolute paths for C/C++, Solidity, and PHP, andaft_import remove/organize_importsreportno_opwhen nothing changed.- ONNX Runtime detection now finds the library in a
lib/subdirectory, so manual installs of Microsoft's release archive are detected instead of reported as missing. - Formatter and type-checker lookup now searches system locations that GUI-launched editors miss from
PATH—/usr/local/go/bin(official Go installer),/usr/bin, and/snap/bin— so tools likegofmt,go, andgoplsare found when installed system-wide. aft doctor --issueno longer files automatically. It writes a sanitized report, asks you to review and edit it, and only files after you confirm. Redaction now also strips the project directory path and credential-shaped values (tokens, API keys,Authorizationheaders, URL credentials), and non-interactive terminals never auto-file.- ONNX Runtime archive extraction rejects symlinks that escape the staging directory, including across Windows drive letters.
- Formatter/checker warnings after
configurenow only appear when formatting or validation is actually enabled (format_on_edit/validate_on_editor an explicit per-languageformatter/checker), instead of firing from a project config file even when those features are off. - Configure warnings are delivered as idle-time notifications and no longer switch the session's model or agent (or stall the first tool turn) when they fire.
- Windows tool resolution finds Biome, Ruff, and other formatters/checkers installed via npm that are present on
PATH, instead of reporting them missing.
Pi
- AFT's namespaced tools in the Pi plugin (
aft_import,aft_refactor,aft_safety,aft_transform,ast_grep_replace,aft_delete,aft_move) now prompt for external-directory access whenrestrict_to_project_rootis enabled, consistent with Pi's hoisted file tools.
Thanks to @M0Rf30 (#73, YAML/Kubernetes support), @Zireael (#72, configure-warning gating and Windows tool resolution), and @Suknna (#75, unified configure-time tool detection) for their contributions to this release.
Join us on Discord: https://discord.gg/DSa65w8wuf
v0.33.0
v0.33.0
This release adds aft_inspect — a single codebase-health view for agents — and reworks how diagnostics reach the agent. It also renames aft_navigate to aft_callgraph and lands a large batch of aft_search correctness fixes.
aft_inspect — codebase health at a glance
aft_inspect gives an agent the "open the project and see what's wrong" view in one call, instead of stitching together greps and separate tools. One call returns a summary across categories; pass sections to drill into any of them.
Categories:
- diagnostics — compile/type errors from language servers
- metrics — file count, lines of code, symbol count
- todos — TODO/FIXME/HACK/XXX/BUG markers with file, line, and author
- dead_code — exported symbols with no reachable caller
- unused_exports — exported symbols nothing imports
- duplicates — near-identical code blocks across files
Treat dead_code as a hint, not an authority: reachability is call-based, so symbols reached only through method dispatch (receiver.method()) or referenced only in type position can still appear as false positives. Verify before deleting.
It runs in tiers. Metrics and todos return synchronously from cache. dead_code, unused_exports, and duplicates run as background scans on session idle; calls return cached results immediately (marked stale when a refresh is pending) rather than blocking. Results persist to disk per project, so they survive restarts.
Options:
sections— one category or an array;allfor everything; omit for summary-onlyscope— restrict to a file or directorytopK— cap drill-down items per category
Scan results persist across restarts
Tier-2 scan results are stored on disk. When you reopen a project after editing files, aft_inspect serves the last completed scan immediately (flagged stale) while a fresh scan runs in the background, instead of showing nothing until the rescan finishes.
Diagnostics now flow through aft_inspect
Diagnostics no longer arrive automatically after every edit. Previously edit, write, and apply_patch waited on language-server diagnostics inline on each call. That work moved into aft_inspect, which an agent runs after a batch of edits and before tests or a commit. The result is faster edits and a single deliberate point to check the codebase.
- The standalone
lsp_diagnosticstool is removed; its capability is part ofaft_inspect'sdiagnosticscategory. - A new
lsp.diagnostics_on_editconfig option (default off) restores per-edit diagnostics for users who want them. aft_inspectdiagnostics are build-authoritative: TypeScript/JavaScript files excluded bytsconfig.json(files/include/exclude, followingextends) are skipped, so results match a realtscrun instead of reporting phantom errors on excluded files.- Scoped diagnostics on a single file wait briefly for a cold language server to start and publish, so the result arrives in one call rather than requiring a re-run. Scopes whose file types have no registered or applicable server report a terminal
no_serverstatus instead of waiting forever.
aft_inspect diagnostics are a fast checkpoint, not the project-wide authority — a clean tsc / cargo check / pyright run remains the real gate.
aft_navigate is now aft_callgraph
The call-graph tool is renamed from aft_navigate to aft_callgraph. The new name matches what the tool does — answer code-relationship questions (callers, call tree, impact/blast radius, execution paths, data flow) from a real call graph — and is the term models reliably reach for. All operations (callers, impact, call_tree, trace_to, trace_to_symbol, trace_data) are unchanged.
The Rust call graph now resolves cross-file callers. Calls through crate::, super::, self::, a workspace crate name (e.g. a binary's mycrate::module::func(...)), and use-imported short names now resolve to the right definition. Previously only same-file callers were found, so callers/impact on a Rust symbol returned far too few results and dead_code over-reported. Liveness also now flows through private intermediary functions, so handlers reached only via a private router are no longer flagged dead.
aft_search correctness
A batch of fixes to the unified aft_search shipped in v0.32:
- Disabled vs. unavailable. With
semantic_search: false, a natural-language query now returns a clear "not enabled" result instead of silently degrading to a lexical scan. The degraded lexical fallback is reserved for when semantic is enabled but temporarily building or unavailable. - Truthful routing.
interpreted_asreports the lane that actually ran (lexicalwhen the semantic lane was skipped), not the lane that was requested. - Result provenance. Each result carries the lane that produced it plus a
hybrid_boostedflag, andmore_availablenow accounts for engine caps and semantic overflow so "more results exist" is accurate. - Quoted phrases. Surrounding quotes on a literal query (
"was not found on PATH") are stripped before matching, matchingrg -F/ code-search behavior, instead of searching for the quote characters. - Classifier routing. A sentence-final
?no longer forces regex mode; a query mixing natural language with an error code routes correctly; bare escape sequences route to regex while Windows-path-shaped queries stay exempt. - No hidden truncation. Lexical candidates beyond the old internal cap are no longer dropped silently before fusion.
Edits no longer echo back to the agent
edit, write, and apply_patch were returning unnecessary diff bytes back to LLM. The agent-facing result is now minimal (success plus added/removed line counts).
Bug fixes
Editing and refactoring:
aft_safetyundo history no longer lost on restart. The first edit to a previously-backed-up file after a restart could overwrite that file's entire on-disk undo history (and reuse a backup id). Prior history is now hydrated before the new snapshot is appended.apply_patchpure-insertion hunks insert at the resolved context line instead of end-of-file.aft_transform add_memberon a genericimpl<T> Foo<T>block now inserts methods into the impl block, not the struct body.aft_refactormove no longer adds a spurious import when a parameter or local variable shadows the moved symbol's name.aft_import organizecorrectly handles Rust nestedusetrees (a::{b::{c, d}, e}) without corrupting them.aft_zoomno longer raises spurious mutual-exclusion errors when a model sends empty or empty-contenttargets/symbolsparameters.
Search and indexing:
- The semantic index verifies per-file freshness strictly and uses canonical paths for watcher invalidation, so edits refresh the right entries and large files aren't downgraded to a weaker freshness check.
aft_outlineaccepts JSON registry/package URLs and outlines their top-level keys.- URL fetching for
aft_outline/aft_zoomretries transient connect/transport failures and now runs entirely in Rust.
Reliability and security:
- The worker bridge stays alive through transient starvation timeouts and only escalates to a kill on a genuine hang, so a busy bridge no longer drops unrelated requests.
- SSRF protection on URL fetches now blocks RFC 6598 (CGNAT,
100.64.0.0/10) and RFC 2544 (198.18.0.0/15) ranges, including their IPv4-mapped IPv6 forms. - Windows hardening: validated
LoadLibraryExWONNX Runtime loading, NuGet cache scanning, storage directory creation, and a binary-replacement retry (contributed in #66/#69). - Killing a PTY-mode background task on Windows now finalizes reliably instead of hanging, so
bash_killand the polling that waits on it always return. - LSP and formatter "not installed" warnings no longer re-fire on every session, and synthetic notifications preserve the active model and variant.
Join us on Discord: https://discord.gg/F2uWxjGnU
v0.32.0
v0.32.0 — Unified aft_search and queryable-during-refresh semantic
The headline change: aft_search is now a single tool that handles every code-search shape — exact identifiers, anchored regex, error messages, natural-language descriptions, and file/URL paths. It auto-routes between regex, literal, semantic, and hybrid lanes based on query shape, with a hint parameter for explicit overrides. Output adapts per mode — grep-style lines for regex/literal, symbol-blocks with provenance for semantic/hybrid. The semantic index now also stays queryable through edits instead of falling back to lexical-only after every save.
Unified aft_search
aft_search replaces the previous split between concept search and grep-style lookup. One query parameter, automatic mode detection, one consistent response shape per mode.
- Classification before status check. Regex queries succeed even when the semantic backend is unavailable; the lexical lane is always available when grep is registered.
- Pre-Tier path/URL exemption. Queries shaped like file paths (
src/lib/main.rs), Windows paths (C:\new\test), URLs (https://api.github.com), and filenames with metacharacters (is_valid?.ts,Cargo.lock) stay in hybrid mode instead of misrouting to regex. - Sequence-based regex detection. Sequences like
.*,.+,\d+, and[A-Z]correctly trigger regex routing while bare punctuation that commonly appears in code (map.get(),foo(),bar?.baz) stays hybrid. hintoverride. Passhint: "regex",hint: "literal", orhint: "semantic"to force a specific lane. Short literals (under three bytes) honorhint: "literal"with a full scan instead of silently rerouting to semantic.- Adaptive output per query mode. Regex and literal modes return grep-style
file:line: textmatches. Semantic and hybrid modes return symbol-blocks withsource: "semantic" | "lexical" | "hybrid"provenance per result. Theinterpreted_asfield tells callers which shape to expect. - Response flags reflect engine limits.
more_available,engine_capped, andfully_degradedreplace the previoustotal_matchesfield, which conflicted with the engine's caps.humanize_degraded_reasonstranslates internal codes to user prose. - Tier D rejections. Lookaround, backreferences, and other regex features the engine doesn't support return explicit errors with rewrite guidance instead of silent zero-match.
The two plugin layers use the same query classification before mutual-exclusion permission checks, so OpenCode and Pi behave identically.
Semantic index stays queryable through edits
Previously, aft_search fell back to lexical-only after every file save because the watcher invalidation set SemanticIndexStatus to Building. The in-memory index still held fresh embeddings for every unchanged file, but the query gate matched on Building regardless of stage and refused the semantic lane.
SemanticIndexStatus::Ready now carries a refreshing: Vec<PathBuf> list. Watcher invalidations append the changed file to that list without leaving Ready. The query path runs the normal semantic lane and adds a soft warning when files are mid-refresh. Building is now reserved for cold builds and fingerprint changes (model, embedding dimension, or base URL changed).
User-visible effects:
aft_searchreturns real semantic results immediately after edits, with a warning like"1 file(s) refreshing; results for those files may be temporarily missing".- The TUI sidebar and
/aft-statusdialog showReady (N file(s) refreshing)as a small dim line instead ofRebuilding…. Above 20 refreshing files it collapses toReady (many files refreshing). - The status RPC adds
refreshing_countto the semantic block. Existing fields are preserved.
Workflow hints promote aft_search
The system prompt's code-exploration section now teaches aft_search as the primary code-search tool, with grep framed as the specialized fallback for exhaustive enumeration (every TODO, every import of X) or strict path-scoped search. Users running with semantic_search: false continue to see the grep-primary hint unchanged.
Bare escape sequences route to regex
Bare \n, \t, and \r queries now correctly route to regex mode. They were missing from both tier_a_regex_signal and the path-exemption guards in the v0.32 classifier. Path-shaped queries containing those escapes (Windows C:\new\test) remain exempt and stay hybrid.
Empty params no longer mislead the agent
GPT-family models often send empty strings, empty arrays, and empty objects ("", [], {}) instead of omitting optional parameters. Previously, that triggered misleading mutual-exclusion errors like 'targets' is mutually exclusive with 'filePath', 'url', and 'symbols' when the agent only meant to pass filePath. The plugin now normalizes empties to undefined before mutual-exclusion checks.
Affected tools across both plugins:
aft_zoom—targets: []andsymbols: ""no longer trigger spurious exclusion errors.aft_refactor— required-field validation rejects empty strings forsymbol,destination, andnameinstead of accepting them and crashing downstream.ast_grep_search/ast_grep_replace— emptypathsandglobsarrays no longer round-trip to Rust as "scope present" when the agent meant whole project.
OpenCode's tool-call header also now stringifies array and object args into the rendered metadata so users can see what the agent actually sent in the call.
Join us on Discord: https://discord.gg/F2uWxjGnU
v0.31.1
v0.31.1 — Strict-LSP diagnostics, Windows doctor UX, Pi grep that doesn't hang
A patch release with two focused fixes: lsp_diagnostics now works correctly against strict LSP servers like tsgo, and aft doctor handles Windows setup gaps that used to produce silent dead ends.
lsp_diagnostics against tsgo and other strict servers (#63)
AFT was sending identifier: null and previousResultId: null in pull-diagnostics requests when those fields had no value, because the upstream lsp-types crate at 0.97 omits the skip_serializing_if = "Option::is_none" annotation on them. The LSP 3.17 spec defines those fields as string-or-absent, not string-or-null. Permissive servers like typescript-language-server accept the null and return diagnostics anyway; strict servers like tsgo reject the request with InvalidParams (-32602), and AFT then waited for push diagnostics that never came from a pull-only server. The user-visible symptom was lsp_diagnostics silently returning empty results for files that genuinely had type errors.
Fixed by introducing AFT-local AftDocumentDiagnosticParams and AftWorkspaceDiagnosticParams types with the missing serde annotations, sent through AftDocumentDiagnosticRequest / AftWorkspaceDiagnosticRequest using the same textDocument/diagnostic and workspace/diagnostic method strings as upstream. No behavior change for servers that accept either shape; tsgo now returns diagnostics correctly. Thanks to @null-axiom for the precise diagnosis.
aft doctor no longer hides Windows setup problems (#64)
Five related Windows / setup-UX fixes for aft doctor and aft doctor --fix:
-
Plugin/CLI version skew is now a visible issue. Running
npx @cortexkit/aft@latest doctor --fixagainst an installation with an older plugin (for example CLI v0.30.3 against@cortexkit/aft-opencode@0.29.1) used to silently download the newer binary into the cache, where the plugin would then ignore it because of strict protocol pinning. Doctor now detects the skew, surfaces a high-severity issue with remediation, and--fixprompts before downloading the binary instead of silently caching one that won't be used.--yesproceeds;--ciand non-TTY environments skip the download cleanly. -
Windows ONNX detection now scans
PATH. Users who install ONNX Runtime via Scoop or a manual zip on Windows typically put theonnxruntime.dlldirectory onPATH. The previous detector only looked in fixed locations and missed those installs. The new path addsPATHentries on Windows with conservative guards: absolute paths only, no current directory or null bytes, case-insensitive filename match. Mac and Linux detection is unchanged. -
Storage "not created" no longer reads as a failure. When AFT hasn't yet spawned a bridge in a session, the storage directory doesn't exist on disk — that's expected lazy behavior, not a problem. Doctor now says so explicitly, and
aft doctor --fixopportunistically creates the directory for registered plugins so the next session starts clean. -
Doctor output has an "Issues found" summary. The previous output was a wall of green checkmarks with any real warnings buried inline. The markdown report now leads with an
Issues foundblock — severity (HIGH/MEDIUM/LOW), scope, message, and remediation — for any non-zero findings. The full per-harness diagnostic stays below for context. Renders the same on TTY, CI, and--issuebug reports. -
bg-notificationslog noise. The plugin used to logWARN [aft-plugin] Live OpenCode HTTP listener unreachable, falling back to in-process promptAsyncon every wake delivery in non---port 0TUI sessions, which is the expected fallback path. The fallback transition is now DEBUG-level; the WARN level is reserved for cases where no wake transport actually delivers. Thanks to@Zireaelfor the detailed bug report.
Join us on Discord: https://discord.gg/F2uWxjGnU
v0.31.0
v0.31.0 — Cross-file navigation, indexed file trees, and Pi grep that doesn't hang
Three new agent capabilities plus a long-overdue Pi UX fix: external-path searches no longer freeze waiting on a permission prompt that has no policy behind it.
Trace the call path between two symbols
aft_navigate has a new trace_to_symbol op for "how does function A reach function B" queries — the single most expensive question to answer by hand. One call returns the shortest call path through every intermediate hop, with file and line for each node.
aft_navigate({
op: "trace_to_symbol",
filePath: "src/bridge.ts",
symbol: "send",
toSymbol: "spawn_child",
})
Returns either the shortest path (each hop annotated with file + line), or a structured error if the target is missing, ambiguous, or unreachable:
target_symbol_not_found— name doesn't exist anywhere in the indexed graphambiguous_target— multiple symbols share the name; rerun withtoFilefrom the candidates listtarget_symbol_not_in_file—toFileprovided but no matching symbol in it; candidate list returnedto_file_not_found— the file you named doesn't existno_path_found— the graph genuinely has no path
The default depth cap is 10; pass depth to raise it.
aft_outline files: true — indexed file tree with per-file metadata
aft_outline target: "<dir>", files: true now returns a flat indexed file tree with language, symbol count, and byte size per file — no symbol bodies, no signatures, just the structural metadata an agent needs to pick which files to actually open next.
aft_outline({ target: "packages/aft-bridge/src", files: true })
Reuses AFT's existing symbol cache, so the call is fast even on cold bridges. The output is honest about truncation: when a directory exceeds the 200-file walk cap, the response sets complete: false and surfaces both walk_truncated and unchecked_files so agents don't mistake a partial tree for a complete one. Multi-target calls render every entry as a project-root-relative path, so two files named lib.rs from different crates can't collide in the output.
Also accepts an array of directories: target: ["crates/aft/src", "packages/aft-bridge/src"].
aft_zoom — cross-file batches and a polymorphic schema
aft_zoom is the read-the-source-of-this-symbol tool. Two changes this release:
New: targets for cross-file batching. Previous symbols: [...] array could only zoom into multiple symbols within the same file. The new targets array lets agents pull bodies from different files in one call:
aft_zoom({ targets: [
{ filePath: "src/a.ts", symbol: "callBridge" },
{ filePath: "src/b.ts", symbol: "spawn_child" },
]})
Schema consolidation (breaking). symbol and symbols collapse into a single polymorphic symbols parameter that accepts either a string or an array. Same for targets (single object or array). URL mode follows the same shape, so an agent can pull multiple sections from a single URL fetch:
aft_zoom({ url: "https://docs.example.com/api", symbols: ["Authentication", "Errors", "Examples"] })
The four shapes (filePath + symbols, targets, url + symbols, and combinations) are mutually exclusive with a clear error when mixed. Old callers using symbol: "name" need to migrate to symbols: "name"; the surface change is small but it is a break.
Output-shape compression — fewer "what failed?" reruns
Bun, npm, and pnpm test compressors now match on the shape of the captured output, not just on the head token of the command. Wrapper invocations like bun run --cwd packages/foo test, npm test, pnpm test, or even bun test && echo done now go through the test-aware compressor instead of falling through to the generic line-dedup path. Failing test bodies and assertion diffs are preserved on the first run; the agent doesn't need to follow up with | grep fail to see what broke.
Pi: external-path tool calls no longer hang
The headline Pi fix. Pi grep/write/edit against a path outside the project root would block the bridge indefinitely on a ui.confirm "Allow external directory access?" prompt — even when the user had restrict_to_project_root: false (the Pi default) which explicitly opts into "no path restriction."
Three causes, all addressed:
-
No tilde expansion.
~/Work/...arrived in the plugin as a literal,path.resolve(cwd, "~/...")resolved to<cwd>/~/..., stat() failed, and Rust returnedpath_not_found. BothassertExternalDirectoryPermissionandresolvePathArgnow expand~/~/foobefore any check. -
No
ui.confirmtimeout. When Pi ran the call from a context that couldn't surface the prompt, the confirm promise simply never resolved. Now bounded at 30 seconds with a deterministic "Permission denied: prompt timed out" so the agent unblocks. -
No policy-aware skip. When
restrict_to_project_root: false— the Pi default and what the user explicitly opted into — the plugin used to nag anyway. Pi has no host-levelexternal_directoryallow-list to consult (unlike OpenCode), so the prompt had no policy behind it. The plugin now defers to Rust without prompting when the user opted into "no restriction."
Behavior matrix:
restrict_to_project_root |
Pi behavior |
|---|---|
false (default) |
Plugin defers to Rust; no prompt |
true + interactive UI |
ui.confirm with 30s timeout |
true + no UI |
Immediate deny with a clear error |
OpenCode's grep/glob path also gained tilde expansion, for parity. OpenCode external-directory checks already routed through the host context.ask({permission: "external_directory"}) which the host resolves against configured rules without blocking on a UI, so the hang did not reproduce there.
Tool descriptions: ~1.2K tokens trimmed
Dropped redundant Returns: blocks from aft_transform, aft_import, aft_refactor, aft_safety, ast_grep_search, and ast_grep_replace — agents see the actual response shape at runtime, no need to also restate it in the prompt. Collapsed lsp_diagnostics from a 700-token inline JSON schema + verbose honesty playbook to a 250-token version that keeps the load-bearing "don't claim 'no errors' when nothing was checked" rule. Standardized path-resolution wording across filePath / path / directory params so the surface is consistent.
Combined: 5,546 → 4,498 tokens (-18.9%) on OpenCode, 4,418 → 4,315 (-2.3%) on Pi. Pi gained less because it had no Returns: blocks to strip.
Per-tool, the biggest cuts:
aft_transform: 788 → 543 (-31%)lsp_diagnostics: 704 → 255 (-64%)aft_import: 435 → 281 (-35%)ast_grep_search: 484 → 384 (-21%)
A separate audit-driven pass also fixed three release-blocking description bugs and seven smaller polish items: an apply_patch claim about atomic rollback that no longer matches the actual per-file-commit behavior, a aft_outline({ url }) example that named the old parameter shape, and ambiguous mutual-exclusion wording in aft_zoom.
Other
trace_to_symbol: ambiguity recovery error now renders the full candidate list as plain text instead of swallowing it insidedata:. Agents can re-issue the call withtoFileimmediately.aft_outline files: true: now asks OpenCode for theexternal_directorypermission when the directory is outside the project, matching how other file-touching tools behave.bunoutput-shape compressor was claiming output from arbitrary text that happened to include aRan N tests across M filessummary. It now requires a structurally valid bun-test marker ((pass)/(fail)followed by name and duration) before claiming the output.- PTY watchdog test budget tightened below the watchdog poll interval so a passing test now actually proves the wake channel beat the periodic poll, instead of just measuring overall wall-clock.
- Pi added e2e and Pi-RPC coverage for
trace_to_symbolandaft_outline files: true.