Skip to content

Releases: cortexkit/aft

v0.30.1

24 May 16:24

Choose a tag to compare

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.

  • ptyRows and ptyCols are now soft-ignored when pty is unset or false. The dimensions are only applied when a PTY is actually requested.
  • pty: true now implies background: 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/ptyCols clarify they apply only when pty: 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 the lsp.servers.<name>.binary path).
  • 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 install stderr is captured on failure for diagnostic visibility.
  • --ignore-scripts is now passed to the install (matches the LSP install hardening).

Other

  • aft_outline/aft_zoom URL fetch keeps the 15-second body-stall safety net that landed in v0.30.0.
  • Subagent sessions continue to silently convert background: true bash to foreground (introduced in v0.30.0), because subagents have no completion-reminder mechanism.

v0.30.0

24 May 09:53

Choose a tag to compare

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.

Yo dawg, I heard you like OpenCode so I put an OpenCode inside your OpenCode

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 keep bash_status snapshots 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, f1f12, and ctrl-a through ctrl-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:

Syncbash_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.

Asyncbash_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

  • bash schema rejects pty: true without background: true and ptyRows/ptyCols without pty: true.
  • OpenCode subagent sessions silently convert background: true to foreground bash unless bash.subagent_background = true in config.
  • bash_status and bash_kill are always registered when bash is registered (no longer gated on experimental.bash.background).
  • Background bash completion delivery now persists completion_delivered across plugin restarts, so previously-delivered tasks no longer replay as fresh reminders after restart.
  • Async bash_watch exit notifications render as task X exited instead of the prior matched "exited (exit 0)" framing.
  • The release script blocks minor-version releases when the in-plugin ANNOUNCEMENT_VERSION is stale relative to the release tag.

v0.29.1

23 May 03:22

Choose a tag to compare

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 0 TUI): uses the workaround, no duplicate runs
  • Live server unreachable (plain TUI): falls back to the in-process promptAsync so 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

22 May 15:33

Choose a tag to compare

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 test
  • env FOO=bar npm install
  • timeout 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

22 May 09:54

Choose a tag to compare

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:

  1. biome (when biome.json exists)
  2. oxfmt (when .oxfmtrc.json exists and no biome config)
  3. prettier (when .prettierrc* exists)
  4. 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-large at 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

22 May 04:48

Choose a tag to compare

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 0 so 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

21 May 17:28

Choose a tag to compare

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-level bash: { 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.jsonc is 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.json lists vue or nuxt as a dep, or when vue.config.* / nuxt.config.* is present
  • Astro auto-installs when astro is a dep or astro.config.* is present
  • Svelte auto-installs when svelte or @sveltejs/kit is a dep or svelte.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:

  1. format/checker resolution 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.
  2. Warnings now read "configured ... but was not found on PATH or in common install locations" — and go install 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 doctor flags a missing AFT binary as a problem (#46). Previously the missing-binary case printed AFT binary: unknown and still claimed "Everything looks good"; doctor now reports it as actionable, and aft doctor --fix downloads the correct version through @cortexkit/aft-bridge.
  • TUI sidebar and /aft-status no 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.log now records seven bash_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 recommended surface. If you don't want it, set "bash": false in aft.jsonc.
  • Old experimental.bash.* config keys migrate automatically on first launch; the rewritten aft.jsonc preserves your existing semantics.
  • The new Vue/Astro/Svelte auto-install only triggers when lsp.auto_install: true (the default).

v0.27.1

21 May 05:11

Choose a tag to compare

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 / write output 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_ty
  • format_on_edit, formatter_timeout_secs, validate_on_edit, formatter / checker per-language
  • restrict_to_project_root, search_index, semantic_search
  • max_callgraph_files, semantic.*, project-safe lsp.* 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 disk line, 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

20 May 18:06

Choose a tag to compare

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_events table that records original vs compressed token counts per terminal bash task, keyed on harness + session + task_id (idempotent insert).

  • Aggregates surfaced in the status response and rendered in OpenCode sidebar, /aft-status dialog, 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 (new aft-tokenizer crate, ~7ms per 128KiB output, linear scaling).

  • Foreground bash with notify_on_completion=false (the OpenCode/Pi bash tool 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

19 May 04:36

Choose a tag to compare

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.