Releases: cortexkit/aft
v0.26.1
v0.26.1
Audit-hardening patch release. 55 fixes across 6 subsystems on top of v0.26.0, plus a callgraph correctness fix for monorepos with nested lockfiles.
Callgraph correctness
aft_navigate callers and aft_navigate impact now resolve cross-package consumers in monorepos correctly, regardless of nested per-package lockfiles. Previously a bun.lock or package-lock.json inside an individual workspace package stopped the upward walk to the real workspace root, leaving some consumers invisible to the call graph.
Workspace resolution received broader improvements too:
- PNPM monorepos are now recognized via
pnpm-workspace.yamlinstead of being silently unsupported. - Glob workspace patterns with negations (
!apps/legacy) and recursive globs (packages/**,apps/*/pkg) now resolve correctly. - TypeScript path aliases (
compilerOptions.paths,baseUrl) resolve before falling back to workspace-package lookup. WORKSPACE_PACKAGE_CACHEis now invalidated on file changes — adding, renaming, or removing a workspace package no longer requires an AFT restart.
Call edge coverage
The callgraph now indexes call edges that were previously missing entirely:
- JSX components (
<Foo />,<Foo.Bar />in TSX) now register as calls to the component. - Constructor calls (
new ClassName()) now register as calls to the class. - Computed string member calls (
obj["method"]()when the key is a literal) now register against the property name. - Re-export alias chains (
export { foo as bar } from "./mod"plusimport { bar }) now follow through to the real symbol, both for the export-side alias and import-side alias. - Default re-exports (
export { default } from "./mod") now resolve to the target file's actual default-export symbol instead of a synthetic ghost. - Same-file
call_treetraversal now descends into local calls instead of dropping them as unresolved leaves. - Source-less export aliases (
export { foo as bar }) recordbaras exported, notfoo. callersandcall_treeprechecks now find non-exported leaf symbols correctly (previously private leaf functions with real callers returnedsymbol_not_found).- Self-call filter no longer drops legitimate external calls that happen to share the enclosing function's short name (e.g.
function add() { return math.add(...) }).
LSP diagnostics honesty + installer security
- Pre-edit snapshot freshness — file-mode push-fallback diagnostics now require version match or epoch advancement past a pre-sync snapshot, instead of a wall-clock test that could accept late publishes for stale file states.
- Unversioned servers no longer count as fresh on epoch advancement alone — AFT now treats their diagnostics as pending until a stronger causal fence resolves.
- Multi-server coverage — directory-mode diagnostics now track coverage per
(server, file)pair. If.tsfiles have both TypeScript LS and Biome registered but only Biome is active, the missing tsserver coverage is reported inunchecked_filesinstead of being hidden behindcomplete: true. - Partial workspace pulls are now reported as
complete: falsewith a dedicatedworkspace_pull_partialstatus instead of being treated as complete. - LSP child cleanup closes the spawn-track gap, adds Linux parent-death tracking (
PR_SET_PDEATHSIG), and adds a Windows console-control handler. LSP wrapper grandchildren no longer leak on bridge SIGKILL or unhandled signals. - GitHub LSP installer rejects tar archives containing hardlink entries before extraction — closing a hardlink escape path that symlink-only validation missed.
- Cached install validation now uses
binarySha256for steady-state TOFU checks (the previous code compared extracted binary hash against archive hash, causing valid caches to fail revalidation and get quarantined).
Safety/backup correctness
Six P0/P1 fixes around aft_safety undo and recursive delete:
- Tampered backup index rejected — disk-loaded backup metadata is now validated against the active project root and rejected if it contains absolute or out-of-root paths. Closes a path-traversal hole that could have turned
aft_safety undointo an arbitrary file overwrite primitive. - Atomic restore now actually atomic — failed mid-batch writes correctly roll back the failing file too (previously only previously-completed writes were restored, leaving the failing file partially written).
- Per-file rollback covers the in-flight file in
ast_grep_replace, globedit_match, andaft_refactor moveto new destinations. - Tombstones for create-only operations —
writeto a new path,edit appendContentto a new path, multi-filetransactioncreates, andaft_refactor moveto a new destination all now record tombstones soaft_safety undocan delete the created file. Previously these operations had no undo history. - Tombstone undo deletes instead of writing empty content — per-file undo on a moved destination now removes the file (and any parent directories the operation created) instead of leaving an empty file.
- Failed-rollback backup cleanup — when an operation rolls back, its op_id-tagged backup is also popped, so the next
aft_safety undotargets the previous successful operation instead of a no-op. aft_moverecords destination tombstone before moving, not after — tombstone capture failure now rolls the move back instead of leaving disk mutated with no undo metadata.- Recursive delete refuses non-regular entries (FIFOs, sockets, device nodes, hard-linked files) explicitly, instead of silently deleting them with no backup.
- Restart-safe latest-op selection —
aft_safety undonow uses a persisted monotonic ordering field instead of(second_timestamp, in-process counter), so restart-within-the-same-second no longer scrambles which operation is "latest." - External-modification warnings now fire for both in-memory and disk-fallback undo paths.
Background bash wake delivery
Three P0s and three P1s in the completion-delivery state machine:
- Drain is now a peek; a new
bash_ack_completionsRPC persistscompletion_delivered=trueonly after the plugin actually appends or wakes. Plugin death in the drain→deliver window no longer loses the completion. - Push-delivered completions are now acked explicitly. Previously the push path never marked tasks delivered, leaving them undelivered on disk forever and uncollectable by GC.
- Replay-orphaned completions now wake the agent — after configure replays terminal tasks, the plugin forces one drain per session so wakes fire even when the original task is no longer tracked.
- Wake retries cap at 5 attempts with exponential backoff capped at 1s, then surface a hard failure. Permanent failures (missing
promptAsync, runtime always throwing) no longer create an infinite retry loop. - Replay-synthesized terminals now insert into the in-memory registry so subsequent ack persistence works correctly.
- Schema-version validation on read — incompatible
bash-tasks/*.jsonfiles are quarantined immediately on replay instead of being silently skipped. - Post-restart long-running reminder suppression — rehydrated running tasks no longer fire a fresh
bash_long_runningreminder on the first watchdog tick after restart. bash_killcross-session lookup mirrorsbash_status— a resumed session can now kill a background task spawned by an earlier session, not just inspect it.
Search/semantic cache reuse
One P0 and four P1s around the v0.24 cross-worktree cache reuse:
- Symbol cache write race fixed — symbol cache persistence now takes a lock parallel to search/semantic caches and uses unique temp filenames. Two main bridges writing the same project no longer race and corrupt
symbols.bin. - Reused search index marks unverified until freshness check completes —
grep/globno longer serve stale state withsuccess: trueimmediately after cold start. Index becomes ready only afterverify_file_mtimesconfirms freshness. - HEAD-change refresh runs filesystem freshness after the
git difffast path, using--name-status -Mto catch renames and pick up untracked + locally-edited files. - Hybrid search gates lexical fusion on
index.ready—aft_searchno longer adds stale lexical boosts while the search index is rebuilding. - Semantic watcher marks edited files stale instead of silently dropping them — semantic search now reports stale-or-rebuilding status when invalidated files are still being re-embedded, instead of returning stale embeddings as ready.
- Cache path validation on read and write — all three caches (search, semantic, symbol) now reject absolute paths and paths containing
..at both serialization and load boundaries.
Plugin transport + aft-bridge
Four P1s and five P2s in the bridge transport layer:
- Transparent retry on version mismatch fixed — host plugins hitting a version mismatch on first call now correctly retry against the fresh bridge instead of failing with "Bridge replaced during version check." This was the highest-value bug — it affected every first user call after AFT auto-upgraded the binary.
- Cached binary probing — versioned-cache resolution now verifies the cached binary's
--versionoutput matches the directory tag before returning. Corrupted, wrong-arch, or mislabeled binaries fall through to the next candidate. - Null-version npm binaries fall through — Gatekeeper-killed unsigned binaries on macOS no longer cause the resolver to return an unusable path.
- Download safeguards —
downloadBinary()now has request timeout, advertised-size check, streaming byte cap, and incremental hashing, matching the patterns already used byonnx-runtime.ts. Stalled networks no longer hang plugin startup indefinitely. - Concurrent upgrade dedup — version-mismatch upgrades coordi...
v0.26.0
v0.26.0
Post-audit hardening release. 32 fixes from 13 parallel audit lanes plus 3 follow-up dogfood-bug fixes, all verified live. No new public surface — every change is a correctness, honesty, or robustness improvement on top of v0.25.2.
Highlights
- Multi-file undo now works.
aft_safety undois one operation: deleting["a","b"]and undoing restores both.aft_moveundo removes the destination AND restores the source (new backup tombstone API).move_symbolandast_replaceare now operation-scoped too. Symlinks are rejected before mutation in single-file delete (directory delete already had this guardrail). aft_navigate callersresolves workspace package imports.import { foo } from "@your-pkg/bar"now correctly maps to source files in monorepo siblings, including whenpackage.jsonmainpoints atdist/but the source lives insrc/. Top-level call sites (e.g. insidedescribe()/test()blocks) are now indexed.bashfindrewrite no longer drops the path.find /tmp/foo -name "*.ts"now correctly passes the absolute path through to glob instead of embedding it in the pattern.- Tri-state response contract enforced end-to-end.
readreports realtotal_linesand returnscomplete: falseon partial reads. The edit family omitssyntax_validwhen validation didn't run instead of falsely returningtrue.inline_symbolcorrectly matches multiline calls by start-line.lsp_diagnosticsdirectory mode reports partial workspace pulls honestly. - Bash background tasks survive restart by default. Replay now runs with the inferred storage_dir, so
bash background:truecompletions are delivered after an OpenCode restart even without explicitstorage_dirconfig. Detached PID liveness recovery handles externally-killed children. aft doctoris now read-only. Plainaft doctorruns inspection without mutating config or running install commands. Useaft doctor --fixfor the previous auto-remediate behavior. ONNX is only flagged as a problem whensemantic_searchis enabled. Issue title sanitization, JSONC comment preservation, and streaming log tail are in.- Out-of-project navigate paths return an honest error. Calling
aft_navigateon a path outsideproject_rootnow returnspath_outside_project_rootwith a clear message instead of misleadingly reporting 0 results.
Detailed changes
Safety and undo
- Operation-scoped backup IDs for multi-file
aft_delete,aft_move,move_symbol,ast_replace - Backup tombstone API for
aft_move(undo removes destination + restores source atomically) delete_filerejects symlinks before mutation- Session marker handling: markerless session dirs are skipped instead of being collapsed into
__default__ - Backup paths resolve against
project_rootconsistently regardless of process CWD storage_dirreset cleans stale checkpoint directories
Navigate / callgraph
- Workspace package imports (
@org/pkg) resolve to monorepo siblings main: "dist/..."falls through tosrc/...when source exists alongside compiled output- Top-level call sites (e.g. inside
describe/testblocks) indexed as<top-level>callers callers,impact,trace_to,trace_datareject out-of-project paths withpath_outside_project_root
Edit / write / read honesty
readreturns real file length intotal_lines(continues scanning past requested range)- Partial reads return
complete: falseinstead of falsely claiming complete - Batch / edit_match / edit_symbol / extract / inline omit
syntax_validwhen validation didn't run inline_symbolmatches multiline calls by start-lineapply_patchall-failed path throws (UI shows error state) instead of returning misleading success
LSP
- Watched-files dynamic registration via
client/registerCapability(LSP 3.17 protocol-correct) workspace/diagnostichonors caller timeout with$/cancelRequest- Centralized Windows URI helper handles
\\?\,\\?\UNC\server\share, and drive paths consistently across manager / position / client - Directory mode reports
WorkspaceDiagnosticReportResult::Partialascomplete: false
Compression
toml_filter[shortcircuit]regex no longer multi-line by default (previously,when = "^\\s*$"could match any blank line and collapse real output tomake: ok)compress_tscpreserves top-level errors likeTS18003: No inputs found in config fileinstead of dropping them
Bash
- Background tasks replay on default storage_dir (completions delivered across restart automatically)
- Detached PID liveness check distinguishes externally-killed children from running tasks
findrewrite routes absolute paths through glob'spatharg instead of embedding in pattern
Parser / extract / imports
- Symbol cache invalidates by content_hash on mtime collision (fixes false-cache hits on dev cycles)
- TS
export { foo }andexport default foocorrectly detected as exports - Default imports resolve to the real symbol name + metadata
- Namespace imports (
import * as ns) preserved throughaft_import organize(previously degraded to side-effect import) extractis scope-aware: detects enclosing function correctly (not the firstconst x = ...)extractpreserves nested indentation in the extracted bodyextractemitslet/varat call-site when caller already hadlet/varextractsubstitution is scope-aware: nested callback parameters shadowing the same name aren't renamed
OpenCode plugin parity
aft_bash,bash_status,bash_killregistered withaft_prefix when host bash hoisting is disabledclient.session.getshape matches current SDK- Transaction edit, delete, legacy
aft_editthrow on Rust failure (consistent with the rest of the tool surface) onVersionMismatchmigrated to coordinated-retry callback shape
Pi plugin parity
- LSP auto-install uses npm (not Bun; Pi runs under Node)
- Version mismatch reads stderr (Pi v0.74 emits version to stderr in RPC mode)
- Hot-swap path: replaceBinary returns new path; bridge retries in-flight request
- AST grep / replace schema hints surface server-provided guidance
aft_deletethrows on Rust failure (was silently returningsuccess: false)onVersionMismatchmigrated to coordinated-retry callback shape
CLI / doctor
- Plain
aft doctoris read-only (use--fixfor remediation;--forcealiased for back-compat) - ONNX
compatible: falseonly flagged as problem whensemantic_searchis enabled - Issue title sanitization (strips usernames/paths from
--issuebundle title) - JSONC comment preservation through config rewrites
- Binary version probe before extracting cached archives
- Streaming log tail for
--issuebundle
Security
url-fetchSSRF check runs at both cache-check time AND fetch time (prevents a URL fetched once withallowPrivate=truefrom being readable later withallowPrivate=false)- Version-mismatch handling no longer fire-and-forget; the in-flight request is coordinated with the hot-swap and retried transparently
CI / release
tests.ymlnow triggers on changes toscripts/**and release workflows (previously could merge with no CI run if only those paths changed)- All npm publish jobs idempotent — preflight
npm viewskips already-published versions as success rather than failing the rerun - macOS E2E hard-fails on missing artifacts or silent
npm installfailures (previously masked by hardcoded"0.19.5"fallback) scripts/wait-release.shfails fast ongherrors instead of polling forever
Upgrade
npx --bun @cortexkit/aft@latest doctor
If your plugin or binary is older than 0.26.0, restart OpenCode after upgrade so the new bridge spawns.
v0.25.2
v0.25.2
Patch release fixing a latent binary auto-download bug that has affected anyone whose npm optional-dependencies didn't install — most commonly Windows users hitting bun add's known reliability issues with optional deps.
What was broken
When the resolver fell through to the GitHub Releases auto-download fallback (because the bundled @cortexkit/aft-<platform> package was missing or version-mismatched), it constructed a 404 URL: releases/download/0.25.1/aft-darwin-arm64 — missing the v prefix that GitHub release tags actually use. Users in that path saw repeated:
ERROR [aft-plugin] Failed to download AFT binary: HTTP 404: Not Found
This is almost certainly the same root cause as issue #39, where a Windows user had to manually place files in the binary cache to recover.
Why this stayed hidden
The auto-download path is the last resort in the resolver. Most users get the binary directly from the npm platform package they install alongside @cortexkit/aft-opencode. The hot-swap upgrade path (which prepends v explicitly) also worked correctly, so all our local upgrade testing passed. Only the "platform package didn't install or doesn't match" first-install case was broken.
What changed
downloadBinary(version) and ensureBinary(version) now normalize the tag to a v-prefixed form internally. Both "v0.25.1" and "0.25.1" produce the same correct URL + cache directory. Three regression tests pin this behavior.
If you've been seeing HTTP 404 in $TMPDIR/aft-plugin.log, upgrading to 0.25.2 fixes it.
v0.25.1
v0.25.1
v0.25.0 shipped to npm but failed to publish to crates.io, and its binaries reported themselves as aft 0.24.0. v0.25.1 is the corrected release of that work — the actual release notes follow below. (Technical details on what went wrong are at the bottom.)
New languages, atomic operation undo, and recursive directory delete with first-class safety guardrails. Every change applies to both @cortexkit/aft-opencode and @cortexkit/aft-pi.
JSON and Scala outlines
aft_outline now understands two more languages.
JSON — top-level object keys outline as Variable symbols with their key span as line range. Works on package.json, tsconfig.json, biome.json, lockfiles, RTK filter manifests, anything. Directory-mode outlines no longer fill skipped_files with unsupported_language: *.json entries.
Scala — classes, objects, traits, defs, vals, vars, case classes, and type aliases now outline with accurate kinds and line ranges. Scala 3 enum types outline as Class, and enum-contained methods are correctly scoped (e.g. Color.describe). Named given definitions outline as Variable; anonymous givens are skipped. aft_zoom works on Scala symbols. AST search/replace is not supported for Scala.
One tool call = one undo
aft_safety undo now restores the entire last mutation operation atomically when called without a filePath.
Every mutating tool (aft_delete, ast_grep_replace, apply_patch, aft_refactor move, aft_move, multi-file edit transactions, etc.) now tags every file it touches with a single operation id. aft_safety undo with no arguments looks up the most recent operation and reverses every file in it as one transaction. aft_safety undo with an explicit filePath still does the existing per-file pop — backwards compatible.
The restore path is properly transactional: AFT preflights every file write to memory, creates any missing parent directories, and only commits the in-memory undo history changes after every write succeeds. If a write fails midway (permission denied, ENOSPC, etc.), AFT rolls back any files already written to their pre-restore content, removes any directories it created, leaves the undo history untouched, and returns the original error with a partial_rollback indicator. You can retry without losing history.
The backup store schema bumped v2 → v3 with seamless migration: legacy v2 backups load with op_id: None and remain per-file undo-able (the old behavior). New backups carry op_ids.
Recursive directory delete with safety guardrails
aft_delete files: [...] now accepts directories when called with recursive: true. It walks the tree, backs every file up under a single op_id (see above), then removes the directory. A single aft_safety undo afterward restores the entire directory tree — files, parent dirs, and all — in one call.
Before deleting, AFT validates the tree contains nothing it can't reliably restore. If the tree contains any symlink or any empty directory, the delete is refused with a unsupported_directory_contents error that names the offending paths. The filesystem is untouched in the rejection case. This is a deliberate guardrail — symlinks could resolve outside the tree on restore (writing arbitrary files), and empty dirs aren't currently representable in the backup format. Both cases will be supported in a future release with proper node-type metadata.
Without recursive: true, directory paths return invalid_request with a clear message pointing to the flag.
Stop orphaning LSP child processes
Fixes the long-standing killall biome workaround. AFT now puts each LSP server in its own process group at spawn and SIGKILLs the entire group on shutdown. Previously only the npm shim wrapper PID was killed, leaving the real server (e.g. @biomejs/cli-darwin-arm64 biome lsp-proxy) orphaned to PID 1 and accumulating across restarts.
Applies to all LSP servers that use a wrapper-and-child structure — biome, eslint, prettier, and similar npm-distributed servers. On Windows, the equivalent fix uses taskkill /F /T to kill the entire process tree.
Other
-
RPC status timeout warnings gone — between bridge spawn and the first push-frame transition, the plugin's status cache was empty, so the TUI sidebar's 1.5s poll would fall through to a bridge call that raced the in-flight eager configure and aborted at 5s. AFT now seeds the cache directly from the eager configure response so the first poll always hits warm cache.
-
CI — workflows bumped to
actions/checkout@v5andactions/setup-node@v5, removing Node 20 deprecation warnings.
Why v0.25.1 (technical detail)
The v0.25.0 tag was placed on a commit where Cargo.toml and package.json files still said 0.24.0. The release workflow then built platform binaries from that stale Cargo.toml, so aft --version reported 0.24.0 (because CARGO_PKG_VERSION is baked in at compile time). cargo publish tried to publish agent-file-tools@0.24.0 to crates.io, got "already exists", and a graceful fallback masked the mismatch as success. The npm publish step had its own version-sync that ran from the tag, so the npm packages did go out at 0.25.0. Net result: npm got 0.25.0 binaries that reported themselves as 0.24.0, and crates.io got nothing new.
Fixed for future releases: version-sync.mjs --from-tag now runs in publish-crates and in every build-* job (not just the npm publish step). The crates.io "already exists" fallback now only treats success if Cargo.toml's post-sync version matches the tag.
Workflow architecture also refactored: both tests.yml (PR-time) and release.yml (tag-push) now call a single reusable _unit-suite.yml for unit-level coverage (Linux, macOS, Windows cargo, Windows bash e2e). Removes ~400 lines of duplicated job logic and ensures PR-time and release-time unit jobs can't drift. The reusable workflow takes a strict boolean: PR mode keeps Windows jobs non-blocking (continue-on-error: true); release mode makes ALL four unit jobs gate the publish flow. A half-published v0.25.0 is exactly the state the new strict gate refuses to ship.
v0.24.0
v0.24.0
Focused improvements to how AFT runs alongside parallel work, how it talks to its plugins, and how it reports its own state. Every change in this release applies to both @cortexkit/aft-opencode and @cortexkit/aft-pi. Matters most for users running subagents, multiple worktrees, the TUI sidebar, or Pi v0.74+.
Cross-worktree cache reuse
When you spawn a new git worktree (e.g. for a subagent task) and AFT starts there, it now reuses the main project's on-disk search, semantic, and symbol caches via content-hash freshness checks instead of rebuilding. The 30-50 second CPU spike per worktree start is gone for typical projects.
Worktree bridges are now ephemeral readers: they load the base cache, refresh anything that has changed via Blake3 content hash, and never write back. The main project bridge stays the sole owner of cache state, so concurrent worktrees can't clobber each other.
One-time forced rebuild of all three caches happens the first time you launch v0.24 against an existing project. Expect ~30-60s on first launch as the new format is populated; every launch after that is fast.
Push-driven status updates
/aft-status and the TUI sidebar used to round-trip through the AFT bridge on every poll (~every 1.5s). On a busy bridge — running grep, semantic builds, or watcher invalidation — that poll would queue behind real work and sometimes hit a 5-second timeout, producing misleading "retrying after port refresh" warnings.
AFT now pushes status changes directly to the plugin when configure completes, index builds finish, or LSP servers attach. The plugin caches the snapshot in memory; status calls hit that cache in microseconds without touching the bridge. Updates are debounced by 1 second to coalesce bursts.
Net effect: status is essentially free now, and the spurious RPC timeout warnings stop. Status push frames are also drained on idle (every 250ms), so the TUI sidebar transitions loading → ready automatically as soon as a background index build completes — no more sitting on "loading" until you fire a tool call.
Redesigned /aft-status dialog
Both harnesses get a redesigned dialog inspired by @cortexkit/opencode-magic-context's /ctx-status:
- OpenCode (TUI) — a themed two-column JSX dialog with flex layout, color-coded status tones, and a
cache_roleaccent (main / worktree / not_initialized). Fits cleanly in the standard TUI viewport without scrolling. - OpenCode (Desktop) — unchanged plain-text snapshot via
sendIgnoredMessage. - Pi — a custom overlay component (
ctx.ui.custom(...)) with bordered two-column layout, themed colors, and 1.5s auto-refresh so loading → ready transitions surface live. Replaces the prior single-line input-prompt rendering that was effectively unreadable.
ONNX Runtime race on Pi launch
When Pi launched with semantic search enabled, the eager bridge warm-up spawned ~4ms BEFORE the ONNX Runtime download path was patched onto the pool's configure overrides. The bridge that served the agent therefore had no _ort_dylib_dir, so Rust fell through to a system-path dlopen("libonnxruntime.dylib") that fails on managed installs. Symptom: /aft-status showed semantic_index: failed with ONNX Runtime not found even though the runtime had finished downloading seconds earlier.
OpenCode already awaited the ONNX promise (capped at 60s) before its eager spawn; Pi now mirrors that exact path. Semantic indexes now build cleanly on first launch instead of staying failed until manual restart.
Background bash completion reliability
Fixed a regression where background bash completion notifications could be silently dropped, leaving the agent waiting indefinitely. The wake path bailed early if the bridge was busy with any in-flight call — but that included unrelated status RPC polls and configure work, not just agent tool calls. When a completion arrived during one of those windows, no follow-up trigger fired and the completion sat in a pending queue forever.
The early-return was wrong; the downstream debounce, timer cancellation, and retry mechanisms already handle the original concern correctly. Wakes are now always scheduled when a completion arrives, regardless of bridge activity. The 200-1000ms debounce window and in-turn drain cancellation guard still prevent duplicate or empty notifications.
Symmetric fix in OpenCode (promptAsync wake path) and Pi (sendUserMessage with deliverAs: "steer"). If you experienced "main agent stuck waiting for background bash" symptoms in v0.23.x, this fixes the root cause for both harnesses.
Pi v0.74 doctor parity (#37)
Pi v0.74 changed where it stores installed extensions and how pi --version writes output, breaking bunx --bun @cortexkit/aft doctor for Pi v0.74+ users. The visible symptom was Plugin registered: false reported even when AFT was correctly installed, plus +0/-0 edit counts in diagnostics. Three fixes:
- Plugin detection now reads
~/.pi/agent/settings.json(new v0.74 location) and falls back to the legacyextensions.jsonfor older Pi installs. Handles all four package-source forms —npm:<spec>,file:<path>, absolute paths, and relative paths against the agent directory. Path entries verify againstpackage.jsonname instead of substring-matching, so look-alikes likeawesome-aft-pi-thiefdon't trigger a false positive. - Host version detection now reads from both stdout and stderr (Pi v0.74 redirects stdout in non-interactive mode) and tolerates startup banners pre-empting the version line.
- Doctor output labels renamed
CLI/BinarytoAFT CLI/AFT binaryto remove ambiguity with Pi's own versions.Host versionis now on its own line so "unknown" is explicit instead of silently omitted.
v0.23.0
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.000→0.333, identifier P@5 went from0.600→0.800, overall from0.607→0.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
sourcetag —"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, andglobnow askexternal_directorypermission 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
grepnow asksexternal_directorypermission too. Already present onread/write/edit/etc, butgrephad 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_zoomPi rendering: improved single-symbol display.aft_outlineURL 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
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
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/diagnosticpull responses no longer over-claimcomplete: truewhen only some servers responded. File-mode push-only freshness is now keyed per-file, so a fresh publish fora.tscan't whitewash stale state forb.ts.aft doctor lsp <file>now reports successful inspections correctly instead of printinglsp_inspect failedwhen other server output arrives mid-stream (#34).
LSP auto-install — supply-chain trust + redirect + bomb resistance
- npm and GitHub auto-installs now write
.aft-installedmetadata (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, orlsp.disabled. Those keys are now user-config only. - ZIP extraction on Windows uses direct
tar.exeinstead of shelling out to PowerShell.
Bash subsystem — permissions, shell selection, kill race
bash_permissionsnow scans redirect targets including dynamic ones likeecho > $OUTFILE, so commands likeecho hi > /tmp/foocorrectly ask forexternal_directorypermission instead of silently bypassing it.- POSIX shell resolution honors
$BASH, falls back towhich(bash), then/bin/sh— previously hardcoded. bash_killnow reads the exit marker before settingKillingstatus. 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
.batwrappers (not.ps1) and captures%ERRORLEVEL%correctly. Backgrounded-task previews are reconstructed from disk after replay. - Bash task slug renamed from
bgb-tobash-.
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
.gitignorerules instead of a hardcoded skip list, so build outputs likedist/,node_modules/, framework caches don't trigger constant cache invalidation. Live rebuild on.gitignorechanges. - Watcher path matching canonicalizes paths to handle macOS
/varvs/private/varand broken symlink chains on Linux.
aft-bridge — transport + ONNX install + pool
- NDJSON stream uses
StringDecoderfor safe multi-byte UTF-8 handling. Bridge timeouts reject sibling pending requests with an explicit abort error before killing the process. checkVersion()hard-fails onsuccess: falseor 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.
BridgePoolandBinaryBridgeaccept alogger?: Loggeroption 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. grepandglobnow askexternal_directorypermission 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_statuslookup 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" }forexportdeclarations now includes the leadingexportkeyword in the symbol range. Replacements that themselves containexportno longer produceexport export function foo() {}and get rolled back. - Pi UI for
aft_zoomnow renders the zoom result instead of showingNo zoom result availablefor 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
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):
- Rust modules — hand-written parsers for high-traffic tools. v0.21 adds three new ones (
eslint,vitest/jestsharing a parser,biome) plus six newgitsubcommand compressors (add,commit,push,pull,fetch,stash) on top of the existinggit status/log/diff/blame. JSON output is parsed where the tool offers it. - TOML filters — declarative
strip+truncate+cap+shortcircuitrules. 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). - 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 nextconfigurewithout 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.mdgained a dedicated "Bash Output Compression" section.
Full Changelog: v0.20.1...v0.21.0
v0.20.1
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-Itemunder detached spawn. ReplacedDETACHED_PROCESSwithCREATE_NO_WINDOWfor Win32 process flags. UnderDETACHED_PROCESS, pwsh sometimes exited before completing later script statements (theMove-Itemthat writes the exit marker never ran), leaving the bg task forever markedFailed: process exited without exit marker.CREATE_NO_WINDOWkeeps 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 recognizedfailures.
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