Releases: cortexkit/aft
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
v0.20.0
Highlights
`Foreground bash now auto-promotes long-running tasks to the background instead of killing them at an arbitrary timeout. Agents get a fast inline result for short commands and a reliable completion reminder for long ones, with no need to predict task duration up front.
Three other user-visible changes:
- Vue (
.vue) is now a first-class language foraft_outline,aft_zoom, andast_grep_search/ast_grep_replace. - Auto-update reliability fix — the plugin update checker now triggers at plugin load instead of only at
session.created, so resumed sessions and parallel OpenCode windows actually check for new versions. - Brace-aware grep includes — patterns like
*.{vue,ts}and*.{js,jsx,ts,tsx}no longer get split on the comma into invalid separate globs.
Foreground-as-polled-background bash
Every bash call now routes through the same background infrastructure internally, so the Rust dispatch loop never blocks. Foreground execution becomes a thin polling layer on top:
- Plugin polls
bash_statusfor up to ~5 seconds. - If the task finishes inside the wait-window, it returns inline as before.
- If it doesn't, the plugin returns a "promoted to background" message and the agent gets a
taskIdit canbash_status/bash_killagainst later. - A completion reminder is delivered automatically when the task actually finishes, even if the agent has already moved on to other work.
Crucially, the wait-window is decoupled from the task's kill cap:
| Call shape | Wait-window | Task kill cap |
|---|---|---|
bash({ command }) |
5s | 30 minutes |
bash({ command, timeout: 30000 }) |
5s | 30s (hard kill at timeout) |
bash({ command, timeout: 2000 }) |
2s | 2s |
bash({ command, background: true }) |
0 (no poll) | 30 min |
bash({ command, background: true, timeout: 600000 }) |
0 | 10 min |
Practical effect: a long-running e2e test launched as foreground bash with no timeout no longer gets killed after 30 seconds. It runs in the background up to the 30-minute default, and the completion reminder carries the actual exit code and a tail of the output. Explicit timeout: N still means "hard kill at N seconds" — same mental model as timeout(1), Docker, and Kubernetes.
bash_status and bash_kill are now registered alongside bash whenever any experimental.bash.* flag is on, not just when experimental.bash.background is enabled. This way the agent always has tools to inspect or kill auto-promoted tasks, regardless of which experimental originally enabled bash hoisting.
The timeout schema is also tightened — agents can only pass positive integer milliseconds; NaN, negatives, zero, and floats are rejected at the schema level, eliminating a class of invalid-input edge cases that could hang the polling loop.
Vue support
tree-sitter-vue is now wired through the parser, language detection, outline, zoom, and AST stack. Single-file components extract template, <script setup lang="ts">, and <style scoped> as top-level outline nodes. Embedded script content is opaque raw_text to tree-sitter-vue (a known upstream limitation), so deep symbol extraction inside the script block is not yet available — but Vue templates and component structure are now searchable and editable through AFT's structural tools.
AST patterns work too: @click="$NAME" and similar template patterns capture meta-variables correctly.
Auto-update fix
The plugin's update checker previously hooked into session.created, which meant resumed opencode -s sessions and parallel windows that joined an existing project never re-checked for new versions. The checker now triggers at plugin init with a short delay, coordinates across parallel plugin instances via an on-disk dedup file under the plugin storage directory, and clears pending timers on abort.
Brace-aware grep includes
Naive comma-splitting in the hoisted grep tool's include parameter was breaking patterns like *.{vue,ts} into the two invalid fragments *.{vue and ts}. The split is now brace-aware in both OpenCode and Pi adapters, so multi-extension include patterns work correctly.
Quality
- +62 Pi unit tests across 17 new files (audited against actual module gaps rather than indiscriminate coverage).
- Audit-driven safety hardening in the bash subsystem: input validation at the schema boundary, wait-window math simplification, transport-timeout cleanup, parity fix between Pi and OpenCode
isTerminalStatusallowlists. - Combined test surface: 1,155 Rust tests, 672 OpenCode plugin tests, 450 Pi plugin tests, plus typecheck and lint clean across all four workspaces.
Full Changelog:
v0.19.6...v0.20.0
v0.19.6
Highlights
Bash permissions
bash:
"*": deny
"terraform fmt *": allow
"terraform validate *": allow
"terraform plan *": allow
"terraform apply *": ask…where the agent was still running non-whitelisted commands. Investigation found four independent bypasses, all fixed in this release:
echoskipped permission asks. The Rust scanner exemptedechofrom the ask list via a special case OpenCode does not have. Removed.- Zero-
command-node inputs ran without asks. Pure redirects (> /tmp/x), variable assignments (FOO=bar), arithmetic (((i++))), test forms ([[ -f foo ]]),readonly,declare, subshells, and grammar-load failures all parsed cleanly but produced nocommandAST node — so the per-node loop iterated zero times and the ask list stayed empty. Now fail-closed: any non-empty input that produces zero asks emits a wildcard"*"bash ask. - Plugin-side
runAsknever executed.@opencode-ai/plugin@1.14.xreturnsEffect<void>fromctx.ask(), and AFT's bundledeffect@3.xrunner could not run an Effect 4 instance — every ask rejected with"Not a valid effect"before permission evaluation ran. AFT now uses the sameeffectruntime as the host SDK (peer dependency), so allow/deny decisions actually propagate. - OpenCode SDK peers bumped to
^1.14.39so deny rules are evaluated against the current permission model.
$SHELL respect on Windows
Mirrors OpenCode's resolution algorithm exactly (packages/opencode/src/shell/shell.ts):
$SHELLenv varpwsh.exepowershell.exe- Git-for-Windows
bash.exeauto-detected next togitonPATH cmd.exe/$COMSPEC
WindowsShell::Posix(PathBuf) is invoked as <shell> -c <command> and ships with the same wrapper / exit-marker mechanics as the PowerShell and cmd variants for both foreground and background bash.
Background bash on Windows now also propagates correctly through the new POSIX path: wrapper script written as .sh, invoked as <bash> <wrapper>, exit code captured via the standard trap + atomic-rename pattern.
ls rewrite preserves intent
Two related fall-through cases in crates/aft/src/bash_rewrite/rules.rs:
ls -l README.mdpreviously rewrote toread README.md, dumping file contents instead of showing size, mtime, permissions, owner. Now falls through to real bash so-lusers see realls -loutput.ls README.md(no flags) andread README.mdare also not equivalent:ls FILEechoes the filename,read FILEdumps contents. Stat the path; fall through when it's a regular file.ls NONEXISTENTfalls through too — bash'sNo such file or directorywording is what agents already know.
ls ., ls DIR, and ls (cwd) continue to rewrite to read for directory listings, where the semantics match.
Other fixes
- Windows background bash picks up the new
WindowsShell::Posixvariant correctly (the v0.19.5 enum refactor missed thebash_backgroundregistry).
Full Changelog:
v0.19.5...v0.19.6
v0.19.5
Highlights
This release is mostly a stability + product-quality patch on top of v0.19.4. Three notable user-visible changes:
- Solidity is now a first-class language for
aft_outline,aft_zoom, andast_grep_search/ast_grep_replace. ast_grep_*now returns actionable hints when an agent's pattern produces zero matches because of common mistakes (regex alternation in AST patterns, Rust match-arm pipes, trailing-colon Python defs, etc.) instead of silently looking like a clean no-op.aft doctor --fixcan now consent-gate ONNX Runtime cleanup so semantic search can recover from a corrupted or incompatible local install without manual surgery;--clearcan also offer to drop stale cachedaftbinary versions.
The release also fixes a correctness bug that made self-hosted semantic-embedding backends unusable, modernizes the install/update story, and brings the release CI in line with the PR-time CI used to gate every change.
New language support
- Solidity (
tree-sitter-solidity) — outline contracts, libraries, interfaces, modifiers, constructors, events, errors, structs, enums, and state variables;aft_zoomworks on any of those;ast_grep_searchandast_grep_replaceuse$(notµ) for meta-variables because$is a valid Solidity identifier character. Callgraph, imports, formatter, and checker integrations are intentionally not part of this drop.
AST-grep hints on zero matches
Both ast_grep_search and ast_grep_replace now ship server-side hint metadata when:
- a pattern fails to compile (
invalid_pattern); or - a pattern compiles but produces zero matches.
The hint engine recognizes patterns like foo|bar (regex-style alternation in an AST pattern), => { ... } | _ => { ... } (Rust match-arm pipes), trailing-colon Python def shapes, anonymous $$$ in rewrites, and several other common AST-vs-text mistakes. Both OpenCode and Pi render the same shared hint, so failures are now self-explaining instead of looking like silent no-ops.
Semantic search backends
Self-hosted embedding endpoints work again. The previous SSRF guard rejected 127.0.0.1 and localhost, which broke the default Ollama configuration. The configure-time validator now allows loopback for openai_compatible and ollama backends; the corresponding README docs were also added for backend selection, fingerprinting (changing backend / model / base_url rebuilds the index), and the trust boundary between user and project config.
Doctor improvements
aft doctor (the unified @cortexkit/aft CLI) gained two interactive remediation paths:
aft doctor --fixnow offers a guided ONNX Runtime cleanup. If a systemlibonnxruntimeis too old to satisfy AFT'sfastembedbuild, the bridge already skips it and falls through to AFT's managed download — but if the managed copy itself is corrupted, this command clears only AFT-managed ONNX paths (never system libraries) and lets the next startup re-download cleanly.aft doctor --clearcan additionally drop stale cached AFT binary versions from~/.cache/aft/bin/while keeping the active version.
The CLI also now tolerates stdout noise from the aft child process (per #29): one-shot bridge requests buffer non-JSON or malformed lines as diagnostics and only fail when too few valid protocol responses arrive, surfacing binary path, exit code, stderr tail, and sample noise lines instead of throwing a raw SyntaxError.
Install and update modernization
User-facing install flows are now npx-only — npx @cortexkit/aft setup, npx @cortexkit/aft doctor, npx @cortexkit/aft doctor lsp <file>. README, CLI help text, and the Rust ONNX error messages all use the new wording.
The auto-update path also moved off bun install. The OpenCode plugin's auto-updater now spawns npm install --no-audit --no-fund --no-progress because OpenCode populates ~/.cache/opencode/packages/<plugin>/ with package-lock.json, not bun.lock — so the previous code path either no-op'd (no bun.lock to touch) or generated a parallel bun.lock that drifted from npm's view of dependencies. Bun is still fully supported as a runtime; the package shebang stays #!/usr/bin/env node.
Release CI now gates on E2E
Tag pushes were previously skipping the full E2E matrix; only the basic test job gated build/publish. An e2e regression on main could ship to production through a tag push without ever running the e2e suite. Two changes:
_e2e-suite.ymlis a single reusable workflow defininge2e-linux(18 Docker scenarios) ande2e-windows(27 native scenarios).tests.ymlandrelease.ymlboth call it — single source of truth, no drift between PR-time and release-time E2E.- All five platform-binary build jobs and
publish-cratesnow depend on[test, e2e]. An E2E regression blocks the entire publish chain (npm packages, platform binaries, GitHub release, and crates.io).
The release workflow also caught up with tests.yml on its own image (ubuntu-22.04), Cargo cache (Swatinem/rust-cache@v2), and test invocation (cargo test --workspace). Release-time CI is now a faithful preview of PR-time CI on the same code, instead of a much-slower divergent run.
Quality and reliability
format::tests::resolve_tool_caches_*is no longer flaky on Linux runners. Both tests mutate the global tool-resolution cache; they now serialize through a tests-module-scopeMutexso one test'sclear_tool_cache()cannot wipe an entry the other test had just written.callgraph::tests::callgraph_watcher_{add,remove}_callerare no longer flaky on macOS. Watcher events have up to ~1s coalescing latency under load; both tests now poll for the watcher to catch up with a generous deadline instead of asserting after a fixed 500 ms sleep.- Remaining Clippy debt was cleared so
cargo clippy --workspace --all-targets -- -D warningsis now part of the green gate. scripts/release.shnow refreshesbun.lockautomatically as part of the version bump. Earlier releases could leavebun.lockout of sync with the syncedpackage.jsonversions; CI runs with--frozen-lockfilewould then fail at install. The release script also keepsCargo.locksynced.- Pre-existing Windows ARM64 Cargo job in
tests.ymlwas scoped to lib tests only and marked non-blocking until the older Windows integration tests get broader JSON-and-path-escaping cleanup. Linux unit tests, Linux Docker E2E, and Windows native E2E remain the authoritative green gates.
Closes
- #29 —
aft-clicrashed on stdout noise from the bridge child.
v0.19.4
Highlights
Background bash on Windows works for the first time. Foreground bash gets robust shell fallback. Watchdog correctness improvements apply to both Unix and Windows.
What's new
Issue #27 fix — foreground bash shell fallback (Windows)
Windows users on stripped SKUs (IoT LTSC, Server Core), restricted PATH inheritance, ASR/AppLocker policies blocking PowerShell — all now get bash via runtime shell selection. AFT walks pwsh.exe → powershell.exe → cmd.exe and retries with the next candidate when a spawn fails with NotFound. cmd.exe is always added as the floor; it lives in a Windows search-path location that ASR/AppLocker policies generally cannot remove.
Foreground bash also stops inheriting stdin from the bridge's NDJSON pipe — the original issue #26 deadlock root cause that survived earlier fixes only on a few SKUs.
Background bash on Windows (new feature)
Detached background bash now works on Windows with full Unix architectural parity:
- File-based wrappers (
.ps1/.batper task) eliminate cross-shell command-line quoting issues that dropped wrapper output silently. cmd.exeis preferred over PowerShell for bg-bash because PowerShell's detached-handle inheritance dropped wrapper stdout/stderr on Windows 11. cmd is reliable and!ERRORLEVEL!correctly captures the user command's real exit code.- PID-based replay survives the AFT process exiting; tasks resume cleanly on next bridge spawn.
- Watchdog detects the wrapper crashing without writing an exit marker — the task moves to
Failedwithin ~250ms instead of staying stuckRunninguntil the 24h timeout.
Watchdog correctness (cross-platform)
Tasks whose wrapper crashes — child segfault, OOM, signal — now terminate within one watchdog tick (~250 ms) and surface as Failed with exit_code=null. Previously they stayed stuck Running until the 24-hour task timeout.
storage_dir fallback fix (Windows)
The HOME env var is not set on Windows by default. AFT's storage_dir fallback chain (storage_dir | HOME | ".") was degrading to "." on Windows when no explicit storage_dir was configured, producing relative-path bg-bash exit-marker writes that failed under move /Y. The fallback now also checks USERPROFILE. Same pattern fixed in three call sites: bash_background, search_index, and semantic_index.
Test coverage
- New Windows e2e harness scenarios verify bg-bash on real Windows 11 ARM64 (via Parallels): cmd-wrapper exit-code recording, PATH-forced cmd fallback, runtime shell retry. 27/27 passing on the live VM.
- Cross-platform unit tests for the retry-loop logic (
try_spawn_with_fallback) and watchdog Failed-marking (reap_child). - Linux Docker E2E now includes a longer scenario-1 timeout for cold-cache QEMU runs.
CI
tests.ymlnow runs Cargo + Bun + Linux Docker E2E + Windows native E2E on every PR. Switched frombun --filtertobun run --cwd <path>for workspace builds (matches the working release.yml syntax). Made integration test helpers cross-platform socargo test --workspacepasses on Windows runners.
Verified scope
- ✅ Foreground bash with shell fallback (live Win11 ARM64)
- ✅ Background bash via cmd.exe wrapper (5 new e2e scenarios)
- ✅
!ERRORLEVEL!exit-code capture under cmd /V:ON - ✅ Watchdog Failed-marking on dead-PID-no-marker
- ✅ Linux x64 Docker E2E (18/18)
- ✅ Windows cross-compile clean
- ✅ 531 lib + 491 integration Rust tests on macOS
Not yet verified
- pwsh.exe (PowerShell 7+) end-to-end — not installed on the test VM. Code path is exercised by unit tests through
try_spawn_with_fallbackmocks. - Other Windows SKUs (Server Core, IoT LTSC) — theoretical based on shell-fallback architecture.
- Windows ARM64 native build —
ring/clangbuild deps still fail; use the cross-compiled x64 binary under WoW64 emulation.
v0.19.3
v0.19.3
A focused stability release closing four real production issues observed on a live developer machine: orphaned LSP processes, wasted bridge spawns when the host launches from $HOME, and a TUI status display that conflated cross-project disk usage with the current project.
Fixes
No more orphaned LSP children on shutdown
When aft received SIGTERM (plugin restart, e2e cleanup, OpenCode quit), the signal handler called process::exit directly without killing spawned LSP children. Direct children (typescript-language-server, biome lsp-proxy, etc.) and Node-shim grandchildren got orphaned to PID 1.
$HOME no longer accepted as a project root
When OpenCode Desktop or Pi launches the plugin from the user's home directory and a session has no stored project directory, the resolver was handing the plugin $HOME as the project root. Configuring against $HOME walks the entire user home tree (often hundreds of thousands of files), hits the 30s configure timeout, gets killed, and silently retries on every reload — wasting one full bridge spawn per host launch.
Two-layer guard:
- The eager-configure flow in both plugins now detects
$HOMEand skips with a clear log message:"Eager configure skipped: cwd=$HOME is the user home directory." BridgePool.getBridge()itself throws a typedHomeProjectRootErrorif it ever receives$HOME(defense-in-depth).
Subdirectories of $HOME remain valid project roots and pass through unchanged.
TUI sidebar shows the current project's disk size, not the cross-project total
Status disk.trigram_disk_bytes and disk.semantic_disk_bytes were summing recursively across the entire <storage>/index/ and <storage>/semantic/ directories — every project the user had ever opened. A 4.8 MB project could appear to use 16+ GB because a sibling project's cache was huge.
Both caches are partitioned per project by project_cache_key(project_root). Status now resolves the current project's key and sizes only that project's slice. The new disk.project_cache_key field also exposes the key so hosts can correlate disk numbers with on-disk cache directories.
Diagnostics
Bridge Spawning binary: ... and Bridge killed after timeout log lines now carry the triggering session ID, making it much easier to correlate plugin logs against agent traces. Lines that legitimately have no session attribution (eager configure, watcher events, startup) stay unprefixed — that's the correct semantics.
Coverage
This release adds 5 new tests:
- 3 LSP-child-registry tests covering
trackon spawn,untrackon graceful shutdown, anduntrackonDrop(regression guard for the orphan leak). - 10
$HOMEguard tests covering the helper function, the typed throw, the cached-entry-bypass case, and the subdirectory passthrough. - 2 status-disk-scope tests proving project A's status doesn't include project B's cache size, and that an empty project reports zero rather than the cross-project total.
All existing tests continue to pass: 522 Rust unit + 490 integration + 32 aft-bridge + 41 CLI + 380 Pi + 678 OpenCode = 2,143 tests, 0 fails.
Recommended cleanup
If you've been running OpenCode Desktop or Pi from $HOME on previous versions, your <storage>/index/ directory may contain a large $HOME-rooted index that is now permanently abandoned. To reclaim disk space:
# Inspect AFT cache sizes
du -sh ~/.local/share/opencode/storage/plugin/aft/index/*
# A 16 GB+ entry that doesn't match a project you actually use is the
# orphaned $HOME index — safe to delete.
rm -rf ~/.local/share/opencode/storage/plugin/aft/index/<hash>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