Skip to content

Releases: cortexkit/aft

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.

v0.23.0

15 May 04:31

Choose a tag to compare

v0.23.0

Highlights

aft_search overhaul — better recall, hybrid lexical lane, source provenance

The biggest semantic-search change since the feature was introduced. Three coordinated improvements landed together:

  • Query-shape classifier + per-shape weighting. The query is now classified as identifier-like (HashMap, useState), path-like (src/utils/auth.ts), error-message-shaped, mixed, or natural-language. Each shape gets a tailored treatment instead of one-size-fits-all cosine ranking.

  • File-summary chunks for small files. Files with two or fewer top-level exports now get a synthetic file-summary chunk that embeds the path, exported names, and signatures together. Generic-file queries like "where is the rate limiter" used to return zero results when the answer was a short single-export module; now they surface the file directly. Per the new built-in eval harness on this codebase: generic-file P@5 went from 0.0000.333, identifier P@5 went from 0.6000.800, overall from 0.6070.750.

  • Hybrid lexical lane. A second retrieval lane runs alongside the existing semantic lane and contributes results that exact-token matches the query. Each result now carries a source tag — "semantic" (embedding match only), "lexical" (trigram exact-token match the embedding lane missed), or "hybrid" (both lanes agreed — strongest signal). The lexical lane especially helps for path-shaped queries and error messages where embeddings underperform.

The aft_search tool description was rewritten around concrete "when to use / when not to use" triggers so agents reach for it for the right shapes of question. Score floor was removed (was suppressing valid 0.30-0.45 hits); Markdown/HTML heading-only chunks no longer outrank code chunks for code-flavored queries.

Bumped semantic-index chunking_version to 2. The old V1 cache deserializes with a serde default and lazily backfills file-summary chunks on the first v0.23 run per project — no manual reindex needed. Total chunk count roughly doubles after backfill (file-summary chunks add one synthetic chunk per qualifying file).

Resolver: refuse stale @cortexkit/aft-<platform> packages on version mismatch

A workspace that upgraded the AFT plugin (e.g. v0.19.5 → v0.22.x) while a stale @cortexkit/aft-<platform> was still hoisted in node_modules — common with bun's .bun/install/cache keeping multiple versions — could see the resolver silently pick the older binary instead of the version-matched cached one. The wrong-version binary still passed basic protocol but emitted pre-rename behavior (in the original repro: bgb- task slugs that don't match the plugin's bash- regex, producing tool-result mismatches).

The resolver now invokes --version on the npm-resolved binary before returning it. If the version doesn't match the plugin's expectedVersion, it logs a warning and falls through to PATH lookup so a locally-built or correctly-installed binary can take over. Both plugin entry points now plumb their PLUGIN_VERSION explicitly into findBinary().

Pi v0.74.0 — migrated to @earendil-works package scope

Pi's coding-agent project moved from the @mariozechner/* npm scope to @earendil-works/* as of Pi v0.74.0; the old packages now carry a "please use @earendil-works/pi-coding-agent instead going forward" deprecation notice. AFT's Pi plugin and Pi RPC test harness now declare the new scope directly. Pi v0.74.0 also switched its embedded type-schema runtime from @sinclair/typebox@0.34 to typebox@1.x — AFT's tool definitions migrated alongside it. No agent-visible API changes.

Pi RPC end-to-end test harness

New tests/pi-rpc/ workspace with a JSONL RPC client, aimock-driven mock OpenAI-compatible provider, and a real Pi process spawn helper modeled on the magic-context Pi runner pattern. Sixteen scenarios cover hoisted reads, permission asks, semantic search, foreground bash, background bash with completion notifications, and post-completion drain across Pi restarts. Wired into the reusable E2E workflow so CI now blocks on Pi behavior the same way it blocks on OpenCode.

The harness already paid for itself during this release cycle: it caught the resolver version-mismatch bug above and a real Pi-side bug where drainCompletions bailed entirely when the RPC envelope omitted sessionID (Pi's RPC mode does not always send one). Pi now forwards an empty params object to Rust so the binary uses its __default__ session namespace and the drain still works.

Permission asks: Pi external-directory parity, OpenCode subagent grep parity

  • Pi's hoisted read, write, edit, apply_patch, grep, and glob now ask external_directory permission for paths outside the project root, mirroring OpenCode's behavior. Previously Pi would silently allow reads/writes anywhere on the filesystem when the agent passed an absolute out-of-project path.
  • OpenCode hoisted grep now asks external_directory permission too. Already present on read/write/edit/etc, but grep had been left out — agents could still read sensitive files outside the project via repeated grep calls.

Fixes

apply_patch rolls back surviving files when one hunk fails

Previously, apply_patch would commit successful per-file changes even when a later hunk in the same patch failed. The combined effect was a partial application that left the workspace half-edited. Now: any hunk failure rolls back the entire patch atomically using the existing checkpoint/restore path.

Parser: TypeScript export symbol range no longer leaks across replacements

aft_edit { mode: "symbol", operation: "replace" } for export function foo() {...} now includes the leading export keyword in the symbol range, so a replacement string that itself starts with export no longer produces export export function foo() {} and get rolled back.

aft doctor lsp <file> no longer mistakes push frames for the response

The CLI's NDJSON request matcher was indexing responses by arrival order, so a configure_warnings push frame arriving between request and response would be treated as the lsp_inspect response and the actual response would be discarded. Matcher now keys by request id and skips push frames, fixing #34.

Cleanup of dryRun dead branches in 16 Rust command files

The dryRun removal in v0.22.0 took the parameter off the agent-facing schemas but left dead branches in command handler code. Cleaned up across aft_import, aft_refactor, aft_transform, write, edit, apply_patch, and related batch helpers. ast_grep_replace still supports dryRun (workspace-wide AST replacement legitimately benefits from a preview pass).

Session-id threading in Rust log lines

Per-request Rust log lines now carry the originating [ses_xxx] session prefix when the request supplied one. Maintenance and watcher events that are not session-scoped (file invalidation, symbol cache pre-warm, configure-time setup) intentionally remain untagged. Helps correlate semantic refreshes, checkpoints, and format runs back to the triggering session when debugging.

Smaller things

  • aft_zoom Pi rendering: improved single-symbol display.
  • aft_outline URL fetching: better content-type negotiation (HTML, Markdown, GitHub README API media types).
  • Plugin auto-update checker: reduced log noise on startup; cross-instance dedup honored on disk for plugin developers running multiple OpenCode windows.

Known issues (planned for v0.24)

  • Cache-reuse across worktrees: when the same git repo is checked out at multiple paths (e.g. parallel worker worktrees), each worktree currently builds its own semantic / search / symbol caches even though git root commit is shared. The v0.24 plan (already spec'd) adds content-hash freshness fallback and migrates semantic to relative paths so worktrees can share the same cache.
  • Lock contention when multiple bridges spawn simultaneously for the same project root logs failed to acquire semantic cache lock: timed out. Cosmetic — they all converge to the correct state — but should be cleaned up alongside the cache-reuse work.

Full Changelog: v0.22.1...v0.23.0

v0.22.1

14 May 04:24

Choose a tag to compare

v0.22.1

Patch release. Four fixes since v0.22.0.

Fixes

Semantic search against OpenAI no longer fails with "you must provide a model parameter" (#36)

When semantic.backend: "openai_compatible" pointed at https://api.openai.com/v1, AFT's embedding requests were rejected with HTTP 400 "you must provide a model parameter" even though the configured model was set correctly in aft.json. Root cause: AFT was sending two Content-Type: application/json headers on the wire — once implicitly via reqwest's .json(&body) (which serializes the body and sets the header) and again via an explicit .header("Content-Type", "application/json") call right after. reqwest's .header() calls HeaderMap::append, not replace, so both ended up on the wire. OpenAI's /v1/embeddings parser treats duplicate Content-Type as malformed and rejects the body — including the model field that's actually there.

The fix drops the redundant explicit header from both the OpenAI and Ollama backends. The Ollama branch had the same defect; most Ollama servers tolerate duplicate Content-Type so it never surfaced in user reports, but the fix lands consistently.

A new regression test captures the raw on-wire request and asserts exactly one Content-Type header is sent.

Pi: stop downloading ONNX Runtime when the configured backend doesn't need it

Pi's startup gated the 60-80 MB ONNX Runtime download on config.semantic_search alone, so Pi users with semantic.backend: "openai_compatible" or "ollama" still triggered the download even though the runtime is never loaded for HTTP-based backends. Pi now mirrors OpenCode's gate — ONNX Runtime is only fetched when semantic_search is enabled AND the backend is fastembed.

aft_zoom now accepts the ## / <h2> prefixed form that aft_outline shows

aft_outline emits Markdown and HTML headings with their level prefix:

## Basic usage 32:219
<h2> Features 219:234

Agents naturally copy-paste that prefixed form into the next aft_zoom call. Until now AFT rejected those lookups with symbol '## Basic usage' not found and only accepted the bare text form. aft_zoom now strips the level prefix on the Markdown/HTML resolution path so both Basic usage and ## Basic usage resolve to the same section. Code-symbol resolution is unchanged — Rust attributes like #[derive(Debug)] still match exactly.

Stop polluting Windows builds with unused-warnings

Six items in bash_background/registry.rs, commands/bash.rs, and semantic_index.rs only have call sites on non-Windows targets but weren't gated, so Windows builds emitted unused_imports and dead_code warnings. Added #[cfg(...)] predicates matching the call-site availability so the items only exist where they're reachable. Also corrected a stale doc comment in windows_shell.rs that referenced a spawn_shell_command function which no longer exists (its body was absorbed into bash_background::registry::spawn_detached_child during the v0.20.x foreground-as-polled-background refactor).

v0.22.0

13 May 18:30

Choose a tag to compare

Highlights

LSP correctness, freshness and isolation

  • Post-edit diagnostics now wait for version-proven publishes per server/root key. Stale cached diagnostics from open-time publishes can no longer satisfy a freshness check started after the edit. Workspace-pull diagnostics now respect a wall-clock timeout and cancellation when supported.
  • textDocument/diagnostic pull responses no longer over-claim complete: true when only some servers responded. File-mode push-only freshness is now keyed per-file, so a fresh publish for a.ts can't whitewash stale state for b.ts.
  • aft doctor lsp <file> now reports successful inspections correctly instead of printing lsp_inspect failed when other server output arrives mid-stream (#34).

LSP auto-install — supply-chain trust + redirect + bomb resistance

  • npm and GitHub auto-installs now write .aft-installed metadata (version + sha256) and validate it on every cache hit. Mismatched binaries are quarantined instead of being trusted on path existence alone.
  • npm version pins go through safe-version validation. GitHub asset downloads are constrained to a hostname allowlist and follow no redirects. Extracted archives are size-capped (256 MB download, 1 GiB extracted).
  • Project config can no longer inject lsp.servers, lsp.versions, lsp.auto_install, lsp.grace_days, or lsp.disabled. Those keys are now user-config only.
  • ZIP extraction on Windows uses direct tar.exe instead of shelling out to PowerShell.

Bash subsystem — permissions, shell selection, kill race

  • bash_permissions now scans redirect targets including dynamic ones like echo > $OUTFILE, so commands like echo hi > /tmp/foo correctly ask for external_directory permission instead of silently bypassing it.
  • POSIX shell resolution honors $BASH, falls back to which(bash), then /bin/sh — previously hardcoded.
  • bash_kill now reads the exit marker before setting Killing status. If the child finished cleanly between the kill request and the registry update, the real exit code wins.
  • Failed-spawn bundle cleanup deletes wrapper/marker files instead of leaving them in the background-task directory.
  • Windows bash uses .bat wrappers (not .ps1) and captures %ERRORLEVEL% correctly. Backgrounded-task previews are reconstructed from disk after replay.
  • Bash task slug renamed from bgb- to bash-.

Subagent bash — no more 5-second auto-promotion

OpenCode subagent sessions (spawned worker turns) no longer convert background: true into a background task with no waiting model on the other end. background: true is silently converted to background: false, and foreground bash polls until the command terminates or its timeout fires. Primary sessions keep the existing auto-promotion behavior.

Search, semantic, configure — atomicity + ordering

  • Search-index persistence writes through temp files and atomic rename. Trigram cache no longer drops on partial write.
  • Semantic-index refresh is non-destructive and re-detects newly added files. Stale data no longer leaks into the warm cache after the index is invalidated.
  • File watcher now respects .gitignore rules instead of a hardcoded skip list, so build outputs like dist/, node_modules/, framework caches don't trigger constant cache invalidation. Live rebuild on .gitignore changes.
  • Watcher path matching canonicalizes paths to handle macOS /var vs /private/var and broken symlink chains on Linux.

aft-bridge — transport + ONNX install + pool

  • NDJSON stream uses StringDecoder for safe multi-byte UTF-8 handling. Bridge timeouts reject sibling pending requests with an explicit abort error before killing the process.
  • checkVersion() hard-fails on success: false or missing version instead of being silently swallowed.
  • ONNX install splits cleanup into a pre-lock staging-dir sweep (cleans abandoned attempts by dead PIDs) and a post-lock target verification. Failed copies hard-fail and remove the partial install. Symlinks are recreated after the real files.
  • Bridge pool LRU cleanup skips bridges with pending requests instead of killing in-flight work.
  • BridgePool and BinaryBridge accept a logger?: Logger option for per-instance logger override.

Plugin orchestration

  • Background-bash completion wake-ups now preserve { providerID, modelID, variant } from the last real assistant message so synthetic prompts don't bust the provider's prefix cache.
  • grep and glob now ask external_directory permission for out-of-project paths, with brace-aware include splitting.
  • Windows path normalization matches OpenCode's native handling so AFT-submitted patterns work with granular ~/projects/personal/**-style permission rules.
  • bash_status lookup falls back to disk when the in-memory registry has been cleared by a bridge restart. Persisted task GC deletes delivered-terminal tasks and quarantines corrupt JSON.

Formatter timeout — honor it for shell-launched hanging tools

When a configured formatter hung (deadlocked linter, stuck network probe, etc.), formatter_timeout_secs could silently turn into the natural exit time of the underlying process. The timeout path killed only the immediate child; orphaned grandchildren kept the stdout/stderr pipes open, and the wait blocked until they exited on their own. On Unix, the child now spawns in its own session and the timeout path kills the entire process group, so formatter_timeout_secs is enforced as advertised.

Code-symbol editing

  • TypeScript / JavaScript / TSX aft_edit { mode: "symbol", operation: "replace" } for export declarations now includes the leading export keyword in the symbol range. Replacements that themselves contain export no longer produce export export function foo() {} and get rolled back.
  • Pi UI for aft_zoom now renders the zoom result instead of showing No zoom result available for single-symbol calls.

Removed dryRun from mutation tools (kept on ast_grep_replace)

aft_import, aft_refactor, aft_transform, write, edit, apply_patch no longer accept dryRun: true. Use aft_safety checkpoint and aft_safety undo for rollback. ast_grep_replace keeps dryRun because workspace-wide AST replacement genuinely benefits from a preview pass.

Full Changelog: v0.21.0...v0.22.0

v0.21.0

11 May 07:30

Choose a tag to compare

Highlights

Tiered bash output compression with TOML filters + new Rust modules

v0.21 ships the long-pending compression mechanism. Hoisted bash output now flows through a three-tier dispatch (with experimental.bash.compress=true):

  1. Rust modules — hand-written parsers for high-traffic tools. v0.21 adds three new ones (eslint, vitest/jest sharing a parser, biome) plus six new git subcommand compressors (add, commit, push, pull, fetch, stash) on top of the existing git status/log/diff/blame. JSON output is parsed where the tool offers it.
  2. TOML filters — declarative strip + truncate + cap + shortcircuit rules. v0.21 ships 15 builtin filters: make, ls, tree, df, du, find, wc, gradle, xcodebuild, terraform, helm, docker, kubectl, gh, ansible-playbook. Filters can also be added by users under <storage_dir>/filters/*.toml (always loaded) or by projects under <project>/.aft/filters/*.toml (trust-gated).
  3. Generic fallback — ANSI strip + consecutive-line dedup + middle-truncate, always applied when no module or filter matches.

Per-call opt-out via compressed: false on the bash tool — preserves raw output for that specific call while keeping the global default on.

Trust model for project filters. Project-supplied filters are an attack vector (a malicious repo could ship a cargo.toml filter that strips real failures and replaces them with tests: ok). They are off by default. Use the new shared CLI to opt in:

npx --bun @cortexkit/aft doctor filters         # list builtin + user + project filters
npx --bun @cortexkit/aft doctor filters trust   # interactive trust prompt for current project
npx --bun @cortexkit/aft doctor filters --show <name>

Trust state lives in <storage_dir>/trusted-filter-projects.json keyed by canonicalized project root.

Issue #33 — TUI plugin loads on OpenCode 1.14.42-43

api.command.register was removed in OpenCode 1.14.42 and reinstated as a deprecated shim in 1.14.44+. The TUI plugin was crashing on the 1.14.42-43 range with api.command is undefined. Migrated to api.tools.toolDefinition + api.keymap.registerLayer, with a backward-compat fallback when those aren't present. /aft-status and the Ctrl+a, ? keybind now work across <=1.14.41, the broken 1.14.42-43 range, and 1.14.44+.

Issue #32 — grep brace-glob splitting at the Rust boundary

The plugin layer already brace-aware-splits **/*.{ts,tsx},**/*.{js,jsx} correctly, but direct binary callers (bash rewrite, CLI users) hit grep: invalid include/exclude glob: unclosed alternate group because the Rust string_array_param only accepted arrays. Now accepts both strings and arrays, and runs every input through a brace-aware splitter that treats , as a separator only when {/} depth is zero. Same robustness across all caller paths.

Other

  • Compression config (experimental.bash.compress, project filter trust state, storage dir, project root) now re-picks up on the next configure without restart — change a setting and the next bash call honors it.
  • Empty-body [BACKGROUND BASH STILL RUNNING] reminders are gone. A plugin-side race between the in-turn drain and the wake debouncer could fire a reminder shell with no pending tasks attached; both OpenCode and Pi now cancel the debounce timer when the drain absorbs the pending list, and the timer itself short-circuits if there's nothing to report.
  • README compression section was rewritten around the three-tier dispatch with a TOML filter authoring guide. ARCHITECTURE.md gained a dedicated "Bash Output Compression" section.

Full Changelog: v0.20.1...v0.21.0

v0.20.1

09 May 08:25

Choose a tag to compare

Highlights

Foreground bash now works correctly on Windows. In v0.20.0 the new foreground-as-polled-background architecture inadvertently routed model-issued bash commands through cmd.exe even when the model wrote PowerShell-syntax ($var = ..., Start-Sleep, Add-Content), and a separate process-flag bug made PowerShell wrappers silently exit before writing the exit marker. The fix:

  • PowerShell wrappers can now flush stdout/stderr and reach Move-Item under detached spawn. Replaced DETACHED_PROCESS with CREATE_NO_WINDOW for Win32 process flags. Under DETACHED_PROCESS, pwsh sometimes exited before completing later script statements (the Move-Item that writes the exit marker never ran), leaving the bg task forever marked Failed: process exited without exit marker. CREATE_NO_WINDOW keeps the child without a visible console while still giving it a hidden console handle, so PowerShell file I/O completes correctly.
  • Restored the natural shell priority (pwsh → powershell → git-bash → cmd). The v0.18-era cmd-first override was a workaround for the now-fixed PS detached-output bug; it silently misrouted PS-syntax commands through cmd, causing immediate '$marker' is not recognized failures.

The Windows native E2E gate is back to blocking releases (the continue-on-error: true from v0.20.0 is removed). Test (Windows — bash perms), Linux Docker E2E, macOS native E2E, and Windows native E2E all gate publishing now.

Full Changelog:
v0.20.0...v0.20.1