Releases: cortexkit/aft
Release list
v0.19.2
What's new in v0.19.2
🐛 Bug fixes
Foreground bash hang on Windows (issue #26)
AFT's foreground bash was inheriting stdin from the long-running bridge process — and the bridge's stdin is the JSON-RPC protocol pipe from OpenCode. Any child process that tried to read from stdin (PowerShell Read-Host, git/npm credential prompts, package-manager confirmations, etc.) would block forever waiting for input that never came, manifesting as the 65s bridge transport timeout reported in the issue.
Background bash had Stdio::null() since day one — foreground bash didn't. That asymmetry produced the bug.
Fix in crates/aft/src/commands/bash.rs:
- Detach foreground bash's stdin with
Stdio::null()(matches background bash and OpenCode's native bash) - Pass
-NonInteractiveto PowerShell on Windows so prompts fail fast instead of hanging
Closes #26.
Windows ARM64 support (Prism emulation)
Three coordinated changes in @cortexkit/aft-bridge so the AFT plugin works on Windows ARM64 via Microsoft Prism (the built-in x64 emulator):
platform.ts: mapwin32-arm64 → win32-x64so the resolver and downloader pick the x64 binary that runs cleanly under Prism. We don't ship a native ARM64 build.downloader.ts: route through the sharedplatformKey()helper so cache-miss downloads also pick up the new mapping.onnx-runtime.ts: align ONNX Runtime arch with the AFT binary's arch (not Node'sprocess.arch). Node correctly reports arm64 but our x64 aft.exe under Prism panics ondlopenof native ARM64 ONNX. The map keeps ONNX in lockstep with the resolved binary.
Verified live on Windows 11 ARM64: aft.exe loads x64 ONNX Runtime, builds the semantic index, and serves all 16 tools.
🧪 Tests
New Windows E2E harness
Real-bridge end-to-end test for Windows (tests/windows-e2e/). Runs OpenCode + a mock LLM inside a Parallels VM against the just-built aft.exe and plugin dist, exercising the full plugin → bridge → child-process flow that the Linux Docker harness can't cover. Includes a dedicated Scenario 2b regression test for issue #26 — invokes Read-Host and asserts bash returns within its own timeout (no bridge transport hang).
22/22 PASS on Windows 11 ARM64.
Linux Docker E2E fixture fix
The Docker E2E mock-server was using the pre-v0.18 aft_outline({ directory: ... }) API; v0.18.x renamed it to target. The first scripted turn was silently failing, the bridge never spawned, and the harness's lenient exit-code checks masked it. Updated to use target — Docker E2E now goes from 16/17 → 18/18 PASS.
v0.19.1
Highlights
This release is a focused stability and ergonomics pass. Two reliability fixes deserve special attention if you're using experimental.bash.background or semantic_search:
- Background bash completion reminders no longer drop after the first wake. The
wakeFiredThisIdlegate suppressed every completion after the first one in any idle window — empirically about 1 in 5 reminders. Subsequent completions now each fire their own reminder, debounced through the existing coalescer. - Semantic Index no longer reports "failed" when ONNX downloads successfully. A startup race could spawn the eager-warm bridge before ONNX Runtime download finished, leaving that bridge without
ORT_DYLIB_PATH. Eager-warm now waits up to 60s for ONNX resolution before spawning, so semantic search is "ready" on the same plugin start instead of needing a second restart.
Changes
Bridge package (@cortexkit/aft-bridge)
- ONNX Runtime download now runs in the background instead of blocking plugin load. Plugin tools register immediately; semantic search lights up once the runtime resolves.
aft_zoomnow returns plain-text formatted output across both OpenCode and Pi via sharedformatZoomText. No more JSON-escaped newlines and quotes in agent-visible blobs.- URL fetch under Node v24 now correctly handles dual
lookupcallback shape withopts.all: true(Piaft_outline(url:)was failing withERR_INVALID_IP_ADDRESS). Bumped toundiciv8 with surfaced underlying fetch causes. - New
BridgePool.setConfigureOverride()API lets the plugin layer patch configure overrides on already-running bridges.
Plugin behavior
bashis now only hoisted when at least one ofexperimental.bash.rewrite,experimental.bash.compress, orexperimental.bash.backgroundis enabled. With all three off, the host's native bash tool is used unmodified. This avoids surprising users who have AFT installed but haven't opted in to bash hoisting.- Background-bash anti-polling guidance: tool descriptions and reminder text now describe completion delivery without prescriptive "end your turn" copy.
bash_statuspolling responses now repeat the same reminder rather than nudging the agent into a polling loop. - Bash rewrite footer is now terser:
Prefer 'read' tool over bash.instead of the previous verbose explanation. - Watcher invalidation now ignores read-only metadata events (
AccessTime,Permissions,Ownership,Extended). Read-only tools like Biome lint no longer invalidate trigram and symbol caches.
Logging
- Plugin log no longer renders LSP errors with four nested
[aft]tags.slog_*!macros and 14 source files were scrubbed of inlined prefixes; env_logger handles the outer tag exclusively. - LSP spawn failures are now cached per
(server_kind, workspace_root). One bad workspace root no longer logs an error on every edit — failed spawns log once per session.
Repo dogfooding
- Root
tsconfig.jsonplusbun-typesandtypescriptdevDeps soscripts/**/*.tsget full LSP coverage when working on the repo itself. (No user-facing impact.)
Polish
- Discord badge and section-nav link in README pointing at the cortexkit Discord (
https://discord.gg/DSa65w8wuf).
Known issues
- Issue #26 (Windows bash transport timeout at 65s) is not addressed in this release. Windows e2e harness work is still in progress; a fix will ship once we have real Windows reproduction in CI.
Full Changelog: v0.19.0...v0.19.1
v0.19.0
First release after v0.18.4, with substantial improvements across plugin reliability, performance, and tool surface.
Critical fixes
- Background-bash idle wakes preserved provider prefix cache — synthetic prompts now reuse the last assistant's
{providerID, modelID, variant}so cached prefixes survive across turn-end notifications, ignored messages, and configure-warning deliveries.
Performance
- Incremental semantic refresh — only re-embed files whose mtime+size changed instead of rebuilding the entire embedding set on every restart. On real OpenCode sessions, this collapses cold-restart cost from minutes to milliseconds.
- Symbol cache disk persistence — per-project symbol tables now persist under
<storage_dir>/symbols/<project_key>/symbols.bin. ~91% cache hit rate on second restart instead of full re-parse every spawn. - Async configure warnings —
configurereturns immediately. File-walk + language detection + missing-binary detection happen on a background thread and stream back asconfigure_warningspush frames. - Home-directory bridge hang fix — FSEvents watcher attach moved off the configure foreground. Launching OpenCode from
$HOMEno longer triggers a 30s configure timeout that wedges the bridge in a respawn loop. - Bash default timeout 30s — foreground bash now defaults to 30s with a workflow hint that tells the agent to use
bash({ background: true })for longer commands.
Pi: URL support for aft_outline and aft_zoom
Pi now mirrors OpenCode's URL-fetching behavior for remote docs.
aft_outline—targetacceptshttp:///https://URLs. Auto-detected by URL prefix.aft_zoom— new optionalurlparameter (mutually exclusive withfilePath). Multi-symbol zoom routes through the cached fetched copy.- Same DNS-pinned, SSRF-guarded fetcher OpenCode already uses. Cached under
<storageDir>/url_cache/with a 1-day TTL.
// Pi
aft_outline({ target: "https://platform.claude.com/docs/en/build-with-claude/prompt-caching.md" })
aft_zoom({ url: "https://...", symbol: "1-hour cache duration" })Tool surface refinements
aft_outlineunifiedtarget— file path, directory path, URL, or array of paths in one parameter (auto-detected). Replaces the previous mutually-exclusivefilePath/files/directoryshape.aft_deletetakesfiles: string[]— pass{ files: ["a.ts", "b.ts"] }for any number of files including one. Returns honest partial-success reporting when some deletes fail.[cmpaft]compressed-output marker — replaces the verbose(compressed by aft)suffix on bash output compression. Saves tokens on every compressed result the agent sees.- Pi background-bash completion via
steer— completion now reaches the agent between its tool batch and the next LLM call instead of waiting for full turn completion. - Workflow hints in system prompt — AFT plugins now inject token-efficient workflow guidance (
aft_outline→aft_zoomchains, URL fetching, long-running command patterns) into the agent's system prompt. Conditional on the actual configured tool surface.
Bug fixes (audit batch)
configure_warningssession routing — async push frames now carrysession_idend-to-end so multi-session OpenCode delivers warnings to the correct client.- Semantic refresh data preservation — transient embed-backend errors no longer drop existing cache entries or update file mtimes. Old embeddings stay until the next successful extraction.
- Prewarm generation guard — background prewarm threads use generation-guarded
set_project_root/load_from_diskso a stale thread can't repopulate the symbol cache after reconfigure. - Semantic addition detection — refresh now handles all cases (added/changed/deleted) and returns a
RefreshSummary. Previously, newly added files weren't picked up between restarts. aft_outlinedirectory honest signaling —walk_truncated,complete, andskipped_filesfields are preserved end-to-end.apply_patchdiff size gate — raised from 100 KB bytes to 5000 lines so large source files (e.g. 114 KB / 3084 lines) get proper diff rendering in the UI.
Architecture
@cortexkit/aft-bridgeshared package — bridge transport, pool, downloader, resolver, platform mapping, ONNX runtime, and URL fetcher all moved into one shared package. Both@cortexkit/aft-opencodeand@cortexkit/aft-piconsume from it. Single source of truth for transport behavior.- URL fetching consolidated —
fetchUrlToTempFile,cleanupUrlCache, and_isPrivateIpv4are now exported from@cortexkit/aft-bridgeso OpenCode and Pi share one DNS-pinning and SSRF-guarding implementation.
Install
bunx --bun @cortexkit/aft setupIf you've already installed:
bunx --bun @cortexkit/aft doctor— the doctor tool will detect the new version and prompt to clear caches if needed.
Upgrading
OpenCode and Pi plugins both pin to @cortexkit/aft-bridge@0.19.0 exactly. Existing installations pull in the new bridge automatically on next package update; no config changes required.
Full changelog: v0.18.4...v0.19.0
v0.18.4
v0.18.4
Security & correctness audit (29 fixes)
A full-codebase council audit found and fixed 29 issues across Rust and both plugins.
P0 — Security
- DNS-pinning SSRF: URL fetch now pins resolved IPs before following redirects (#1)
- Bash permission-scan parse failure no longer bypasses the permission gate (#2)
- Shared stdout
BufWriterfor background bash frames prevents interleaved output (#4) - Dangerous env vars (
LD_PRELOAD,DYLD_INSERT_LIBRARIES, etc.) blocked from bash rewrites (#5) - Private IPs rejected in semantic search
base_urlat configure time (#25)
P1 — Correctness
- LSP
didOpennotification now uses the validated path afterdelete_file(#6) aft_outlinedirectory mode skips symlinked directories to prevent loops (#7)- Search index cache files now include a CRC32 integrity check (#8)
- Background bash tasks detach cleanly on SIGTERM/SIGINT so completions survive bridge restart (#9)
- Ambiguous
move_symbolnow fails with a clear error instead of silently picking one (#10) appendContent(edit append mode) runs syntax validation after write (#11)- Active bridge lookup scoped by project root, not global (#13)
- OpenCode pool canonicalizes keys with
realpathSyncto avoid duplicate bridges (#14)
P2 — Robustness
- Background bash temp files include task ID for easier debugging (#12)
- Commands report
complete: true/falsehonestly per the tri-state protocol (#16) - Transaction rollback failures use a distinct
rollback_failederror code (#17) greppasses leading-dash patterns torgvia--to prevent flag injection (#18)- Bash rewrite regex patterns capped at 10 KB to prevent ReDoS (#19)
- Glob
edit_matchcheckpoints include the request ID for uniqueness (#20) - Bash permission paths canonicalized before lookup (#22)
- Large file ranged reads no longer rejected — clamps to file length (#23)
- Batched
aft_zoomsurfaces partial failures per symbol (#24) - Background task IDs use OS entropy (
getrandom) — format is nowbgb-<8hex>(#26) - File watcher filters by exact path component, not substring (#28)
bash_statusnow includesstderr_pathfor inspecting background task stderr (#29)- Pi bash hoisting decoupled from read hoisting (#30)
- Pi config migration preserves block comments (#31)
- RPC client retries on stale port file (#32)
Workflow hints — system prompt injection
Both OpenCode and Pi now inject a short ## Prefer AFT tools for token efficiency block into the agent system prompt. Sections are conditional on the registered tool surface:
- Web/URL access —
aft_outline({ url })→aft_zoom({ url, symbol })instead of fetching whole pages - Code exploration —
grep/aft_search→aft_outline→aft_zoominstead of chains ofreadcalls - Relationship questions —
aft_navigate(callers,impact,trace_to,trace_data) instead of grep + read chains (shown only attool_surface: "all") - Long-running commands —
bash({ background: true })+bash_status(shown only whenexperimental.bash.backgroundis enabled)
Always-on, no config toggle needed. Irrelevant sections are omitted automatically.
Bug fixes
- Windows build:
signal_hookgated behindcfg(unix)— Windows x64 binary now builds correctly - Test files missing
/// <reference path="../bun-test.d.ts" />directive fixed
v0.18.3
What's Changed
Bug Fixes
- Background bash status —
timed_outtasks now display correctly; the plugin was checking for"timeout"while Rust serializes as"timed_out" - Pi bash wake messages —
sendUserMessagenow passes{ deliverAs: "followUp" }to prevent "Agent is already processing" rejections mid-turn - HTML
aft_zoomsection content — heading symbols now return the full section body down to the next sibling heading, not just the heading line itself - Pi bash renderer —
renderResultnow shows last 25 lines of output, matching Pi's built-in bash behavior handle_appendsyntax validation —appendContentnow calls real syntax validation instead of hardcodingsyntax_valid: true- Glob
edit_matchformatting — glob edits now use the fullauto_formatpipeline; the separatewrite_format_onlypath is removed formatter_timeout_secsconfig — the timeout override now flows correctly from both plugin config loaders into Rustconfigure- Pi formatter result fields — hoisted mutation results now expose
formattedandformatSkippedReasonto agents formatter_excluded_pathskip reason — paths excluded by scoped formatter config are now distinguished from generic formatter errors
Logging
- Rust session-id logging — all Rust command-side log lines now include
[ses_xxx]for correlation; background threads capture session_id at spawn time - Plugin per-request session-id — OpenCode and Pi plugin tool dispatch logs carry session IDs via
sessionLog/Warn/Error() - Test log isolation —
bun testnow writes toaft-plugin-test.loginstead of the liveaft-plugin.logto prevent test noise in production logs - RPC call/result noise removed — sidebar and
/aft-statuspolling no longer floods the plugin log
Audit fixes (P0/P1)
- Stdout write race — watchdog thread and main loop now share a properly serialized output path
delete_fileLSP path — uses validated canonical path for LSP notification instead of raw user inputmove_symbolambiguous result — returnssuccess: falsefor ambiguous symbol matches instead of misreporting successatomic_writetemp name — includes task ID to prevent concurrent write collisions- OpenCode pool
realpathSync—normalizeKey()now resolves symlinks like the Pi pool, fixing duplicate bridge entries on macOS/var→/private/var - Random slug collision — background task IDs now use 64-bit entropy instead of 32-bit
v0.18.2
Bug fixes
TUI plugin failed to load on npm installs (sidebar + /aft-status missing)
The published v0.18.0 and v0.18.1 npm tarballs were missing src/logger.ts,
which is a transitive dependency of src/shared/rpc-client.ts and several
other shared modules used by the TUI plugin. When OpenCode loaded the TUI
plugin from the npm package, the import chain failed with module-not-found,
silently disabling the entire TUI plugin entry — both the AFT sidebar slot
and the /aft-status command disappeared.
Local dev installs (file:// or workspace path) were unaffected because
src/logger.ts was always present on disk regardless of the package files
manifest. The bug existed since the sidebar was first added in v0.18.0.
Fix: src/logger.ts is now included in the published files array so
all transitive imports resolve when the package is installed from npm.
v0.18.1
v0.18.0
Highlights
Hoisted bash (experimental)
AFT now optionally replaces the built-in bash tool with a richer implementation. All three modes are opt-in via project or user config:
experimental.bash.rewrite: true—cat,grep,find,sed,lscalls are automatically rewritten to AFT counterparts (faster, structured output). Falls through to real bash when the rewrite would fail (e.g. paths outside project root).experimental.bash.compress: true— output compressors forgit,cargo,npm,bun,pnpm,pytest,tsc, plus a generic ANSI/dedup/truncate fallback.experimental.bash.background: true—bash({ background: true })returns a shortbgb-<8hex>task ID. Newbash_statusandbash_killtools manage long-running tasks. Tasks survive AFT restarts (disk-persisted) and completions are delivered via in-turn hooks and turn-end wake.
TUI sidebar
A live sidebar now shows the AFT badge, version, search index status, semantic index status, and on-disk sizes for both indexes. Refreshes every 1.5 s.
Config schema update (auto-migrated)
| Old key | New key |
|---|---|
experimental_search_index |
search_index (graduated to top-level) |
experimental_semantic_search |
semantic_search (graduated to top-level) |
experimental_lsp_ty |
experimental.lsp_ty |
experimental_bash_* |
experimental.bash.* |
Run aft doctor to confirm the migration.
Bug fixes
aft_refactorcorruptedexportkeyword —move,extract, andinlinenow correctly preserve theexporton the source file and don't leak it onto the next declaration. Inline call-site indentation is no longer doubled.apply_patchis now per-file commit — successful files are kept even if one hunk fails elsewhere.*** Add File:rejects overwriting an existing target. Fuzzy tab/space indent tier added. Move-hunk rollback fixed.ast_grep_replace— anonymous$$$in rewrites is now rejected at request time (was silently emitting$$$literally). Replaces every match per file (was first-match-only). Dry-run shows the actual diff.aft_import organizepreserves named-import aliases ({ stdin as input }) and per-nametypemarkers.- Glob
edit_matchrestores the checkpoint when any edited file fails syntax validation. transaction— batchedworkspace/didChangeWatchedFilesnotification now fires once per transaction, not once per edited file.
LSP
- Config-file edits (
package.json,tsconfig.json,Cargo.toml, lockfiles, etc.) now sendworkspace/didChangeWatchedFilesso phantomCannot find module 'bun'errors clear after multi-file patches. - LSP notifications fire after transaction validation, not before.
Reliability
- Bridge wire-format collision fixed —
bash,sed,grepno longer break when their params containcommand/methodfields. - Background bash: process-group kill, byte-safe truncate, session-scoped completions, 30-min default timeout.
- Bridge crash errors now appear in plugin logs only — agent-facing messages stay clean.
- Symlink chain escape in
validate_pathfixed for Linux (single-hop check missed multi-hop chains). - Windows build:
terminate_pgidimport and call gated behind#[cfg(unix)].
Internal
- 1064 → 1182 tests across Rust and plugins.
v0.17.3
Bug fixes
Stop returning stale LSP diagnostics after edits
The post-edit LSP diagnostics path could return entries from before the edit. Concrete repro from a real session: an agent's edit added 4 function call sites that used previously-unused imports — the edit response then reported "declared but never read" for those exact 4 imports. The agent thought it had broken something it had just fixed.
The fix is layered:
- Version-aware freshness check. When an LSP server publishes diagnostics, the post-edit wait now requires
publishDiagnostics.versionto match the document version we just sent. Servers that publish without a version (some still do) fall back to an epoch-delta check against a pre-edit snapshot. Either way, pre-edit cached entries can no longer be returned as fresh. - Multi-server completion. The wait now waits for all expected servers (TypeScript + ESLint + Biome co-servers, for example), not just the first one to publish. Servers that don't respond before the deadline appear in
lsp_pending_servers. Servers whose process exited mid-edit appear inlsp_exited_servers. - Default
wait_msraised to 3000ms. The previous 1500ms was too tight for tsserver re-analysis on monorepo files (real-world: 2–5s). Still capped at 10000ms. - Honest empty-publish handling. When a server publishes
diagnostics: []for the post-edit version, that's now correctly classified as fresh-and-clean instead of getting confused with timeout silence.
Honest tri-state reporting on edit responses
Edit responses (write, edit, apply_patch) now always include lsp_diagnostics, lsp_complete, lsp_pending_servers, and lsp_exited_servers when diagnostics: true was requested — even when the diagnostics array is empty. Before, an empty array was silently dropped from the response, so agents couldn't tell "checked clean" from "nothing happened."
Edit responses no longer carry LSP fields at all when diagnostics weren't requested (Oracle pre-release fix — preserves the no-diagnostics-requested response shape).
OpenCode plugin write/edit handlers now surface lsp_pending_servers / lsp_exited_servers to the agent as inline notes, so silence isn't mistaken for "all clear":
Note: LSP server(s) did not respond in time: typescript. Diagnostics may be incomplete; rerun lsp_diagnostics later for a fresh check.
Tighter complete calculation in lsp_diagnostics
Standalone lsp_diagnostics file mode no longer marks push_only servers as fresh without proof. With the default wait_ms=0, no waiting happened — so reporting complete: true against pre-existing stale cache was misleading. Now push_only only contributes to complete when wait_ms > 0 AND a publish actually arrived during the wait.
Multi-root server-key handling: publishDiagnostics events now correctly carry the workspace root instead of an empty PathBuf, fixing a latent bug that could mix up entries across multi-root sessions.
Internal
- Per-server epoch snapshots before
didChangeenable deterministic freshness detection - 5 new integration tests cover post-edit freshness, multi-server completion, root-aware storage, empty-publish-as-clean, and version-mismatch rejection (the last one uses an env-toggled stale-version mode in the fake LSP server)
- Tri-state reporting contract documented in
ARCHITECTURE.mdand theResponsedoc comments
Upgrade
{
"plugin": ["@cortexkit/aft-opencode@latest"]
}Or run bunx --bun @cortexkit/aft@latest doctor to verify your install.