Releases: cortexkit/aft
v0.27.1
AFT v0.27.1 fixes two GitHub-reported regressions and ships several polish items on top of v0.27.0.
Fixes
OpenCode LSP auto-install now uses npm (GitHub #46)
@vue/language-server and other newer LSP servers were never installing for users without bun on PATH, even with lsp.auto_install: true. The OpenCode plugin previously spawned bun add to install LSP packages, which silently ENOENT-failed when bun wasn't available. By the next configure, the failed binary was no longer in flight, so Rust correctly emitted Install vue-language-server and ensure it is on PATH — and the cycle repeated on every startup.
OpenCode now spawns npm install --no-save instead, matching the Pi plugin's existing behavior. npm is guaranteed to be present whenever the plugin reaches the user through the standard OpenCode distribution flows. After upgrading, missing LSP servers like Vue, Astro, and Svelte install automatically on next startup and the recurring warning disappears.
Honest no_op reporting for byte-identical writes (GitHub #45)
edit and write calls that resulted in byte-identical file content (e.g. oldString === newString, or a formatter that normalized the change back to the original) reported +0/-0 with no explanation, which agents read as "the tool is broken."
Rust now sets no_op: true on the response whenever the post-write file matches the pre-write state. The match was satisfied (replacements: 1), but agents and the TUI now see an explicit note:
- Pi sidebar/dialog renders
+0/-0 (no net change)in muted text instead of bare+0/-0 - Pi tool result text appends
no net file change — newString may be identical to oldString or formatting normalized the result - OpenCode
edit/writeoutput appends the same explanation
Applies across all 4 write/edit paths: find/replace, append, symbol replace, full-file write.
Per-project bridge config in OpenCode Desktop
OpenCode Desktop runs one plugin instance for many open projects. The plugin previously loaded AFT config once at startup from whichever directory OpenCode happened to launch from, then froze it for every project. If you opened a session in Project A whose .opencode/aft.jsonc set bash.background: false, the bridge for Project A still used Project B's config because Project B's was the one loaded at init.
Per-project AFT config now loads at bridge spawn time. Project-scoped fields take effect immediately when opening a session in that project:
experimental.bash.{rewrite, compress, background},experimental.lsp_tyformat_on_edit,formatter_timeout_secs,validate_on_edit,formatter/checkerper-languagerestrict_to_project_root,search_index,semantic_searchmax_callgraph_files,semantic.*, project-safelsp.*fields
Plugin-init-time decisions (tool_surface, disabled_tools, hoist_builtin_tools, ONNX runtime setup) stay global because they affect tool registration, not per-bridge configure. Pi is one process per session so this never affected Pi.
Polish
v0.27 startup announcement
OpenCode and Pi now surface a once-per-version dialog summarizing v0.27's headline changes — CortexKit storage migration, SQLite-backed bash task state, bash output compression rendering in /aft-status, and the new Discord link — so users who skipped the v0.27.0 release notes still see what changed.
JSON Schema for aft.jsonc
aft setup now writes a $schema URL pointing at assets/aft.schema.json in fresh aft.jsonc files, so editors with JSON Schema support (VS Code, IntelliJ, neovim with jsonls) get autocomplete and inline validation for AFT config keys. Existing config files are unchanged.
Quieter logs
Two routine plugin-log lines were demoted from INFO to DEBUG/silent:
- The per-bash
compression event recorded for ... (N → M tokens)line, which fires on every bash invocation - The
bash task replay DB miss for session __default__; falling back to diskline, which fires once per cold-start on a session-less configure (eager warm, anonymous protocol calls) and means nothing actionable
WARN remains for real lookup errors. Migration paths still log INFO when disk actually returns tasks worth surfacing.
Upgrade notes
No config changes required. Existing project bridges keep working unchanged. The npm-vs-bun LSP install fix takes effect on next plugin startup. The per-project config fix takes effect on next bridge spawn for each project.
v0.27.0
v0.27.0
AFT now has a single shared CortexKit storage root with SQLite-backed state, end-to-end bash compression accounting visible in the status UI, and seven new tree-sitter grammars. The CortexKit migration runs once on first launch and is a one-way move; expect a brief "AFT is migrating its data" message during that first start.
New storage root: CortexKit
All AFT persistent state has moved out of the per-harness opencode/storage/plugin/aft/ and pi/storage/plugin/aft/ directories into a single shared root at ~/.local/share/cortexkit/aft/ (or %APPDATA%\cortexkit\aft\ on Windows). Search indexes, semantic indexes, symbol caches, backups, ONNX Runtime, bash task spill files, and the RPC port directory are all unified under one path that both @cortexkit/aft-opencode and @cortexkit/aft-pi share.
On first launch, the plugin spawns a blocking aft migrate-storage step that moves and merges your existing legacy data into the new layout. The migration is content-hash safe (no data is duplicated), idempotent (re-running is a no-op), and shows a brief "AFT is migrating its data" notice in OpenCode while it runs. Typical migration finishes in seconds; large semantic indexes can take up to a minute.
SQLite-backed bash tasks, backups, and state
Bash task records, aft_safety backup history, the warned_tools notification dedupe, and migration markers now live in cortexkit/aft/db/aft.sqlite alongside the existing JSON/disk files (dual-write during v0.27 for safety; DB becomes the source of truth in a future release). The schema is versioned and migrations run automatically on configure.
Practical effect: bash_status lookups now work across bridge restarts, session changes, and concurrent project layouts that the JSON-only paths could miss.
Bash output compression now accounted
The bash compression pipeline that's been compressing tool output since v0.22 was completely invisible — there was no way to see whether it was actually saving tokens or how much. v0.27 adds:
-
A new SQLite
compression_eventstable that records original vs compressed token counts per terminal bash task, keyed on harness + session + task_id (idempotent insert). -
Aggregates surfaced in the
statusresponse and rendered in OpenCode sidebar,/aft-statusdialog, and Pi status overlay:Compression Session Tokens Saved 6,419 Compression Ratio 19% Project Tokens Saved 7,026 Compression Ratio 20% -
Tokenization uses a Claude-compatible BPE tokenizer ported from
ai-tokenizer(newaft-tokenizercrate, ~7ms per 128KiB output, linear scaling). -
Foreground bash with
notify_on_completion=false(the OpenCode/Pibashtool default) now records compression events — previously this path skipped the database write entirely, so >99% of real-world bash usage contributed zero to the aggregate. This was the single biggest gap in the compression telemetry. -
Large outputs that exceed the in-memory cap now tokenize the most recent tail bytes instead of being silently skipped, so build logs, test runs, and other high-volume bash tasks contribute their full reduction count.
Seven new tree-sitter grammars
aft_outline, aft_zoom, and ast_grep_search/ast_grep_replace now work on Java, Ruby, Kotlin, Swift, PHP, Lua, and Perl. Total grammar count: 23. Each grammar ships a hand-written symbol query verified against the installed crate's actual node-type names, an extract function that handles classes/interfaces/methods/functions/fields with proper scope chains, and integration coverage for outline behavior.
Filesystem locks
A new fs_lock module provides crash-safe filesystem locks used by the migration runner, cache writers, and concurrent worktree initialization paths. Replaces ad-hoc lock files that could leak across crashes. No agent-visible change; matters if you ran into "lock file already exists" errors after an AFT process was force-killed.
Pi — restrict_to_project_root now respected for external-directory prompts
When restrict_to_project_root: false was set in aft.jsonc, Pi's hoisted write/edit/grep tools were still showing a ui.confirm dialog on every absolute or out-of-project path. The flag's documented intent is "don't gate operations on project membership", so the per-call dialog defeated its purpose. The prompt now fires only when the user opts INTO restriction (restrict_to_project_root: true); the Pi default (false, for parity with Pi's built-in tools) skips the dialog entirely.
v0.26.4
Fixes
OpenCode — bounded session.messages calls
AFT's wake-up and status-cleanup paths previously called client.session.messages() without a query.limit, causing OpenCode to hydrate the entire session into memory. On long sessions (30k+ messages, 100k+ parts) this could exhaust host memory. Both call sites now request only the recent tail (50 messages).
OpenCode — lazy bridge spawn
AFT no longer spawns an aft bridge for every project loaded in OpenCode Desktop's sidebar at startup. Bridges now spawn lazily on the first tool call against a project, so opening Desktop with many projects in the sidebar no longer multiplies AFT bridge processes for projects you never interact with.
/aft-status and the TUI sidebar show "Waiting for first tool call to populate" until a bridge actually exists.
v0.26.3
Fixes
OpenCode 1.15.5+ compatibility — ctx.ask permission flow
OpenCode 1.15.5 reverts ToolContext.ask() from Effect.Effect<void> back to Promise<void>. AFT's permission layer is updated to match the restored Promise contract, and the bundled effect runtime dependency is removed.
This release restores AFT permission asks (bash, edit, read, grep, glob, AST replace, safety operations) on OpenCode 1.15.5 and newer. Users on 1.15.4 or earlier should stay on v0.26.2.
The @opencode-ai/plugin peer-dependency requirement is now >=1.15.5.
v0.26.2
AFT v0.26.2 makes home-directory project roots usable for migration tasks instead of refusing them outright, and brings a handful of CI and release-pipeline fixes.
Home-directory project roots
Opening OpenCode (or Pi) from $HOME previously threw HomeProjectRootError and the plugin refused to spawn a bridge at all. That blocked legitimate migration tasks — dotfile sweeps, shell config maintenance, machine-setup scripts — that genuinely need to run from ~.
$HOME bridges now spawn in auto-degraded mode:
read,write,edit,bash,aft_outline,aft_zoom,aft_safetyall work normallysearch_indexandsemantic_searchauto-disable (so we don't try to trigram-index~/Library/Caches)aft_navigate(callers, impact, trace_to, trace_data) returnsproject_too_largebecause the project root is over the callgraph file-count cap- The TUI sidebar shows a red
Degradedbadge withhome_rootas the reason /aft-statusreports the same in its dialog and markdown output
Same behavior applies if the canonical project root resolves to $HOME through a symlink chain (Stow-style dotfiles, chrooted containers, etc.), so the previously confusing ~/.dotfiles → /home/user setup is covered too.
The search-index threshold also catches very large project trees more honestly: if the synchronous source-file count exceeds 20,000, search is auto-disabled with a search_too_many_files:20000 reason instead of starting an index build that would never realistically be useful.
Fixes
- Configure no longer times out on
$HOME. Two synchronous walks ran before degraded-mode detection: the source-file count walk (bounded bytake(20_001)but still traversing millions of non-source files first) and the nested-.gitignorediscovery walk (max_depth(8)across~/Library/*). Both now skip when the project root is$HOME, dropping configure time from a 30s timeout to roughly 1-2s. - Linux CI flake in
findBinarySyncresolved. Bun runs test files concurrently in one process andprocess.envis process-shared; broad env mutations in one test could be clobbered by parallel tests between the precondition spawn and the subsequentfindBinarySynccall. The resolver now snapshotsprocess.envonce at entry and uses that snapshot for cache path, PATH lookup, and cargo fallback, so concurrent test env mutations can't slip in.
Release pipeline
Discord release announcements now fire inline from the release workflow's discord-announce job. The previous standalone workflow only ran on manual dispatch because GitHub suppresses release: published events triggered by GITHUB_TOKEN. The manual workflow (discord-release.yml) is preserved as a re-fire override.
Upgrade notes
No config changes required. Existing project bridges keep working unchanged. If you've been working around the $HOME refusal with a placeholder subdirectory, you can drop the workaround.
v0.26.1
v0.26.1
Audit-hardening patch release. 55 fixes across 6 subsystems on top of v0.26.0, plus a callgraph correctness fix for monorepos with nested lockfiles.
Callgraph correctness
aft_navigate callers and aft_navigate impact now resolve cross-package consumers in monorepos correctly, regardless of nested per-package lockfiles. Previously a bun.lock or package-lock.json inside an individual workspace package stopped the upward walk to the real workspace root, leaving some consumers invisible to the call graph.
Workspace resolution received broader improvements too:
- PNPM monorepos are now recognized via
pnpm-workspace.yamlinstead of being silently unsupported. - Glob workspace patterns with negations (
!apps/legacy) and recursive globs (packages/**,apps/*/pkg) now resolve correctly. - TypeScript path aliases (
compilerOptions.paths,baseUrl) resolve before falling back to workspace-package lookup. WORKSPACE_PACKAGE_CACHEis now invalidated on file changes — adding, renaming, or removing a workspace package no longer requires an AFT restart.
Call edge coverage
The callgraph now indexes call edges that were previously missing entirely:
- JSX components (
<Foo />,<Foo.Bar />in TSX) now register as calls to the component. - Constructor calls (
new ClassName()) now register as calls to the class. - Computed string member calls (
obj["method"]()when the key is a literal) now register against the property name. - Re-export alias chains (
export { foo as bar } from "./mod"plusimport { bar }) now follow through to the real symbol, both for the export-side alias and import-side alias. - Default re-exports (
export { default } from "./mod") now resolve to the target file's actual default-export symbol instead of a synthetic ghost. - Same-file
call_treetraversal now descends into local calls instead of dropping them as unresolved leaves. - Source-less export aliases (
export { foo as bar }) recordbaras exported, notfoo. callersandcall_treeprechecks now find non-exported leaf symbols correctly (previously private leaf functions with real callers returnedsymbol_not_found).- Self-call filter no longer drops legitimate external calls that happen to share the enclosing function's short name (e.g.
function add() { return math.add(...) }).
LSP diagnostics honesty + installer security
- Pre-edit snapshot freshness — file-mode push-fallback diagnostics now require version match or epoch advancement past a pre-sync snapshot, instead of a wall-clock test that could accept late publishes for stale file states.
- Unversioned servers no longer count as fresh on epoch advancement alone — AFT now treats their diagnostics as pending until a stronger causal fence resolves.
- Multi-server coverage — directory-mode diagnostics now track coverage per
(server, file)pair. If.tsfiles have both TypeScript LS and Biome registered but only Biome is active, the missing tsserver coverage is reported inunchecked_filesinstead of being hidden behindcomplete: true. - Partial workspace pulls are now reported as
complete: falsewith a dedicatedworkspace_pull_partialstatus instead of being treated as complete. - LSP child cleanup closes the spawn-track gap, adds Linux parent-death tracking (
PR_SET_PDEATHSIG), and adds a Windows console-control handler. LSP wrapper grandchildren no longer leak on bridge SIGKILL or unhandled signals. - GitHub LSP installer rejects tar archives containing hardlink entries before extraction — closing a hardlink escape path that symlink-only validation missed.
- Cached install validation now uses
binarySha256for steady-state TOFU checks (the previous code compared extracted binary hash against archive hash, causing valid caches to fail revalidation and get quarantined).
Safety/backup correctness
Six P0/P1 fixes around aft_safety undo and recursive delete:
- Tampered backup index rejected — disk-loaded backup metadata is now validated against the active project root and rejected if it contains absolute or out-of-root paths. Closes a path-traversal hole that could have turned
aft_safety undointo an arbitrary file overwrite primitive. - Atomic restore now actually atomic — failed mid-batch writes correctly roll back the failing file too (previously only previously-completed writes were restored, leaving the failing file partially written).
- Per-file rollback covers the in-flight file in
ast_grep_replace, globedit_match, andaft_refactor moveto new destinations. - Tombstones for create-only operations —
writeto a new path,edit appendContentto a new path, multi-filetransactioncreates, andaft_refactor moveto a new destination all now record tombstones soaft_safety undocan delete the created file. Previously these operations had no undo history. - Tombstone undo deletes instead of writing empty content — per-file undo on a moved destination now removes the file (and any parent directories the operation created) instead of leaving an empty file.
- Failed-rollback backup cleanup — when an operation rolls back, its op_id-tagged backup is also popped, so the next
aft_safety undotargets the previous successful operation instead of a no-op. aft_moverecords destination tombstone before moving, not after — tombstone capture failure now rolls the move back instead of leaving disk mutated with no undo metadata.- Recursive delete refuses non-regular entries (FIFOs, sockets, device nodes, hard-linked files) explicitly, instead of silently deleting them with no backup.
- Restart-safe latest-op selection —
aft_safety undonow uses a persisted monotonic ordering field instead of(second_timestamp, in-process counter), so restart-within-the-same-second no longer scrambles which operation is "latest." - External-modification warnings now fire for both in-memory and disk-fallback undo paths.
Background bash wake delivery
Three P0s and three P1s in the completion-delivery state machine:
- Drain is now a peek; a new
bash_ack_completionsRPC persistscompletion_delivered=trueonly after the plugin actually appends or wakes. Plugin death in the drain→deliver window no longer loses the completion. - Push-delivered completions are now acked explicitly. Previously the push path never marked tasks delivered, leaving them undelivered on disk forever and uncollectable by GC.
- Replay-orphaned completions now wake the agent — after configure replays terminal tasks, the plugin forces one drain per session so wakes fire even when the original task is no longer tracked.
- Wake retries cap at 5 attempts with exponential backoff capped at 1s, then surface a hard failure. Permanent failures (missing
promptAsync, runtime always throwing) no longer create an infinite retry loop. - Replay-synthesized terminals now insert into the in-memory registry so subsequent ack persistence works correctly.
- Schema-version validation on read — incompatible
bash-tasks/*.jsonfiles are quarantined immediately on replay instead of being silently skipped. - Post-restart long-running reminder suppression — rehydrated running tasks no longer fire a fresh
bash_long_runningreminder on the first watchdog tick after restart. bash_killcross-session lookup mirrorsbash_status— a resumed session can now kill a background task spawned by an earlier session, not just inspect it.
Search/semantic cache reuse
One P0 and four P1s around the v0.24 cross-worktree cache reuse:
- Symbol cache write race fixed — symbol cache persistence now takes a lock parallel to search/semantic caches and uses unique temp filenames. Two main bridges writing the same project no longer race and corrupt
symbols.bin. - Reused search index marks unverified until freshness check completes —
grep/globno longer serve stale state withsuccess: trueimmediately after cold start. Index becomes ready only afterverify_file_mtimesconfirms freshness. - HEAD-change refresh runs filesystem freshness after the
git difffast path, using--name-status -Mto catch renames and pick up untracked + locally-edited files. - Hybrid search gates lexical fusion on
index.ready—aft_searchno longer adds stale lexical boosts while the search index is rebuilding. - Semantic watcher marks edited files stale instead of silently dropping them — semantic search now reports stale-or-rebuilding status when invalidated files are still being re-embedded, instead of returning stale embeddings as ready.
- Cache path validation on read and write — all three caches (search, semantic, symbol) now reject absolute paths and paths containing
..at both serialization and load boundaries.
Plugin transport + aft-bridge
Four P1s and five P2s in the bridge transport layer:
- Transparent retry on version mismatch fixed — host plugins hitting a version mismatch on first call now correctly retry against the fresh bridge instead of failing with "Bridge replaced during version check." This was the highest-value bug — it affected every first user call after AFT auto-upgraded the binary.
- Cached binary probing — versioned-cache resolution now verifies the cached binary's
--versionoutput matches the directory tag before returning. Corrupted, wrong-arch, or mislabeled binaries fall through to the next candidate. - Null-version npm binaries fall through — Gatekeeper-killed unsigned binaries on macOS no longer cause the resolver to return an unusable path.
- Download safeguards —
downloadBinary()now has request timeout, advertised-size check, streaming byte cap, and incremental hashing, matching the patterns already used byonnx-runtime.ts. Stalled networks no longer hang plugin startup indefinitely. - Concurrent upgrade dedup — version-mismatch upgrades coordi...
v0.26.0
v0.26.0
Post-audit hardening release. 32 fixes from 13 parallel audit lanes plus 3 follow-up dogfood-bug fixes, all verified live. No new public surface — every change is a correctness, honesty, or robustness improvement on top of v0.25.2.
Highlights
- Multi-file undo now works.
aft_safety undois one operation: deleting["a","b"]and undoing restores both.aft_moveundo removes the destination AND restores the source (new backup tombstone API).move_symbolandast_replaceare now operation-scoped too. Symlinks are rejected before mutation in single-file delete (directory delete already had this guardrail). aft_navigate callersresolves workspace package imports.import { foo } from "@your-pkg/bar"now correctly maps to source files in monorepo siblings, including whenpackage.jsonmainpoints atdist/but the source lives insrc/. Top-level call sites (e.g. insidedescribe()/test()blocks) are now indexed.bashfindrewrite no longer drops the path.find /tmp/foo -name "*.ts"now correctly passes the absolute path through to glob instead of embedding it in the pattern.- Tri-state response contract enforced end-to-end.
readreports realtotal_linesand returnscomplete: falseon partial reads. The edit family omitssyntax_validwhen validation didn't run instead of falsely returningtrue.inline_symbolcorrectly matches multiline calls by start-line.lsp_diagnosticsdirectory mode reports partial workspace pulls honestly. - Bash background tasks survive restart by default. Replay now runs with the inferred storage_dir, so
bash background:truecompletions are delivered after an OpenCode restart even without explicitstorage_dirconfig. Detached PID liveness recovery handles externally-killed children. aft doctoris now read-only. Plainaft doctorruns inspection without mutating config or running install commands. Useaft doctor --fixfor the previous auto-remediate behavior. ONNX is only flagged as a problem whensemantic_searchis enabled. Issue title sanitization, JSONC comment preservation, and streaming log tail are in.- Out-of-project navigate paths return an honest error. Calling
aft_navigateon a path outsideproject_rootnow returnspath_outside_project_rootwith a clear message instead of misleadingly reporting 0 results.
Detailed changes
Safety and undo
- Operation-scoped backup IDs for multi-file
aft_delete,aft_move,move_symbol,ast_replace - Backup tombstone API for
aft_move(undo removes destination + restores source atomically) delete_filerejects symlinks before mutation- Session marker handling: markerless session dirs are skipped instead of being collapsed into
__default__ - Backup paths resolve against
project_rootconsistently regardless of process CWD storage_dirreset cleans stale checkpoint directories
Navigate / callgraph
- Workspace package imports (
@org/pkg) resolve to monorepo siblings main: "dist/..."falls through tosrc/...when source exists alongside compiled output- Top-level call sites (e.g. inside
describe/testblocks) indexed as<top-level>callers callers,impact,trace_to,trace_datareject out-of-project paths withpath_outside_project_root
Edit / write / read honesty
readreturns real file length intotal_lines(continues scanning past requested range)- Partial reads return
complete: falseinstead of falsely claiming complete - Batch / edit_match / edit_symbol / extract / inline omit
syntax_validwhen validation didn't run inline_symbolmatches multiline calls by start-lineapply_patchall-failed path throws (UI shows error state) instead of returning misleading success
LSP
- Watched-files dynamic registration via
client/registerCapability(LSP 3.17 protocol-correct) workspace/diagnostichonors caller timeout with$/cancelRequest- Centralized Windows URI helper handles
\\?\,\\?\UNC\server\share, and drive paths consistently across manager / position / client - Directory mode reports
WorkspaceDiagnosticReportResult::Partialascomplete: false
Compression
toml_filter[shortcircuit]regex no longer multi-line by default (previously,when = "^\\s*$"could match any blank line and collapse real output tomake: ok)compress_tscpreserves top-level errors likeTS18003: No inputs found in config fileinstead of dropping them
Bash
- Background tasks replay on default storage_dir (completions delivered across restart automatically)
- Detached PID liveness check distinguishes externally-killed children from running tasks
findrewrite routes absolute paths through glob'spatharg instead of embedding in pattern
Parser / extract / imports
- Symbol cache invalidates by content_hash on mtime collision (fixes false-cache hits on dev cycles)
- TS
export { foo }andexport default foocorrectly detected as exports - Default imports resolve to the real symbol name + metadata
- Namespace imports (
import * as ns) preserved throughaft_import organize(previously degraded to side-effect import) extractis scope-aware: detects enclosing function correctly (not the firstconst x = ...)extractpreserves nested indentation in the extracted bodyextractemitslet/varat call-site when caller already hadlet/varextractsubstitution is scope-aware: nested callback parameters shadowing the same name aren't renamed
OpenCode plugin parity
aft_bash,bash_status,bash_killregistered withaft_prefix when host bash hoisting is disabledclient.session.getshape matches current SDK- Transaction edit, delete, legacy
aft_editthrow on Rust failure (consistent with the rest of the tool surface) onVersionMismatchmigrated to coordinated-retry callback shape
Pi plugin parity
- LSP auto-install uses npm (not Bun; Pi runs under Node)
- Version mismatch reads stderr (Pi v0.74 emits version to stderr in RPC mode)
- Hot-swap path: replaceBinary returns new path; bridge retries in-flight request
- AST grep / replace schema hints surface server-provided guidance
aft_deletethrows on Rust failure (was silently returningsuccess: false)onVersionMismatchmigrated to coordinated-retry callback shape
CLI / doctor
- Plain
aft doctoris read-only (use--fixfor remediation;--forcealiased for back-compat) - ONNX
compatible: falseonly flagged as problem whensemantic_searchis enabled - Issue title sanitization (strips usernames/paths from
--issuebundle title) - JSONC comment preservation through config rewrites
- Binary version probe before extracting cached archives
- Streaming log tail for
--issuebundle
Security
url-fetchSSRF check runs at both cache-check time AND fetch time (prevents a URL fetched once withallowPrivate=truefrom being readable later withallowPrivate=false)- Version-mismatch handling no longer fire-and-forget; the in-flight request is coordinated with the hot-swap and retried transparently
CI / release
tests.ymlnow triggers on changes toscripts/**and release workflows (previously could merge with no CI run if only those paths changed)- All npm publish jobs idempotent — preflight
npm viewskips already-published versions as success rather than failing the rerun - macOS E2E hard-fails on missing artifacts or silent
npm installfailures (previously masked by hardcoded"0.19.5"fallback) scripts/wait-release.shfails fast ongherrors instead of polling forever
Upgrade
npx --bun @cortexkit/aft@latest doctor
If your plugin or binary is older than 0.26.0, restart OpenCode after upgrade so the new bridge spawns.
v0.25.2
v0.25.2
Patch release fixing a latent binary auto-download bug that has affected anyone whose npm optional-dependencies didn't install — most commonly Windows users hitting bun add's known reliability issues with optional deps.
What was broken
When the resolver fell through to the GitHub Releases auto-download fallback (because the bundled @cortexkit/aft-<platform> package was missing or version-mismatched), it constructed a 404 URL: releases/download/0.25.1/aft-darwin-arm64 — missing the v prefix that GitHub release tags actually use. Users in that path saw repeated:
ERROR [aft-plugin] Failed to download AFT binary: HTTP 404: Not Found
This is almost certainly the same root cause as issue #39, where a Windows user had to manually place files in the binary cache to recover.
Why this stayed hidden
The auto-download path is the last resort in the resolver. Most users get the binary directly from the npm platform package they install alongside @cortexkit/aft-opencode. The hot-swap upgrade path (which prepends v explicitly) also worked correctly, so all our local upgrade testing passed. Only the "platform package didn't install or doesn't match" first-install case was broken.
What changed
downloadBinary(version) and ensureBinary(version) now normalize the tag to a v-prefixed form internally. Both "v0.25.1" and "0.25.1" produce the same correct URL + cache directory. Three regression tests pin this behavior.
If you've been seeing HTTP 404 in $TMPDIR/aft-plugin.log, upgrading to 0.25.2 fixes it.
v0.25.1
v0.25.1
v0.25.0 shipped to npm but failed to publish to crates.io, and its binaries reported themselves as aft 0.24.0. v0.25.1 is the corrected release of that work — the actual release notes follow below. (Technical details on what went wrong are at the bottom.)
New languages, atomic operation undo, and recursive directory delete with first-class safety guardrails. Every change applies to both @cortexkit/aft-opencode and @cortexkit/aft-pi.
JSON and Scala outlines
aft_outline now understands two more languages.
JSON — top-level object keys outline as Variable symbols with their key span as line range. Works on package.json, tsconfig.json, biome.json, lockfiles, RTK filter manifests, anything. Directory-mode outlines no longer fill skipped_files with unsupported_language: *.json entries.
Scala — classes, objects, traits, defs, vals, vars, case classes, and type aliases now outline with accurate kinds and line ranges. Scala 3 enum types outline as Class, and enum-contained methods are correctly scoped (e.g. Color.describe). Named given definitions outline as Variable; anonymous givens are skipped. aft_zoom works on Scala symbols. AST search/replace is not supported for Scala.
One tool call = one undo
aft_safety undo now restores the entire last mutation operation atomically when called without a filePath.
Every mutating tool (aft_delete, ast_grep_replace, apply_patch, aft_refactor move, aft_move, multi-file edit transactions, etc.) now tags every file it touches with a single operation id. aft_safety undo with no arguments looks up the most recent operation and reverses every file in it as one transaction. aft_safety undo with an explicit filePath still does the existing per-file pop — backwards compatible.
The restore path is properly transactional: AFT preflights every file write to memory, creates any missing parent directories, and only commits the in-memory undo history changes after every write succeeds. If a write fails midway (permission denied, ENOSPC, etc.), AFT rolls back any files already written to their pre-restore content, removes any directories it created, leaves the undo history untouched, and returns the original error with a partial_rollback indicator. You can retry without losing history.
The backup store schema bumped v2 → v3 with seamless migration: legacy v2 backups load with op_id: None and remain per-file undo-able (the old behavior). New backups carry op_ids.
Recursive directory delete with safety guardrails
aft_delete files: [...] now accepts directories when called with recursive: true. It walks the tree, backs every file up under a single op_id (see above), then removes the directory. A single aft_safety undo afterward restores the entire directory tree — files, parent dirs, and all — in one call.
Before deleting, AFT validates the tree contains nothing it can't reliably restore. If the tree contains any symlink or any empty directory, the delete is refused with a unsupported_directory_contents error that names the offending paths. The filesystem is untouched in the rejection case. This is a deliberate guardrail — symlinks could resolve outside the tree on restore (writing arbitrary files), and empty dirs aren't currently representable in the backup format. Both cases will be supported in a future release with proper node-type metadata.
Without recursive: true, directory paths return invalid_request with a clear message pointing to the flag.
Stop orphaning LSP child processes
Fixes the long-standing killall biome workaround. AFT now puts each LSP server in its own process group at spawn and SIGKILLs the entire group on shutdown. Previously only the npm shim wrapper PID was killed, leaving the real server (e.g. @biomejs/cli-darwin-arm64 biome lsp-proxy) orphaned to PID 1 and accumulating across restarts.
Applies to all LSP servers that use a wrapper-and-child structure — biome, eslint, prettier, and similar npm-distributed servers. On Windows, the equivalent fix uses taskkill /F /T to kill the entire process tree.
Other
-
RPC status timeout warnings gone — between bridge spawn and the first push-frame transition, the plugin's status cache was empty, so the TUI sidebar's 1.5s poll would fall through to a bridge call that raced the in-flight eager configure and aborted at 5s. AFT now seeds the cache directly from the eager configure response so the first poll always hits warm cache.
-
CI — workflows bumped to
actions/checkout@v5andactions/setup-node@v5, removing Node 20 deprecation warnings.
Why v0.25.1 (technical detail)
The v0.25.0 tag was placed on a commit where Cargo.toml and package.json files still said 0.24.0. The release workflow then built platform binaries from that stale Cargo.toml, so aft --version reported 0.24.0 (because CARGO_PKG_VERSION is baked in at compile time). cargo publish tried to publish agent-file-tools@0.24.0 to crates.io, got "already exists", and a graceful fallback masked the mismatch as success. The npm publish step had its own version-sync that ran from the tag, so the npm packages did go out at 0.25.0. Net result: npm got 0.25.0 binaries that reported themselves as 0.24.0, and crates.io got nothing new.
Fixed for future releases: version-sync.mjs --from-tag now runs in publish-crates and in every build-* job (not just the npm publish step). The crates.io "already exists" fallback now only treats success if Cargo.toml's post-sync version matches the tag.
Workflow architecture also refactored: both tests.yml (PR-time) and release.yml (tag-push) now call a single reusable _unit-suite.yml for unit-level coverage (Linux, macOS, Windows cargo, Windows bash e2e). Removes ~400 lines of duplicated job logic and ensures PR-time and release-time unit jobs can't drift. The reusable workflow takes a strict boolean: PR mode keeps Windows jobs non-blocking (continue-on-error: true); release mode makes ALL four unit jobs gate the publish flow. A half-published v0.25.0 is exactly the state the new strict gate refuses to ship.
v0.24.0
v0.24.0
Focused improvements to how AFT runs alongside parallel work, how it talks to its plugins, and how it reports its own state. Every change in this release applies to both @cortexkit/aft-opencode and @cortexkit/aft-pi. Matters most for users running subagents, multiple worktrees, the TUI sidebar, or Pi v0.74+.
Cross-worktree cache reuse
When you spawn a new git worktree (e.g. for a subagent task) and AFT starts there, it now reuses the main project's on-disk search, semantic, and symbol caches via content-hash freshness checks instead of rebuilding. The 30-50 second CPU spike per worktree start is gone for typical projects.
Worktree bridges are now ephemeral readers: they load the base cache, refresh anything that has changed via Blake3 content hash, and never write back. The main project bridge stays the sole owner of cache state, so concurrent worktrees can't clobber each other.
One-time forced rebuild of all three caches happens the first time you launch v0.24 against an existing project. Expect ~30-60s on first launch as the new format is populated; every launch after that is fast.
Push-driven status updates
/aft-status and the TUI sidebar used to round-trip through the AFT bridge on every poll (~every 1.5s). On a busy bridge — running grep, semantic builds, or watcher invalidation — that poll would queue behind real work and sometimes hit a 5-second timeout, producing misleading "retrying after port refresh" warnings.
AFT now pushes status changes directly to the plugin when configure completes, index builds finish, or LSP servers attach. The plugin caches the snapshot in memory; status calls hit that cache in microseconds without touching the bridge. Updates are debounced by 1 second to coalesce bursts.
Net effect: status is essentially free now, and the spurious RPC timeout warnings stop. Status push frames are also drained on idle (every 250ms), so the TUI sidebar transitions loading → ready automatically as soon as a background index build completes — no more sitting on "loading" until you fire a tool call.
Redesigned /aft-status dialog
Both harnesses get a redesigned dialog inspired by @cortexkit/opencode-magic-context's /ctx-status:
- OpenCode (TUI) — a themed two-column JSX dialog with flex layout, color-coded status tones, and a
cache_roleaccent (main / worktree / not_initialized). Fits cleanly in the standard TUI viewport without scrolling. - OpenCode (Desktop) — unchanged plain-text snapshot via
sendIgnoredMessage. - Pi — a custom overlay component (
ctx.ui.custom(...)) with bordered two-column layout, themed colors, and 1.5s auto-refresh so loading → ready transitions surface live. Replaces the prior single-line input-prompt rendering that was effectively unreadable.
ONNX Runtime race on Pi launch
When Pi launched with semantic search enabled, the eager bridge warm-up spawned ~4ms BEFORE the ONNX Runtime download path was patched onto the pool's configure overrides. The bridge that served the agent therefore had no _ort_dylib_dir, so Rust fell through to a system-path dlopen("libonnxruntime.dylib") that fails on managed installs. Symptom: /aft-status showed semantic_index: failed with ONNX Runtime not found even though the runtime had finished downloading seconds earlier.
OpenCode already awaited the ONNX promise (capped at 60s) before its eager spawn; Pi now mirrors that exact path. Semantic indexes now build cleanly on first launch instead of staying failed until manual restart.
Background bash completion reliability
Fixed a regression where background bash completion notifications could be silently dropped, leaving the agent waiting indefinitely. The wake path bailed early if the bridge was busy with any in-flight call — but that included unrelated status RPC polls and configure work, not just agent tool calls. When a completion arrived during one of those windows, no follow-up trigger fired and the completion sat in a pending queue forever.
The early-return was wrong; the downstream debounce, timer cancellation, and retry mechanisms already handle the original concern correctly. Wakes are now always scheduled when a completion arrives, regardless of bridge activity. The 200-1000ms debounce window and in-turn drain cancellation guard still prevent duplicate or empty notifications.
Symmetric fix in OpenCode (promptAsync wake path) and Pi (sendUserMessage with deliverAs: "steer"). If you experienced "main agent stuck waiting for background bash" symptoms in v0.23.x, this fixes the root cause for both harnesses.
Pi v0.74 doctor parity (#37)
Pi v0.74 changed where it stores installed extensions and how pi --version writes output, breaking bunx --bun @cortexkit/aft doctor for Pi v0.74+ users. The visible symptom was Plugin registered: false reported even when AFT was correctly installed, plus +0/-0 edit counts in diagnostics. Three fixes:
- Plugin detection now reads
~/.pi/agent/settings.json(new v0.74 location) and falls back to the legacyextensions.jsonfor older Pi installs. Handles all four package-source forms —npm:<spec>,file:<path>, absolute paths, and relative paths against the agent directory. Path entries verify againstpackage.jsonname instead of substring-matching, so look-alikes likeawesome-aft-pi-thiefdon't trigger a false positive. - Host version detection now reads from both stdout and stderr (Pi v0.74 redirects stdout in non-interactive mode) and tolerates startup banners pre-empting the version line.
- Doctor output labels renamed
CLI/BinarytoAFT CLI/AFT binaryto remove ambiguity with Pi's own versions.Host versionis now on its own line so "unknown" is explicit instead of silently omitted.