Skip to content

Releases: cortexkit/aft

v0.19.6

06 May 19:47

Choose a tag to compare

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:

  1. echo skipped permission asks. The Rust scanner exempted echo from the ask list via a special case OpenCode does not have. Removed.
  2. 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 no command AST 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.
  3. Plugin-side runAsk never executed. @opencode-ai/plugin@1.14.x returns Effect<void> from ctx.ask(), and AFT's bundled effect@3.x runner could not run an Effect 4 instance — every ask rejected with "Not a valid effect" before permission evaluation ran. AFT now uses the same effect runtime as the host SDK (peer dependency), so allow/deny decisions actually propagate.
  4. OpenCode SDK peers bumped to ^1.14.39 so 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):

  1. $SHELL env var
  2. pwsh.exe
  3. powershell.exe
  4. Git-for-Windows bash.exe auto-detected next to git on PATH
  5. 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.md previously rewrote to read README.md, dumping file contents instead of showing size, mtime, permissions, owner. Now falls through to real bash so -l users see real ls -l output.
  • ls README.md (no flags) and read README.md are also not equivalent: ls FILE echoes the filename, read FILE dumps contents. Stat the path; fall through when it's a regular file.
  • ls NONEXISTENT falls through too — bash's No such file or directory wording 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::Posix variant correctly (the v0.19.5 enum refactor missed the bash_background registry).

Full Changelog:
v0.19.5...v0.19.6

v0.19.5

06 May 11:58

Choose a tag to compare

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, and ast_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 --fix can now consent-gate ONNX Runtime cleanup so semantic search can recover from a corrupted or incompatible local install without manual surgery; --clear can also offer to drop stale cached aft binary 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_zoom works on any of those; ast_grep_search and ast_grep_replace use $ (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 --fix now offers a guided ONNX Runtime cleanup. If a system libonnxruntime is too old to satisfy AFT's fastembed build, 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 --clear can 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.yml is a single reusable workflow defining e2e-linux (18 Docker scenarios) and e2e-windows (27 native scenarios). tests.yml and release.yml both call it — single source of truth, no drift between PR-time and release-time E2E.
  • All five platform-binary build jobs and publish-crates now 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-scope Mutex so one test's clear_tool_cache() cannot wipe an entry the other test had just written.
  • callgraph::tests::callgraph_watcher_{add,remove}_caller are 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 warnings is now part of the green gate.
  • scripts/release.sh now refreshes bun.lock automatically as part of the version bump. Earlier releases could leave bun.lock out of sync with the synced package.json versions; CI runs with --frozen-lockfile would then fail at install. The release script also keeps Cargo.lock synced.
  • Pre-existing Windows ARM64 Cargo job in tests.yml was 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

  • #29aft-cli crashed on stdout noise from the bridge child.

v0.19.4

05 May 19:40

Choose a tag to compare

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 / .bat per task) eliminate cross-shell command-line quoting issues that dropped wrapper output silently.
  • cmd.exe is 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 Failed within ~250ms instead of staying stuck Running until 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.yml now runs Cargo + Bun + Linux Docker E2E + Windows native E2E on every PR. Switched from bun --filter to bun run --cwd <path> for workspace builds (matches the working release.yml syntax). Made integration test helpers cross-platform so cargo test --workspace passes 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_fallback mocks.
  • Other Windows SKUs (Server Core, IoT LTSC) — theoretical based on shell-fallback architecture.
  • Windows ARM64 native build — ring/clang build deps still fail; use the cross-compiled x64 binary under WoW64 emulation.

v0.19.3

05 May 12:13

Choose a tag to compare

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 $HOME and skips with a clear log message: "Eager configure skipped: cwd=$HOME is the user home directory."
  • BridgePool.getBridge() itself throws a typed HomeProjectRootError if 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 track on spawn, untrack on graceful shutdown, and untrack on Drop (regression guard for the orphan leak).
  • 10 $HOME guard 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

04 May 22:45

Choose a tag to compare

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 -NonInteractive to 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: map win32-arm64 → win32-x64 so 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 shared platformKey() 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's process.arch). Node correctly reports arm64 but our x64 aft.exe under Prism panics on dlopen of 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

04 May 18:38

Choose a tag to compare

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 wakeFiredThisIdle gate 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_zoom now returns plain-text formatted output across both OpenCode and Pi via shared formatZoomText. No more JSON-escaped newlines and quotes in agent-visible blobs.
  • URL fetch under Node v24 now correctly handles dual lookup callback shape with opts.all: true (Pi aft_outline(url:) was failing with ERR_INVALID_IP_ADDRESS). Bumped to undici v8 with surfaced underlying fetch causes.
  • New BridgePool.setConfigureOverride() API lets the plugin layer patch configure overrides on already-running bridges.

Plugin behavior

  • bash is now only hoisted when at least one of experimental.bash.rewrite, experimental.bash.compress, or experimental.bash.background is 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_status polling 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.json plus bun-types and typescript devDeps so scripts/**/*.ts get 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

04 May 07:48

Choose a tag to compare

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 warningsconfigure returns immediately. File-walk + language detection + missing-binary detection happen on a background thread and stream back as configure_warnings push frames.
  • Home-directory bridge hang fix — FSEvents watcher attach moved off the configure foreground. Launching OpenCode from $HOME no 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_outlinetarget accepts http:// / https:// URLs. Auto-detected by URL prefix.
  • aft_zoom — new optional url parameter (mutually exclusive with filePath). 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_outline unified target — file path, directory path, URL, or array of paths in one parameter (auto-detected). Replaces the previous mutually-exclusive filePath / files / directory shape.
  • aft_delete takes files: 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_outlineaft_zoom chains, URL fetching, long-running command patterns) into the agent's system prompt. Conditional on the actual configured tool surface.

Bug fixes (audit batch)

  • configure_warnings session routing — async push frames now carry session_id end-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_disk so 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_outline directory honest signalingwalk_truncated, complete, and skipped_files fields are preserved end-to-end.
  • apply_patch diff 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-bridge shared package — bridge transport, pool, downloader, resolver, platform mapping, ONNX runtime, and URL fetcher all moved into one shared package. Both @cortexkit/aft-opencode and @cortexkit/aft-pi consume from it. Single source of truth for transport behavior.
  • URL fetching consolidatedfetchUrlToTempFile, cleanupUrlCache, and _isPrivateIpv4 are now exported from @cortexkit/aft-bridge so OpenCode and Pi share one DNS-pinning and SSRF-guarding implementation.

Install

bunx --bun @cortexkit/aft setup

If 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

02 May 08:10

Choose a tag to compare

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 BufWriter for 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_url at configure time (#25)

P1 — Correctness

  • LSP didOpen notification now uses the validated path after delete_file (#6)
  • aft_outline directory 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_symbol now 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 realpathSync to avoid duplicate bridges (#14)

P2 — Robustness

  • Background bash temp files include task ID for easier debugging (#12)
  • Commands report complete: true/false honestly per the tri-state protocol (#16)
  • Transaction rollback failures use a distinct rollback_failed error code (#17)
  • grep passes leading-dash patterns to rg via -- to prevent flag injection (#18)
  • Bash rewrite regex patterns capped at 10 KB to prevent ReDoS (#19)
  • Glob edit_match checkpoints 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_zoom surfaces partial failures per symbol (#24)
  • Background task IDs use OS entropy (getrandom) — format is now bgb-<8hex> (#26)
  • File watcher filters by exact path component, not substring (#28)
  • bash_status now includes stderr_path for 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 accessaft_outline({ url })aft_zoom({ url, symbol }) instead of fetching whole pages
  • Code explorationgrep/aft_searchaft_outlineaft_zoom instead of chains of read calls
  • Relationship questionsaft_navigate (callers, impact, trace_to, trace_data) instead of grep + read chains (shown only at tool_surface: "all")
  • Long-running commandsbash({ background: true }) + bash_status (shown only when experimental.bash.background is enabled)

Always-on, no config toggle needed. Irrelevant sections are omitted automatically.

Bug fixes

  • Windows build: signal_hook gated behind cfg(unix) — Windows x64 binary now builds correctly
  • Test files missing /// <reference path="../bun-test.d.ts" /> directive fixed

v0.18.3

01 May 14:51

Choose a tag to compare

What's Changed

Bug Fixes

  • Background bash statustimed_out tasks now display correctly; the plugin was checking for "timeout" while Rust serializes as "timed_out"
  • Pi bash wake messagessendUserMessage now passes { deliverAs: "followUp" } to prevent "Agent is already processing" rejections mid-turn
  • HTML aft_zoom section content — heading symbols now return the full section body down to the next sibling heading, not just the heading line itself
  • Pi bash rendererrenderResult now shows last 25 lines of output, matching Pi's built-in bash behavior
  • handle_append syntax validationappendContent now calls real syntax validation instead of hardcoding syntax_valid: true
  • Glob edit_match formatting — glob edits now use the full auto_format pipeline; the separate write_format_only path is removed
  • formatter_timeout_secs config — the timeout override now flows correctly from both plugin config loaders into Rust configure
  • Pi formatter result fields — hoisted mutation results now expose formatted and formatSkippedReason to agents
  • formatter_excluded_path skip 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 isolationbun test now writes to aft-plugin-test.log instead of the live aft-plugin.log to prevent test noise in production logs
  • RPC call/result noise removed — sidebar and /aft-status polling 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_file LSP path — uses validated canonical path for LSP notification instead of raw user input
  • move_symbol ambiguous result — returns success: false for ambiguous symbol matches instead of misreporting success
  • atomic_write temp name — includes task ID to prevent concurrent write collisions
  • OpenCode pool realpathSyncnormalizeKey() 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

30 Apr 18:36

Choose a tag to compare

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.