Skip to content

Releases: cortexkit/aft

v0.27.1

21 May 05:11

Choose a tag to compare

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 / write output 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_ty
  • format_on_edit, formatter_timeout_secs, validate_on_edit, formatter / checker per-language
  • restrict_to_project_root, search_index, semantic_search
  • max_callgraph_files, semantic.*, project-safe lsp.* 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 disk line, 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

20 May 18:06

Choose a tag to compare

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_events table that records original vs compressed token counts per terminal bash task, keyed on harness + session + task_id (idempotent insert).

  • Aggregates surfaced in the status response and rendered in OpenCode sidebar, /aft-status dialog, 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 (new aft-tokenizer crate, ~7ms per 128KiB output, linear scaling).

  • Foreground bash with notify_on_completion=false (the OpenCode/Pi bash tool 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

19 May 04:36

Choose a tag to compare

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

19 May 02:54

Choose a tag to compare

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

18 May 03:54

Choose a tag to compare

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_safety all work normally
  • search_index and semantic_search auto-disable (so we don't try to trigram-index ~/Library/Caches)
  • aft_navigate (callers, impact, trace_to, trace_data) returns project_too_large because the project root is over the callgraph file-count cap
  • The TUI sidebar shows a red Degraded badge with home_root as the reason
  • /aft-status reports 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 by take(20_001) but still traversing millions of non-source files first) and the nested-.gitignore discovery 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 findBinarySync resolved. Bun runs test files concurrently in one process and process.env is process-shared; broad env mutations in one test could be clobbered by parallel tests between the precondition spawn and the subsequent findBinarySync call. The resolver now snapshots process.env once 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

17 May 15:36

Choose a tag to compare

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.yaml instead 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_CACHE is 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" plus import { 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_tree traversal now descends into local calls instead of dropping them as unresolved leaves.
  • Source-less export aliases (export { foo as bar }) record bar as exported, not foo.
  • callers and call_tree prechecks now find non-exported leaf symbols correctly (previously private leaf functions with real callers returned symbol_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 .ts files have both TypeScript LS and Biome registered but only Biome is active, the missing tsserver coverage is reported in unchecked_files instead of being hidden behind complete: true.
  • Partial workspace pulls are now reported as complete: false with a dedicated workspace_pull_partial status 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 binarySha256 for 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 undo into 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, glob edit_match, and aft_refactor move to new destinations.
  • Tombstones for create-only operationswrite to a new path, edit appendContent to a new path, multi-file transaction creates, and aft_refactor move to a new destination all now record tombstones so aft_safety undo can 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 undo targets the previous successful operation instead of a no-op.
  • aft_move records 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 selectionaft_safety undo now 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_completions RPC persists completion_delivered=true only 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/*.json files 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_running reminder on the first watchdog tick after restart.
  • bash_kill cross-session lookup mirrors bash_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 completesgrep/glob no longer serve stale state with success: true immediately after cold start. Index becomes ready only after verify_file_mtimes confirms freshness.
  • HEAD-change refresh runs filesystem freshness after the git diff fast path, using --name-status -M to catch renames and pick up untracked + locally-edited files.
  • Hybrid search gates lexical fusion on index.readyaft_search no 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 --version output 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 safeguardsdownloadBinary() now has request timeout, advertised-size check, streaming byte cap, and incremental hashing, matching the patterns already used by onnx-runtime.ts. Stalled networks no longer hang plugin startup indefinitely.
  • Concurrent upgrade dedup — version-mismatch upgrades coordi...
Read more

v0.26.0

17 May 03:42

Choose a tag to compare

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 undo is one operation: deleting ["a","b"] and undoing restores both. aft_move undo removes the destination AND restores the source (new backup tombstone API). move_symbol and ast_replace are now operation-scoped too. Symlinks are rejected before mutation in single-file delete (directory delete already had this guardrail).
  • aft_navigate callers resolves workspace package imports. import { foo } from "@your-pkg/bar" now correctly maps to source files in monorepo siblings, including when package.json main points at dist/ but the source lives in src/. Top-level call sites (e.g. inside describe()/test() blocks) are now indexed.
  • bash find rewrite 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. read reports real total_lines and returns complete: false on partial reads. The edit family omits syntax_valid when validation didn't run instead of falsely returning true. inline_symbol correctly matches multiline calls by start-line. lsp_diagnostics directory mode reports partial workspace pulls honestly.
  • Bash background tasks survive restart by default. Replay now runs with the inferred storage_dir, so bash background:true completions are delivered after an OpenCode restart even without explicit storage_dir config. Detached PID liveness recovery handles externally-killed children.
  • aft doctor is now read-only. Plain aft doctor runs inspection without mutating config or running install commands. Use aft doctor --fix for the previous auto-remediate behavior. ONNX is only flagged as a problem when semantic_search is enabled. Issue title sanitization, JSONC comment preservation, and streaming log tail are in.
  • Out-of-project navigate paths return an honest error. Calling aft_navigate on a path outside project_root now returns path_outside_project_root with 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_file rejects symlinks before mutation
  • Session marker handling: markerless session dirs are skipped instead of being collapsed into __default__
  • Backup paths resolve against project_root consistently regardless of process CWD
  • storage_dir reset cleans stale checkpoint directories

Navigate / callgraph

  • Workspace package imports (@org/pkg) resolve to monorepo siblings
  • main: "dist/..." falls through to src/... when source exists alongside compiled output
  • Top-level call sites (e.g. inside describe/test blocks) indexed as <top-level> callers
  • callers, impact, trace_to, trace_data reject out-of-project paths with path_outside_project_root

Edit / write / read honesty

  • read returns real file length in total_lines (continues scanning past requested range)
  • Partial reads return complete: false instead of falsely claiming complete
  • Batch / edit_match / edit_symbol / extract / inline omit syntax_valid when validation didn't run
  • inline_symbol matches multiline calls by start-line
  • apply_patch all-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/diagnostic honors caller timeout with $/cancelRequest
  • Centralized Windows URI helper handles \\?\, \\?\UNC\server\share, and drive paths consistently across manager / position / client
  • Directory mode reports WorkspaceDiagnosticReportResult::Partial as complete: false

Compression

  • toml_filter [shortcircuit] regex no longer multi-line by default (previously, when = "^\\s*$" could match any blank line and collapse real output to make: ok)
  • compress_tsc preserves top-level errors like TS18003: No inputs found in config file instead 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
  • find rewrite routes absolute paths through glob's path arg 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 } and export default foo correctly detected as exports
  • Default imports resolve to the real symbol name + metadata
  • Namespace imports (import * as ns) preserved through aft_import organize (previously degraded to side-effect import)
  • extract is scope-aware: detects enclosing function correctly (not the first const x = ...)
  • extract preserves nested indentation in the extracted body
  • extract emits let/var at call-site when caller already had let/var
  • extract substitution is scope-aware: nested callback parameters shadowing the same name aren't renamed

OpenCode plugin parity

  • aft_bash, bash_status, bash_kill registered with aft_ prefix when host bash hoisting is disabled
  • client.session.get shape matches current SDK
  • Transaction edit, delete, legacy aft_edit throw on Rust failure (consistent with the rest of the tool surface)
  • onVersionMismatch migrated 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_delete throws on Rust failure (was silently returning success: false)
  • onVersionMismatch migrated to coordinated-retry callback shape

CLI / doctor

  • Plain aft doctor is read-only (use --fix for remediation; --force aliased for back-compat)
  • ONNX compatible: false only flagged as problem when semantic_search is enabled
  • Issue title sanitization (strips usernames/paths from --issue bundle title)
  • JSONC comment preservation through config rewrites
  • Binary version probe before extracting cached archives
  • Streaming log tail for --issue bundle

Security

  • url-fetch SSRF check runs at both cache-check time AND fetch time (prevents a URL fetched once with allowPrivate=true from being readable later with allowPrivate=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.yml now triggers on changes to scripts/** and release workflows (previously could merge with no CI run if only those paths changed)
  • All npm publish jobs idempotent — preflight npm view skips already-published versions as success rather than failing the rerun
  • macOS E2E hard-fails on missing artifacts or silent npm install failures (previously masked by hardcoded "0.19.5" fallback)
  • scripts/wait-release.sh fails fast on gh errors 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

16 May 07:56

Choose a tag to compare

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

16 May 00:33

Choose a tag to compare

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@v5 and actions/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

15 May 14:10

Choose a tag to compare

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_role accent (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 legacy extensions.json for 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 against package.json name instead of substring-matching, so look-alikes like awesome-aft-pi-thief don'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 / Binary to AFT CLI / AFT binary to remove ambiguity with Pi's own versions. Host version is now on its own line so "unknown" is explicit instead of silently omitted.