Releases: cortexkit/aft
v0.30.1
v0.30.1
Patch release. Three classes of user-facing fixes: bash PTY parameter handling, LSP failure diagnostics, and Windows plugin auto-update.
Bash — PTY parameter handling
Agents that defensively included ptyRows or ptyCols on regular (non-PTY) bash calls were hitting a strict validation error. Some models tried to "fix" it by adding pty: true to non-interactive commands, which auto-promoted them to background and broke inline output.
ptyRowsandptyColsare now soft-ignored whenptyis unset or false. The dimensions are only applied when a PTY is actually requested.pty: truenow impliesbackground: true. The two flags no longer have to be set together.- Out-of-range or non-integer values return a clean error naming the allowed bounds (e.g.
ptyRows must be an integer between 1 and 60). - Tool descriptions for
ptyRows/ptyColsclarify they apply only whenpty: true.
Plugin tool schemas
All optional numeric parameters across the OpenCode plugin (bash, read, aft_search, aft_navigate, aft_zoom, aft_outline, refactor, lsp_diagnostics) now use a JSON-Schema-representable bounded integer schema. Empty sentinels (null, empty string, zero) are rejected at validation with a clear message instead of silently being coerced or — as in an earlier internal build — causing the plugin to fail to load.
A schema-conversion regression test now covers every registered tool, so any future change that introduces an unrepresentable shape will fail before release.
LSP — failure visibility
When an LSP server fails to start, AFT's response now surfaces stderr output captured from the child process. Previously, broken language-server shims (such as a typescript-language-server whose cli.mjs was missing) returned opaque spawn_failed errors without context.
- Stderr from LSP children is captured in a bounded ring buffer and included in failure responses.
- When stderr contains
MODULE_NOT_FOUND, the response adds a hint pointing at the likely fix (reinstall the package-manager binary, or check thelsp.servers.<name>.binarypath). - Clients that crash after a successful initialize are now marked as failed so subsequent file requests stop re-issuing pulls against the dead pipe.
Auto-update on Windows
Plugin self-update used spawn("npm") directly, which fails on Windows because the binary is npm.cmd. The auto-update path now resolves npm.cmd on Windows (same fix shape as the v0.28.2 LSP install correction).
npm installstderr is captured on failure for diagnostic visibility.--ignore-scriptsis now passed to the install (matches the LSP install hardening).
Other
aft_outline/aft_zoomURL fetch keeps the 15-second body-stall safety net that landed in v0.30.0.- Subagent sessions continue to silently convert
background: truebash to foreground (introduced in v0.30.0), because subagents have no completion-reminder mechanism.
v0.30.0
PTY support — agents can now drive real terminals
The headline of this release. bash now accepts pty: true (with background: true) to spawn commands inside a real PTY — every interactive program that needed a terminal is now reachable from an agent loop. Python and Node REPLs, vim, htop, top, less, fzf, build TUIs, even a nested opencode session — all work end-to-end.
Yes, really — opencode inside opencode works. PTY support means the agent can drive any TUI, including a full nested AFT-equipped OpenCode session, complete with sidebar, MCP servers, LSP status, and another agent answering prompts. Recursion all the way down.
How it works
-
bash({pty: true, background: true, ptyRows?, ptyCols?})— spawn a PTY-backed task. Defaults are 24×80; caps are 60×140 to keepbash_statussnapshots bounded. -
bash_status({taskId, outputMode})— read the terminal state."screen"— vt100-rendered visible terminal (rows × cols characters)"raw"— uncompressed bytes including ANSI escape sequences"both"— separate fields for each
-
bash_write({taskId, input})— send keystrokes. Input is either a verbatim string or an array mixing strings and{key: "..."}objects for atomic text + control key sequences:bash_write({taskId, input: [ "iHello", {key: "esc"}, ":wq", {key: "enter"}, ]})Named keys cover
enter/return(CR),tab,space,backspace,esc/escape, arrow keys, navigation keys,delete,insert,f1–f12, andctrl-athroughctrl-z.
PTY tasks run on Unix via portable-pty and on Windows via ConPTY.
bash_watch unifies pattern notifications and sync waits
New bash_watch tool replaces ad-hoc wait flags on bash_status. Two modes:
Sync — bash_watch({taskId, pattern?, timeoutMs?}) blocks until the pattern matches, the task exits, or timeout. Without a pattern it waits for task exit. Returns the snapshot inline so the agent gets the result without a separate completion reminder.
Async — bash_watch({taskId, pattern, background: true}) registers a pattern watcher and returns immediately. When the pattern matches mid-stream or the task exits, a single [BG BASH NOTIFY] reminder fires with the matched line. The default [BACKGROUND BASH COMPLETED] reminder is suppressed for that task.
bash_status is now a pure snapshot tool — wait/watch semantics live in bash_watch.
URL fetches no longer hang on slow servers
aft_outline and aft_zoom URL targets now abort with a clear stall error after 15 seconds without a chunk. Previously a slow or stalled server could hang the bridge indefinitely while waiting on reader.read().
Other
bashschema rejectspty: truewithoutbackground: trueandptyRows/ptyColswithoutpty: true.- OpenCode subagent sessions silently convert
background: trueto foreground bash unlessbash.subagent_background = truein config. bash_statusandbash_killare always registered whenbashis registered (no longer gated onexperimental.bash.background).- Background bash completion delivery now persists
completion_deliveredacross plugin restarts, so previously-delivered tasks no longer replay as fresh reminders after restart. - Async
bash_watchexit notifications render astask X exitedinstead of the priormatched "exited (exit 0)"framing. - The release script blocks minor-version releases when the in-plugin
ANNOUNCEMENT_VERSIONis stale relative to the release tag.
v0.29.1
v0.29.1
Patch release with three user-facing fixes uncovered during v0.29.0 dogfooding.
TUI background bash wakes work without --port 0
Plain OpenCode TUI users (without opencode --port 0) previously couldn't receive background bash completion reminders. The live-server wake workaround couldn't reach the HTTP listener, and the in-process fallback was disabled to avoid the upstream duplicate-runner bug (anomalyco/opencode#28202).
Now the plugin probes the live server once at startup and picks the best available transport:
- Live server reachable (Desktop,
opencode --port 0TUI): uses the workaround, no duplicate runs - Live server unreachable (plain TUI): falls back to the in-process
promptAsyncso wakes still arrive
The one-time --port 0 nudge ignored-message is removed. The trace event bash_completion_wake_prompt_async_start now carries wake_client_path: "live-server" | "in-process-fallback" for post-mortem.
Schema gap: top-level bash field
The bash field that graduated out of experimental.bash in v0.27.2 was missing from the published aft.schema.json. Editors like VS Code reported the field as invalid even though it was accepted. Fixed in build-schema.ts and serving immediately from the master schema URL.
Background bash watchdog: false-positive Failed state
The watchdog's reap_child() could mark a still-cleaning-up task as Failed when the bash wrapper's exit-marker write race lost a single sample. The watchdog now defers the failure decision to the next poll cycle — if the marker is still absent, it declares failure; otherwise the next pass picks it up normally. Adds at most ~500ms latency to genuinely-failed tasks, eliminates the test-suite flake.
Also includes per-test storage isolation in configure_background to prevent parallel test instances from replaying each other's background tasks.
v0.29.0
AFT v0.29.0 is the compression-overhaul release: a fundamental dispatch fix lifts existing compressors out of silent failure on real-world bash invocations, eight new tool-specific compressors land alongside seven new TOML filters for high-leverage tools agents actually run, and bun test learns to keep output from chained shell commands.
Specificity-based compressor dispatch
Until v0.29, package-manager compressors (bun, npm, pnpm) claimed any command starting with their head token — so bun run vitest, npm test, and pnpm test were silently routed through the package-manager fallback path instead of through the specific test-runner compressors that knew how to keep failure detail.
v0.29 introduces a two-tier dispatch system:
- Specific compressors (vitest, eslint, biome, pytest, tsc, plus the seven new ones below) match by token-anywhere and win first.
- PackageManager compressors (bun, npm, pnpm) match by head-token and run only if no Specific match was found.
This means bun run vitest --reporter=verbose now compresses through the Vitest reporter logic, not the generic Bun fallback. The same fix unlocks npm test, pnpm test, pnpm run typecheck, and similar idiomatic invocations that were never reaching their intended compressor.
Shell-prefix normalization
A real-DB sweep of 165 historical bash invocations found that ~21% of bun test calls and ~7% of npm calls were wrapped in shell-prefix idioms agents reach for routinely:
cd /path && bun testenv FOO=bar npm installtimeout 30 cargo build(cd /path; cmd)nohup cmd &
These wrappers hid the real command head from per-module matchers, so all of them silently fell through to generic middle-truncation. v0.29 normalizes shell prefixes once in the dispatcher before passing to compressors. Each module's matcher stays unchanged; the normalization is one well-defined entry point.
Multi-command chains (pwd && git status && bun test) are intentionally not normalized — extracting the "intended command" from a genuine pipeline is ambiguous, and the existing generic path handles those reasonably.
Seven new Rust compressors
| Tool | What's preserved |
|---|---|
| prettier | Formatting issue list ([warn] / [error] lines), warning count, file failures; drops "Code style issues found" boilerplate |
| ruff | Rule violation lines (F401, E501, etc.), file:line:col anchors, summary count |
| mypy | Type errors with file:line:col, error notes, "Found N errors in M files" |
| go test | --- FAIL: ... blocks, panic traces, file:line diff markers, package summaries |
| golangci-lint | Per-linter issue blocks, summary, exit reason |
| playwright | Failure stack traces, test titles, retry blocks; drops per-step pass output |
| next (build/lint) | Errors, warnings with code-frame, route info; drops compilation progress lines |
Each compressor caps catastrophic output (1000+ errors) at sensible limits so a runaway run still fits the inline output cap, with a +N more... trailer.
Seven new TOML filters
| Tool | What it does |
|---|---|
| deno | Drops Download / Check progress, keeps errors and summary |
| pip install | Shortcircuits "Requirement already satisfied" runs to a one-line summary |
| uv install | Same shape as pip — collapses noop runs, keeps errors |
| aws cli | Caps verbose JSON output, keeps errors |
| psql | Caps long table output, keeps errors and notices |
| curl / wget | Drops progress bars, keeps response status and errors |
bun test chained-command output preservation
When bun test is part of a chain like bun test && bun run build or bun test; echo done, the v0.28.2 pass-only path was keeping only the bun header and summary — silently dropping the chained command's output.
v0.29 detects the Ran N tests across M files. [Xms] boundary line (both singular file. and plural files. forms) and preserves everything after it verbatim. Chained commands' output is no longer lost.
aft_navigate LAST_SESSION fallback
Between-request Rust events (watcher invalidation, gitignore matcher rebuilds, semantic-refresh) now inherit the most-recently-known session id on the same thread, so plugin logs read correctly even when the event isn't tied to an active tool dispatch. Previously these lines emitted untagged.
Coverage
This release adds ~600 lines of new compressor logic, +51 unit and integration tests, +12 dispatch specificity tests, and +2 chained-output regression tests — for a total of 2,635 Rust tests in the workspace, all green.
v0.28.2
AFT v0.28.2 is a community contribution release: four PRs from Chris Olszewski ship together — oxfmt formatter, tsgo type checker, high-dimensional embeddings (closes #49), and dotted custom LSP root markers — plus a fix for repeated LSP servers from lsp.servers overrides (closes #56), a Windows LSP auto-install fix, and a new bun test output compressor.
Thank you Chris! 🎉
New formatter: oxfmt
A new formatter: "oxfmt" option enables oxfmt (by the oxc-project — same family as oxlint AFT already supports). AFT auto-detects oxfmt projects via .oxfmtrc.json and routes JavaScript / TypeScript edits through it.
Priority order when multiple formatters are detected:
- biome (when
biome.jsonexists) - oxfmt (when
.oxfmtrc.jsonexists and no biome config) - prettier (when
.prettierrc*exists) - deno fmt (Deno projects)
Set format.formatter: "oxfmt" explicitly to force it.
Contributed in PR #52.
New type checker: tsgo
A new checker: "tsgo" option enables tsgo — the experimental Go-based TypeScript compiler — as an opt-in replacement for tsc. Output is tsc-compatible, so AFT's existing diagnostic parser works unchanged.
To use it, install @typescript/native-preview in your project and set format.checker: "tsgo". AFT will run tsgo --noEmit --pretty false instead of tsc. The default remains tsc.
Contributed in PR #53.
High-dimensional semantic embeddings (closes #49)
The semantic search embedding-dimension cap is raised from 1024 → 4096, with comprehensive validation. This unblocks:
- OpenAI
text-embedding-3-largeat the maximum 3072 dimensions - Common open-weight 4096-dimensional embedding models for local hosting
Dimension validation is now centralized in validate_embedding_dimension(), with consistent rejection of zero / over-cap values at both index build and disk-cache deserialization. Reported and fixed by the same contributor in #49 and PR #55.
Dotted custom LSP root markers (PR #51)
lsp.servers configuration entries with dotted root markers like ".clangd", ".oxlintrc.json", or ".env" were having the leading dot silently stripped by config parsing — so root-marker matching failed to find dot-prefixed configs and any custom server with a .config-style file was effectively unconfigurable.
The fix scopes the leading-dot strip to extensions only (where it's correct, because Path::extension() returns extensions without the dot). args and root_markers are now passed through verbatim. Contributed in PR #51.
lsp.servers overrides now merge with built-ins (closes #56)
Configuring a built-in server through lsp.servers — for example, lsp.servers.clangd: { args: ["--query-driver=...", ...] } — would previously spawn two clangd servers per .cpp file: the original built-in and a parallel Custom("clangd") instance from the user config. Each emitted diagnostics independently and competed for the same project.
v0.28.2 detects when a user's lsp.servers entry has an id matching a built-in (clangd, pyright, rust_analyzer, etc.) and merges them: the user's fields take precedence, and any field the user left empty inherits from the built-in. The result is exactly one spawned server per file with the configuration the user asked for.
This means common workflows like "I want clangd but with extra args" or "I want pyright but with my own initialization options" now work the way they read. Reported in #56.
Windows: LSP auto-install no longer fails with ENOENT
LSP auto-install on Windows was failing with:
ENOENT: no such file or directory, uv_spawn 'npm'
The cause: Node's child_process.spawn on Windows does not auto-resolve .cmd shim extensions, and npm is shipped as npm.cmd on Windows GitHub runners and most user machines. AFT was passing the bare "npm" string to spawn, which only resolves a binary literally named npm (no extension).
v0.28.2 selects npm.cmd on win32 and npm everywhere else. LSP auto-install on Windows now works.
bun test output now keeps failure details
Until v0.28.2, bun test output fell through to the generic output compressor, which middle-truncates large captures. Bun writes the failure block — error message, source pointer, expected/received diff, stack trace, and the explicit (fail) ... marker — between the header and the final summary, so a real test run could routinely lose the only thing the agent needs to debug.
A new compressor preserves failure blocks verbatim while dropping per-file pass-only section headers, regardless of how big the run is. Catastrophic failure counts (1000+ failed tests) are capped at 25 preserved blocks with a +N more failures trailer so the inline output cap stays honest.
v0.28.1
AFT v0.28.1 fixes a CLI install break, a TUI plugin-load crash, and the duplicate background-bash completion-message bug, plus several quality-of-life improvements for the new TUI sidebar and LSP notifications.
npx @cortexkit/aft fixed
The v0.28.0 release of @cortexkit/aft (the unified CLI behind aft setup and aft doctor) shipped with a literal "workspace:*" reference to @cortexkit/aft-bridge in its package.json dependencies. npm rejects that protocol on install with:
npm error code EUNSUPPORTEDPROTOCOL
npm error Unsupported URL Type "workspace:": workspace:*
The leak only affected @cortexkit/aft@0.28.0; the other npm packages were unaffected. Running npx @cortexkit/aft@latest setup or aft doctor from a clean machine was broken throughout v0.28.0.
v0.28.1 fixes scripts/version-sync.mjs so the CLI's aft-bridge dependency is rewritten to a real semver at publish time, matching how the plugin packages already work. @cortexkit/aft@0.28.1 and onward install cleanly under both npm and Bun.
If you saw EUNSUPPORTEDPROTOCOL after running setup or doctor with v0.28.0, just run npx @cortexkit/aft@latest doctor again — it will pull v0.28.1 and work normally.
OpenCode TUI plugin no longer crashes on load
OpenCode TUI runs the plugin under Bun. v0.28.0 transitively pulled in undici@8, which calls webidl.util.markAsUncloneable at module-load time — a Web IDL helper Bun does not currently polyfill. The result was an immediate plugin-load crash:
error=__require is not a function failed to load plugin
Users on OpenCode Desktop (Node runtime) were unaffected; users on the TUI silently lost all AFT tools.
v0.28.1 pins undici@7, which calls the missing helper lazily and never triggers the crash. AFT loads correctly under Bun TUI again.
Background bash completion duplicate-message bug worked around
The most disruptive bug since v0.27: every background-bash completion reminder could spawn multiple final assistant stop messages in OpenCode, wasting tokens and confusing the conversation. Traced to anomalyco/opencode#28202 — OpenCode's in-process plugin client uses a separate Effect memoMap from the live request listener, so SessionRunState.ensureRunning cannot coalesce AFT-initiated wake requests against the live session.
v0.28.1 routes background-bash wake reminders through a separate createOpencodeClient pointed at the live HTTP server URL. The wake hits the same Effect runtime as user-driven turns, so OpenCode's coalescing fires correctly and exactly one assistant child responds.
The workaround works in both runtimes:
- OpenCode Desktop: works automatically with no user action.
- OpenCode TUI: requires launching with
opencode --port 0so the in-process server is reachable over loopback. If you start TUI without--port 0, AFT shows a one-time ignored message explaining the situation and linking to the upstream issue.
Wake reminders still preserve the assistant's model and variant on synthetic prompts, so provider prefix caches stay warm.
TUI sidebar populates correctly with --port 0
A side effect of OpenCode TUI under --port 0 is that it loads the AFT plugin twice in the same process (separate ESM module graphs, separate BridgePool, two RPC servers). Pre-v0.28.1 the two plugin instances overwrote each other's RPC port file last-write-wins, so the /aft-status sidebar could end up polling the plugin instance whose bridge was cold and just kept showing the lazy-spawn placeholder.
v0.28.1 changes the RPC port layout from a single port file to a ports/<instance-id>.json directory, with one file per plugin instance. The TUI sidebar reads every entry, probes each port, and prefers whichever instance's bridge has warmed up. The old port file is still read as a fallback for older plugin versions in mixed deployments.
After this fix, the sidebar populates within ~1.5s of the first AFT tool call, regardless of which plugin instance the tool was routed through.
lsp.auto_install: false actually silences missing-binary warnings
Users with lsp.auto_install: false in their aft.jsonc were still receiving repeated lsp_binary_missing warnings on every configure — both as TUI toasts and Desktop ignored-message notifications — for built-in LSP servers AFT would have installed if the flag had been left on.
The root cause was in the plugin layer: both OpenCode and Pi sent Rust the full list of auto-installable binaries via lsp_auto_install_binaries regardless of the user's preference. The Rust-side detect_missing_lsp_binaries walk then warned for any of those that weren't on disk.
v0.28.1 makes both plugins send an empty lsp_auto_install_binaries list when lsp.auto_install: false. The Rust walk short-circuits and emits no built-in warnings. Explicit lsp.servers entries you've configured yourself are unaffected and continue to warn when their binary is missing — that's a user-managed surface, not auto.
v0.28.0
AFT v0.28.0 graduates bash hoisting to a stable top-level feature, fixes two GitHub-reported regressions, and ships a native Windows ARM64 binary.
Bash hoisting is now default-on
The bash hoist (rewrite + compression + background execution) graduates from experimental to a stable, default-on capability on the recommended and all tool surfaces. Configure with the new top-level bash block:
{
// Disable AFT bash hoist entirely — falls back to the host's native bash
"bash": false,
// Or selectively turn features off
"bash": {
"rewrite": true, // default true: rewrite cat/sed/find/etc. to AFT tools
"compress": true, // default true: compress git, cargo, eslint, etc. output
"background": true // default true: foreground >5s auto-promotes to background
}
}Old experimental.bash.* config migrates automatically on first launch:
experimental.bash.rewrite: true→ top-levelbash: { rewrite: true, compress: false, background: false }(exact pre-graduation semantics preserved — missing keys defaulted to false in the legacy schema, so the migration materializes all three keys explicitly)- Tuning-only blocks like
experimental.bash: { long_running_reminder_interval_ms: 5000 }migrate without enabling feature flags - The migrated
aft.jsoncis rewritten on disk with the new shape, comments preserved
To opt out entirely: set bash: false at the top level. The migration also runs in-memory for read-only filesystems.
Vue, Astro, and Svelte language servers now auto-install (GitHub #48)
The npm auto-install table already shipped @vue/language-server, @astrojs/language-server, and svelte-language-server in v0.17.0, but they never installed in real projects because their server specs had no rootMarkers — auto-install fell back to a bounded extension walk (depth 4, 200 directories) that often missed .vue / .astro / .svelte files in monorepos or src/ subtree layouts. Users opening a Vue file got "configured ... but not installed" warnings.
Detection now reads package.json directly:
- Vue auto-installs when
package.jsonlistsvueornuxtas a dep, or whenvue.config.*/nuxt.config.*is present - Astro auto-installs when
astrois a dep orastro.config.*is present - Svelte auto-installs when
svelteor@sveltejs/kitis a dep orsvelte.config.*is present
Vite-based Vue projects (no vue.config.* file) and Nuxt 3 projects (no nuxt.config under the walk budget) now auto-install correctly.
Honest formatter/checker resolution (GitHub #47)
When AFT was launched from a GUI (Spotlight, Dock, Alfred), its inherited PATH did not include /opt/homebrew/bin or ~/.cargo/bin even when tools like go, rustfmt, or biome were installed there. The previous warning text claimed the tool was "not installed," sending users on a fix-the-install goose chase.
Two changes:
format/checkerresolution now falls through to well-known install locations when PATH lookup fails:/opt/homebrew/bin,/usr/local/bin,$HOME/.cargo/bin,$HOME/go/bin,$HOME/.local/bin. AFT uses the resolved absolute path so the spawned tool works regardless of inherited PATH.- Warnings now read "configured ... but was not found on PATH or in common install locations" — and
goinstall hints point at the Homebrew bin directory.
Windows is unaffected (these are POSIX paths).
Native Windows ARM64 binary
Windows ARM64 hosts previously ran the win32-x64 binary under emulation. v0.28.0 ships a real aarch64-pc-windows-msvc build as @cortexkit/aft-win32-arm64, and @cortexkit/aft-bridge resolves it natively on ARM64 hosts. Existing Windows x64 users see no change.
Other improvements
aft doctorflags a missing AFT binary as a problem (#46). Previously the missing-binary case printedAFT binary: unknownand still claimed "Everything looks good"; doctor now reports it as actionable, andaft doctor --fixdownloads the correct version through@cortexkit/aft-bridge.- TUI sidebar and
/aft-statusno longer flash placeholder index rows before the first tool call. With lazy bridge spawn, the Search Index, Semantic Index, and Compression sections are hidden until the bridge starts, replaced by a single line: "AFT bridge is now spawned lazily, information here will be populated after first tool call." - Structured trace logs around bash-completion wake delivery.
/tmp/aft-plugin.lognow records sevenbash_completion_*events (wake_scheduled,wake_fire,wake_prompt_async_start/ok/error,ack_ok,in_turn_append) with a per-delivery UUID and a sha256 of the reminder body. The full reminder is never logged. These tracking points exist solely to diagnose the rare case where a background-bash reminder triggers more than one model run for the same parent message; behavior is unchanged.
Upgrade notes
- Bash hoist is now active by default on the
recommendedsurface. If you don't want it, set"bash": falseinaft.jsonc. - Old
experimental.bash.*config keys migrate automatically on first launch; the rewrittenaft.jsoncpreserves your existing semantics. - The new Vue/Astro/Svelte auto-install only triggers when
lsp.auto_install: true(the default).
v0.27.1
AFT v0.27.1 fixes two GitHub-reported regressions and ships several polish items on top of v0.27.0.
Fixes
OpenCode LSP auto-install now uses npm (GitHub #46)
@vue/language-server and other newer LSP servers were never installing for users without bun on PATH, even with lsp.auto_install: true. The OpenCode plugin previously spawned bun add to install LSP packages, which silently ENOENT-failed when bun wasn't available. By the next configure, the failed binary was no longer in flight, so Rust correctly emitted Install vue-language-server and ensure it is on PATH — and the cycle repeated on every startup.
OpenCode now spawns npm install --no-save instead, matching the Pi plugin's existing behavior. npm is guaranteed to be present whenever the plugin reaches the user through the standard OpenCode distribution flows. After upgrading, missing LSP servers like Vue, Astro, and Svelte install automatically on next startup and the recurring warning disappears.
Honest no_op reporting for byte-identical writes (GitHub #45)
edit and write calls that resulted in byte-identical file content (e.g. oldString === newString, or a formatter that normalized the change back to the original) reported +0/-0 with no explanation, which agents read as "the tool is broken."
Rust now sets no_op: true on the response whenever the post-write file matches the pre-write state. The match was satisfied (replacements: 1), but agents and the TUI now see an explicit note:
- Pi sidebar/dialog renders
+0/-0 (no net change)in muted text instead of bare+0/-0 - Pi tool result text appends
no net file change — newString may be identical to oldString or formatting normalized the result - OpenCode
edit/writeoutput appends the same explanation
Applies across all 4 write/edit paths: find/replace, append, symbol replace, full-file write.
Per-project bridge config in OpenCode Desktop
OpenCode Desktop runs one plugin instance for many open projects. The plugin previously loaded AFT config once at startup from whichever directory OpenCode happened to launch from, then froze it for every project. If you opened a session in Project A whose .opencode/aft.jsonc set bash.background: false, the bridge for Project A still used Project B's config because Project B's was the one loaded at init.
Per-project AFT config now loads at bridge spawn time. Project-scoped fields take effect immediately when opening a session in that project:
experimental.bash.{rewrite, compress, background},experimental.lsp_tyformat_on_edit,formatter_timeout_secs,validate_on_edit,formatter/checkerper-languagerestrict_to_project_root,search_index,semantic_searchmax_callgraph_files,semantic.*, project-safelsp.*fields
Plugin-init-time decisions (tool_surface, disabled_tools, hoist_builtin_tools, ONNX runtime setup) stay global because they affect tool registration, not per-bridge configure. Pi is one process per session so this never affected Pi.
Polish
v0.27 startup announcement
OpenCode and Pi now surface a once-per-version dialog summarizing v0.27's headline changes — CortexKit storage migration, SQLite-backed bash task state, bash output compression rendering in /aft-status, and the new Discord link — so users who skipped the v0.27.0 release notes still see what changed.
JSON Schema for aft.jsonc
aft setup now writes a $schema URL pointing at assets/aft.schema.json in fresh aft.jsonc files, so editors with JSON Schema support (VS Code, IntelliJ, neovim with jsonls) get autocomplete and inline validation for AFT config keys. Existing config files are unchanged.
Quieter logs
Two routine plugin-log lines were demoted from INFO to DEBUG/silent:
- The per-bash
compression event recorded for ... (N → M tokens)line, which fires on every bash invocation - The
bash task replay DB miss for session __default__; falling back to diskline, which fires once per cold-start on a session-less configure (eager warm, anonymous protocol calls) and means nothing actionable
WARN remains for real lookup errors. Migration paths still log INFO when disk actually returns tasks worth surfacing.
Upgrade notes
No config changes required. Existing project bridges keep working unchanged. The npm-vs-bun LSP install fix takes effect on next plugin startup. The per-project config fix takes effect on next bridge spawn for each project.
v0.27.0
v0.27.0
AFT now has a single shared CortexKit storage root with SQLite-backed state, end-to-end bash compression accounting visible in the status UI, and seven new tree-sitter grammars. The CortexKit migration runs once on first launch and is a one-way move; expect a brief "AFT is migrating its data" message during that first start.
New storage root: CortexKit
All AFT persistent state has moved out of the per-harness opencode/storage/plugin/aft/ and pi/storage/plugin/aft/ directories into a single shared root at ~/.local/share/cortexkit/aft/ (or %APPDATA%\cortexkit\aft\ on Windows). Search indexes, semantic indexes, symbol caches, backups, ONNX Runtime, bash task spill files, and the RPC port directory are all unified under one path that both @cortexkit/aft-opencode and @cortexkit/aft-pi share.
On first launch, the plugin spawns a blocking aft migrate-storage step that moves and merges your existing legacy data into the new layout. The migration is content-hash safe (no data is duplicated), idempotent (re-running is a no-op), and shows a brief "AFT is migrating its data" notice in OpenCode while it runs. Typical migration finishes in seconds; large semantic indexes can take up to a minute.
SQLite-backed bash tasks, backups, and state
Bash task records, aft_safety backup history, the warned_tools notification dedupe, and migration markers now live in cortexkit/aft/db/aft.sqlite alongside the existing JSON/disk files (dual-write during v0.27 for safety; DB becomes the source of truth in a future release). The schema is versioned and migrations run automatically on configure.
Practical effect: bash_status lookups now work across bridge restarts, session changes, and concurrent project layouts that the JSON-only paths could miss.
Bash output compression now accounted
The bash compression pipeline that's been compressing tool output since v0.22 was completely invisible — there was no way to see whether it was actually saving tokens or how much. v0.27 adds:
-
A new SQLite
compression_eventstable that records original vs compressed token counts per terminal bash task, keyed on harness + session + task_id (idempotent insert). -
Aggregates surfaced in the
statusresponse and rendered in OpenCode sidebar,/aft-statusdialog, and Pi status overlay:Compression Session Tokens Saved 6,419 Compression Ratio 19% Project Tokens Saved 7,026 Compression Ratio 20% -
Tokenization uses a Claude-compatible BPE tokenizer ported from
ai-tokenizer(newaft-tokenizercrate, ~7ms per 128KiB output, linear scaling). -
Foreground bash with
notify_on_completion=false(the OpenCode/Pibashtool default) now records compression events — previously this path skipped the database write entirely, so >99% of real-world bash usage contributed zero to the aggregate. This was the single biggest gap in the compression telemetry. -
Large outputs that exceed the in-memory cap now tokenize the most recent tail bytes instead of being silently skipped, so build logs, test runs, and other high-volume bash tasks contribute their full reduction count.
Seven new tree-sitter grammars
aft_outline, aft_zoom, and ast_grep_search/ast_grep_replace now work on Java, Ruby, Kotlin, Swift, PHP, Lua, and Perl. Total grammar count: 23. Each grammar ships a hand-written symbol query verified against the installed crate's actual node-type names, an extract function that handles classes/interfaces/methods/functions/fields with proper scope chains, and integration coverage for outline behavior.
Filesystem locks
A new fs_lock module provides crash-safe filesystem locks used by the migration runner, cache writers, and concurrent worktree initialization paths. Replaces ad-hoc lock files that could leak across crashes. No agent-visible change; matters if you ran into "lock file already exists" errors after an AFT process was force-killed.
Pi — restrict_to_project_root now respected for external-directory prompts
When restrict_to_project_root: false was set in aft.jsonc, Pi's hoisted write/edit/grep tools were still showing a ui.confirm dialog on every absolute or out-of-project path. The flag's documented intent is "don't gate operations on project membership", so the per-call dialog defeated its purpose. The prompt now fires only when the user opts INTO restriction (restrict_to_project_root: true); the Pi default (false, for parity with Pi's built-in tools) skips the dialog entirely.
v0.26.4
Fixes
OpenCode — bounded session.messages calls
AFT's wake-up and status-cleanup paths previously called client.session.messages() without a query.limit, causing OpenCode to hydrate the entire session into memory. On long sessions (30k+ messages, 100k+ parts) this could exhaust host memory. Both call sites now request only the recent tail (50 messages).
OpenCode — lazy bridge spawn
AFT no longer spawns an aft bridge for every project loaded in OpenCode Desktop's sidebar at startup. Bridges now spawn lazily on the first tool call against a project, so opening Desktop with many projects in the sidebar no longer multiplies AFT bridge processes for projects you never interact with.
/aft-status and the TUI sidebar show "Waiting for first tool call to populate" until a bridge actually exists.
