Releases: cortexkit/aft
Release list
v0.17.2
v0.17.1
Highlights
This release closes the four supply-chain audit findings disclosed in v0.17.0's known-issues, fixes #24, and adds the auto-update hook OpenCode plugin users have been asking for.
New: aft doctor lsp <file>
#24 reported lsp_diagnostics returning total: 0 with no actionable feedback when an LSP server couldn't spawn — a misconfigured LSP looks identical to a clean file.
The new CLI subcommand explains exactly what AFT's LSP layer would do for any file:
$ bunx --bun @cortexkit/aft doctor lsp ./python/dns_editor/__init__.py
LSP inspection — ./python/dns_editor/__init__.py
Resolved file: /repo/python/dns_editor/__init__.py
File extension: .py
Project root: /repo
Active config: experimental_lsp_ty=true, disabled_lsp=python
Server attempts:
✗ ty
Binary: ty (NOT FOUND on PATH or in lsp_paths_extra)
Workspace root: /repo/python (markers: requirements.txt)
Status: binary not installed
Action: Install with `uv tool install ty` or `pip install ty`.
The corresponding lsp_diagnostics tool description in both OpenCode and Pi plugins now points agents at this command when results look unhelpful, and explicitly distinguishes "nothing was checked" (no server registered) from "checked clean".
Pass --harness opencode|pi to disambiguate when both plugins are installed.
New: Auto-update checker (OpenCode plugin)
OpenCode stopped automatically refreshing the cached @cortexkit/aft-opencode package even when users had @latest in their config.plugin entry. Without action, every patch we shipped stranded users on whatever version was current when they first installed.
The new auto-update hook (modeled on oh-my-opencode-slim's pattern) runs once per OpenCode session start:
- Local dev (file:// or relative path) → toast and skip
- Pinned version (e.g.
@0.16.5) → notify-only ("v0.17.1 available; update your config") @latest+auto_update: true(default) → fetch from npm, install viabun add, toast success/failure@latest+auto_update: false→ notify-only
User-facing config field auto_update (snake_case to match the rest of AFT config) added to ~/.config/opencode/aft.jsonc. User-only via the strict trust-boundary allowlist — a hostile project repo cannot silently disable security update notifications.
Supply-chain hardening (closes v0.17.0 known issues)
The four follow-up items disclosed at v0.17.0 ship are now closed:
ONNX Runtime download path hardened to LSP installer parity
The previous implementation used curl with no size cap, no archive containment validation, no install lock, and no integrity verification. This rewrite brings ONNX onto the same security floor as the v0.17.0 LSP GitHub installer:
- Streaming size cap via
fetch+ReadableStreamtransformer (256 MB) - Streaming SHA-256 of the downloaded archive, persisted in
.aft-onnx-installed - Atomic
O_EXCLinstall lock with PID-aware stale-lock recovery - Containment-checked extraction: every entry validated against staging root, no symlinks pointing outside, post-extraction byte cap at 1 GiB (decompression bomb defense)
- TOFU verification on subsequent loads — refuse to use a tampered binary if the recorded hash no longer matches; falls back to system install when the cached binary fails verification
releaseInstallLock PID ownership check (TOCTOU defense)
Read the PID from the lock file and only unlink if it matches process.pid. Catches ENOENT silently for the common vanish-between-stat-and-unlink race. Fixes the failure mode where process A's lock got reclaimed by B due to STALE_LOCK_MS, then A's finally block deleted B's freshly-created lock and broke mutual exclusion. Both OpenCode and Pi plugins.
assertSafeVersion validation on cached latest_eligible
New isSafeVersion() predicate (non-throwing variant of assertSafeVersion) wired into npm and GitHub install paths, both plugins. Disk corruption or a future bug that wrote a shell-injectable version into the version-check cache cannot flow into bun add or download URLs anymore — unsafe cache is treated as cache miss, forcing a fresh registry/release probe.
GitHub browser_download_url hostname allowlist
assertAllowedDownloadUrl() called at the top of downloadFile in both plugins. Allowlist covers github.com, api.github.com, objects.githubusercontent.com, release-assets.githubusercontent.com, raw.githubusercontent.com, codeload.github.com. Rejects http downgrade, file://, attacker-controlled hosts, subdomain confusion (github.com.evil.example), and unparseable URLs. Hostname matched case-insensitively.
Tests
- 1,550 total (811 Rust + 739 TypeScript), 0 failures
- +44 new tests since v0.17.0 covering the audit-3 fixes (TOCTOU defense, URL allowlist × 2 plugins, TOFU),
lsp_inspectintegration, CLI rendering, auto-update checker (438 OpenCode tests now), and theauto_updatetrust-boundary regression test
Verifying the install
After upgrading, the auto-update checker will keep your @latest install fresh on the next session. To verify the new diagnostic command:
bunx --bun @cortexkit/aft doctor lsp <some-file-in-your-project>
Issue #24 closed.
v0.17.0
Highlights
LSP auto-install — AFT now installs language servers automatically when your project needs them. No more "Pyright not found on PATH" errors. Servers download lazily, cache per user, and resolve through a layered binary lookup chain.
- npm path for Pyright, BashLS, YAML LS, etc. — installed via
bun add --ignore-scriptsinto a managed cache - GitHub path for clangd, Lua LS, ZLS, tinymist, texlab — release archives downloaded, validated, extracted, and pinned by tag
- Layered resolver: project
node_modules/.bin→ AFT cache →PATH - Smart relevance: only installs servers for languages your project actually uses
- Per-server pinning via
lsp.versions: { "<package>": "<version>" } - Disabling via
lsp.disabled: ["<server-id>"] - doctor can inspect, list, and clear the LSP cache (
@cortexkit/aft doctor --clear)
Security hardening
This release ran through three independent multi-model security audits. Notable defenses landed:
- Strict trust boundary between user config and project config. Project
.opencode/aft.jsoncand.pi/aft.jsonccan no longer override security-relevant fields (restrict_to_project_root,url_fetch_allow_private,storage_dir,max_callgraph_files,lsp.*,semantic.*). A hostile repository cannot weaken your supply-chain defenses or redirect installs. - 256 MB download cap + 1 GiB extracted-bytes cap on all GitHub release archives, with streaming size verification.
- Archive validation:
lstatSync+realpathSyncwalk rejects symlinks and any path escaping the staging directory before atomicrenameSyncto the cache. - Atomic install lock via
O_EXCL— no concurrent double-claim, even under heavy parallel session usage. Stale-lock recovery usesMath.abs(age)to defend against backward clock skew (NTP correction, sleep/wake on stale RTC). - TOFU verification: SHA-256 of every install (npm + GitHub) recorded in
.aft-installed. Reinstall of the same tag with a different hash is refused — points toaft doctor --clearfor recovery. - Version-pin enforcement:
lsp.versionschanges trigger transparent reinstall instead of being silently ignored when a binary is already cached. assertSafeVersionregex validates user-pinned versions against^[A-Za-z0-9._+-]+$— defense-in-depth against shell metacharacters slipping intobun addor release-tag URLs.- PowerShell removed from Windows ZIP extraction. AFT now invokes
tar.exedirectly viaexecFileSyncargv — no shell parser in the chain. (Windows 10 build 17063+ shipstar.exein System32.) - Windows liveness check skipped in lock recovery —
process.kill(pid, 0)is unreliable on Win32; age-based timeout governs reclamation. - Probe failure handling: stale GitHub release probes return
tag: nullinstead of cached-tag-with-empty-assets, so callers fail loud. grace_daysschema: rejected zero values that bypassed the supply-chain grace window.
Test coverage
- 808 Rust tests
- 402 OpenCode plugin tests
- 257 Pi plugin tests
- 33 aft-cli tests
Total: 1,500 tests passing across the workspace.
v0.16.1
Fixes
lsp.python: "ty"now actually disables Pyright. The plugin was sending"pyright"to Rust'sdisabled_lspset, but Rust matches againstServerKind::id_str()which returns"python"for the built-in Pyright server. Pyright kept running alongside ty. Sending the correct id ("python") now disables it.- README examples and docs corrected to use the actual built-in server ids:
typescript,python,rust,go,bash,yaml,ty. If you havelsp.disabled: ["pyright"]in your config, change it tolsp.disabled: ["python"]. - Stopped poisoning
node:fsacross the test suite.resolver-error.test.tshad a globalmock.module("node:fs", ...)that leaked into every later-loaded test file, breaking notification dedup persistence in CI based on test load order.
Docs
- New Response convention section under Tool Reference covering the tri-state contract (
success: false/success: truewithcomplete: true/success: truewithcomplete: false), gap field names (pending_files,unchecked_files,skipped_files,scope_warnings,walk_truncated,no_files_matched_scope), and approvedformat_skipped_reason/validate_skipped_reasonvalues. aft_outlinenow documentsskipped_filesreporting infilesanddirectorymodes.ast_grep_searchnow documents theno_files_matched_scope+scope_warningsdistinction from "searched and found nothing".aft_import op="remove"now documentsremoved: falsewithmodule_not_found/name_not_foundreasons.
v0.16.0
LSP
New built-in servers
- Bash (
bash-language-server) for.sh,.bash,.zsh - YAML (
yaml-language-server) for.yaml,.yml
Configurable Python LSP
lsp.pythonselects betweenpyright(default) and the experimentaltyserver- Set
experimental_lsp_ty: trueandlsp.python: "ty"to opt in
User-defined LSP servers
The lsp.servers config block accepts custom server definitions:
{
"lsp": {
"servers": [
{
"id": "my-lsp",
"extensions": ["foo"],
"binary": "my-language-server",
"args": ["--stdio"],
"root_markers": ["my.config.json"],
"env": { "MY_LSP_LOG": "info" },
"initialization_options": { "someFlag": true }
}
]
}
}Custom servers participate in lsp_diagnostics and edit-time diagnostic flow alongside the built-ins.
Disabling servers
lsp.disabled accepts a list of server IDs (built-in or custom) to skip:
{ "lsp": { "disabled": ["yaml", "my-lsp"] } }lsp_diagnostics rewrite
Reworked into a focused file/scope check with explicit completeness reporting.
- Per-server, per-workspace, per-file diagnostic state with publish epochs and pull
resultIdtracking - Uses LSP 3.17 pull diagnostics when supported; falls back to push with bounded wait otherwise
- Response includes
lsp_servers_used: [{ server_id, scope, status }]wherestatusis one ofpull_ok,
pull_unchanged,push_only,binary_not_installed: <name>,spawn_failed: ...,no_root_marker (...), or
workspace_pull_unsupported - Files with no registered server return an empty
lsp_servers_usedand anotefield instead oftotal: 0 - Directory mode reports
unchecked_filesandwalk_truncatedfor partial scans - Open documents are resynced from disk if their on-disk
(mtime, size)changed since the last sync - Diagnostic cache is bounded by
lsp.diagnostic_cache_size(default 5000) - This is not a project-wide type checker. Use
tsc --noEmit,cargo check,pyright, etc., for full coverage.
Tool response convention
Tool responses now follow a tri-state contract documented in ARCHITECTURE.md:
success: falsewith acodeandmessagewhen the work could not be performedsuccess: truewithcomplete: truewhen the result is trustworthysuccess: truewithcomplete: falseplus named gap fields (pending_files,unchecked_files,scope_warnings,
skipped_files: [{file, reason}],walk_truncated) when the result is partial
Tool changes
aft_outlinedirectory mode reportsskipped_fileswith per-file reasons (parse_error,unsupported_language)ast_grep_searchandast_grep_replacenow reportno_files_matched_scopeandscope_warningswhen paths or globs
resolve to zero files, instead of returningfiles_searched: 0aft_import op="remove"returnsremoved: falsewithreason: "module_not_found"orreason: "name_not_found"when
there is nothing to remove- Format skip reasons split into
unsupported_language,no_formatter_configured,formatter_not_installed,
timeout,error(the previous sharednot_foundvalue is removed). Equivalent split for checker skip reasons. apply_patchper-file additions/deletions are now sourced from the Rust diff engine; previously they were recomputed
in TypeScript and could over-report on small patches.
Configure warnings
When the Rust binary detects missing tooling at startup, the plugin surfaces a one-time sendIgnoredMessage (Desktop)
or toast (TUI):
- Formatter binary missing for a language with a configured formatter
- Checker binary missing for a language with a configured checker
- LSP server binary missing for an enabled language
Warnings are deduplicated per-project in <storage_dir>/warned_tools.json and do not re-fire on subsequent sessions for
the same warning.
v0.15.5
Fixed
Patch tool no longer emits whole-file diffs (issue #22)
apply_patch, edit, and write were sending bloated metadata diffs that the OpenCode UI rendered as huge diff views — for example, inserting a single line near the top of a 2000-line file produced ~1996 lines of "diff" instead of one localized hunk.
Root cause: the unified-diff helper used a naive line-by-line index comparison. Any insertion or deletion shifted line numbers, so every subsequent line compared unequal and got emitted as -old / +new. The bug shipped in v0.15.3 alongside the apply_patch metadata enrichment that fixed the "no diff in TUI" report.
Fix: replaced with a proper LCS-based diff (O(n*m) DP table, backwards walk to produce ops, hunk grouping with 3-line context and 6-line gap merging). Output matches GNU diff -u layout. No new dependencies.
11 new unit tests cover the exact #22 reproducer plus localized hunks, multi-hunk splitting, hunk merging, file creation/deletion, and the 100KB skip cap.
v0.15.4
Security
restrict_to_project_root now defaults to true for plugin contexts
Both @cortexkit/aft-opencode and @cortexkit/aft-pi now default to restrict_to_project_root: true, which makes write-capable Rust commands reject file paths outside the configured project root. The Rust CLI default stays false for direct/scripted use, where a hard project boundary doesn't make sense.
This is a behavior change. If you intentionally rely on the plugin to edit files outside the OpenCode/Pi project directory (e.g. shared libraries, sibling repos), set restrict_to_project_root: false in ~/.config/opencode/aft.jsonc or ~/.pi/agent/aft.jsonc.
Shell injection in aft doctor --issue
aft doctor --issue shells out to gh issue create to file diagnostic bundles. The previous implementation interpolated the title and repo into a shell command via execSync, so a title containing backticks or $(...) would execute. Switched to spawnSync with an argv array — no shell, no interpolation.
Per-server RPC authentication
The /aft-status RPC server (used by Desktop and TUI) listens on 127.0.0.1 and previously accepted any local request. While listening on loopback already excludes remote attackers, anything that could reach 127.0.0.1 (browsers loading malicious pages, other local processes) could probe AFT's status endpoints.
Now the server generates a 32-byte random token at startup, writes it as {"port": N, "token": "..."} in the per-project port file (mode 0600), and rejects requests without a matching token field with HTTP 403. Legacy integer-only port files from older AFT versions still parse correctly so a new client can interoperate with an old server (and vice versa).
URL-fetch SSRF guard
aft_outline(url=...) and aft_zoom(url=...) previously trusted whatever the resolver returned and followed all redirects. A public URL could redirect to http://127.0.0.1:8080/ or http://[::ffff:127.0.0.1]/ and AFT would happily fetch internal services.
The guard now:
- Looks up the host with
dns.lookupand rejects if any returned address falls in private ranges (RFC1918, loopback, link-local, multicast, AWS metadata, IPv6 ULA / link-local / unspecified). - Handles redirects manually with a 5-hop limit, re-validating the host at every hop.
- Expands IPv6 to 8 hextets to catch the IPv4-mapped (
::ffff:127.0.0.1) and IPv4-compatible (::127.0.0.1) bypass — these forms route to the embedded IPv4 address even though they look like IPv6 to a naive check, and Node's URL parser canonicalizes them in surprising ways ([::ffff:127.0.0.1]becomes[::ffff:7f00:1]).
The Oracle review caught the IPv4-mapped IPv6 case as a real exploitable bypass in the first iteration of this fix; the final implementation expands the address fully and extracts the embedded IPv4 before checking against the IPv4 private list.
aft_conflicts now validates paths against project_root
When restrict_to_project_root: true, every read-capable Rust command must run user-supplied paths through validate_path(). handle_git_conflicts was reading conflicted file paths directly from git ls-files --unmerged without validation, which would have allowed reading files outside the project root if a malicious repo had submodule paths pointing elsewhere. Fixed.
Plugin reliability
Pi aft_zoom multi-symbol regression from v0.15.3
The v0.15.3 Pi session-id refactor routed every tool call through callBridge(extCtx, ...) so each request carries Pi's real session id from extCtx.sessionManager.getSessionId(). One spot was missed: the multi-symbol fan-out path in aft_zoom called bridge.send("zoom", req) directly, bypassing the wrapper. Multi-symbol zoom calls on Pi therefore lost session scoping, which meant their backup/checkpoint state could leak across sessions in the same project.
Fixed — the fan-out now uses callBridge like every other code path.
Pi pool keys bridges by canonical filesystem path
@cortexkit/aft-opencode's pool already canonicalized session directories with realpathSync so /Users/me/proj and /Users/me/proj/. resolved to the same bridge. @cortexkit/aft-pi only stripped trailing slashes, so symlinks and relative-path variations could spawn duplicate bridges with split state — same problem the OpenCode plugin solved before its v0.14 refactor. Pi now matches.
Stdout buffer overflow no longer hangs the bridge
The plugin's stdout reader appended every chunk from the aft binary to an in-memory buffer until it found a newline. A misbehaving binary that wrote multi-GB of output without a newline (network corruption, runaway log loop, fuzzing) would balloon Node memory until the process OOM'd. The buffer is now capped at 64MB; overflow is treated as a bridge crash and triggers a respawn. Both @cortexkit/aft-opencode and @cortexkit/aft-pi got the same cap.
LSP child cleanup on shutdown timeout
If a language server stopped responding to shutdown requests within the 5-second timeout, AFT logged the timeout and moved on — but never killed the child process. That left orphaned tsserver / pyright / rust-analyzer processes consuming CPU and file watches until the OS reaped them. The LSP client now kill()s the child after timeout and again from a Drop impl as a backstop.
Correctness
Glob edit_match is atomic
aft_edit glob mode (filePath: "src/**/*.ts") wrote files in a loop, formatting after each one. If the third write failed (disk full, permission error, sigkill), the first two had already landed and the rest never happened — partial state with no rollback. Now wrapped in a checkpoint snapshot taken before the first write; any failure during the bulk operation restores all matched files.
ast_grep_search and ast_grep_replace surface invalid patterns
A malformed regex or AST pattern previously slipped through to ast-grep's matcher and produced 0 matches with no signal. The agent saw "no matches found" and assumed the pattern just didn't match anything in the codebase. Now the pattern is validated up front and returns an invalid_pattern error so the model can fix the pattern instead of pivoting to a fallback.
Type-checker uses project root, not file's parent directory
When auto_format_or_validate invoked tsc / cargo check / pyright to validate after an edit, the working directory was set to path.parent() of the edited file. That broke project-aware tools: tsc could not find tsconfig.json, cargo check could not find Cargo.toml, validation silently produced wrong diagnostics. Now uses config.project_root, matching the formatter path which had this right already.
Zoom ambiguous-symbol candidates show line ranges
When aft_zoom(symbol="foo") matched multiple symbols, the suggestion list was foo:42, MyClass::foo:120 — start line only. Now it's foo:42-58, MyClass::foo:120-145 so you can see at a glance which match is the function you actually want.
Read directory listings cap at 1000 entries
A read on a directory with hundreds of thousands of files (e.g. node_modules, generated assets) would build a huge response in memory and could time out the bridge. Capped at 1000 entries with a truncation note.
validate_on_edit accepts booleans
The Rust configure handler only accepted the "syntax" | "full" | "off" strings, but the plugin was passing through whatever the user wrote in JSONC config. A user setting validate_on_edit: true got a silent invalid_request error and validation was disabled entirely. Now booleans are accepted (true → "full", false → "off") and the existing string mode keeps working.
lsp_rename and lsp_find_references agree on column convention
lsp_find_references documented character as 0-based and accepted 0. lsp_rename rejected character: 0 with "must be > 0", because its docs implied 1-based. They now both use 1-based, matching every other AFT position parameter and the rest of the agent surface. Internal LSP calls still convert at the protocol boundary.
Workflow
release.ymlhas top-level concurrency control so two release tags pushed in quick succession can't race each other through the publish steps.- The
publish-cratesjob nowneeds: build, gating crates.io on a clean JS build instead of trying to publish if the build was about to fail. - Linux CI uses
sha256suminstead of the BSDshasumperl wrapper that has surprised CI runners in the past.
v0.15.3
Critical fixes
Grep/glob could crash the aft process on actively-changing directories
Every `grep` / `glob` call that touched a directory being modified in parallel (by file watchers, editors, other tools) could panic the aft process with:
user-provided comparison function does not correctly implement a total order
This destroyed warmed LSP state, symbol caches, backups, and checkpoints — the next tool call had to cold-start from scratch. The bug existed in the v0.15.1 production binary.
Root cause: three sort comparators in the search path called `fs::metadata(path)` directly inside `slice::sort_by()`. `stat()` results can vary across invocations for the same path (file deleted mid-sort, mtime updated by a watcher, etc.), which makes the comparator non-deterministic — and Rust's sort panics on detection. Caught by CI on a Pi e2e test where the file-watcher invalidated files in parallel with grep's sort.
Fix: snapshot mtimes once before sorting, look up from the snapshot inside the closure. Pure function ⇒ guaranteed total order.
Semantic index re-embedded ~500 files on every OpenCode restart
Caused a 30-50 second, ~800% CPU spike on every OpenCode restart (reported from real logs against a 508-file project). The semantic on-disk format stored file mtimes as whole seconds, but live mtimes from `fs::metadata().modified()` carry subsecond precision on APFS, ext4-with-nsec, and NTFS. The equality comparison in `is_file_stale()` therefore reported ~99% of files stale on every restart and re-embedded them via fastembed (all CPU cores).
Fix: new V3 on-disk format with `secs + subsec_nanos`, preserving full filesystem precision. V1/V2 caches load one more time (triggers the existing one-time rebuild on upgrade), then persist as V3 and stabilise forever.
The V3 reader also rejects corrupt/malicious caches with out-of-range nanos or overflowing secs instead of panicking `SystemTime + Duration`.
apply_patch now renders diffs in TUI and desktop
The `apply_patch` tool wasn't showing any per-file diffs in either OpenCode TUI or desktop UI — just the summary text. OpenCode's UI silently drops any `metadata.files` entry that doesn't carry a `patch` (or `before`/`after`) field, and AFT was only sending `{ filePath, relativePath, type }`.
Fixed by enriching each file metadata entry with the per-file unified diff, `additions`, `deletions`, and `movePath` (when applicable) — matching OpenCode's built-in `apply_patch` contract exactly.
Config validation
`max_callgraph_files` config rejects non-integer values
`configure` was only validated for positive integers. String, boolean, null, float, array, and object values went through the permissive silent-clamp path and could produce surprising behavior. Now all non-integer payloads are explicitly rejected with `invalid_request`. Regression test added.
Internal
- `scripts/wait-release.sh` now uses newline output and a configurable `--max-wait` flag so agent/CI wrappers see the script terminate immediately on workflow completion instead of waiting their own timeout.
v0.15.1
Bug fixes
Call-graph operations no longer hang when AFT is opened in a huge directory
Opening OpenCode or Pi in a very large root (monorepo top, ~/Work, /home — anything with hundreds of thousands of source files) used to break call-graph navigation. aft_navigate operations like callers, trace_to, trace_data, and impact try to build a reverse index over every source file in the project. On a 557K-file root that walk easily exceeded the 30 s bridge timeout, at which point the plugin SIGKILLed the Rust child — losing warmed LSP state, symbol caches, backups, and checkpoints — and the next tool call had to cold-start from scratch.
AFT now handles oversized projects gracefully with a new max_callgraph_files config knob (default 20000):
- Fast fail instead of timeout. When the project exceeds the cap, the four call-graph commands return a clean
project_too_largeerror in microseconds with a message pointing you to the fix: open a specific subdirectory, or raisemax_callgraph_filesin.opencode/aft.jsonc. - Startup warning.
configurenow reportssource_file_countandsource_file_count_exceeds_maxback to the plugin, which logs a one-time WARN so you see the situation on startup rather than waiting for the first timeout. - Longer budgets for legitimate slow operations. Per-command timeouts let
callers,trace_to,trace_data,impact,grep, andglobuse 60 s;semantic_searchuses 45 s. Small edit/read operations keep the tight 30 s default.
grep, glob, read, edit, outline, zoom, and every other tool are completely unaffected by the cap — those already had their own guards or scale fine.
OpenCode plugin was silently stripping max_callgraph_files from your config
The OpenCode plugin's zod config schema didn't declare max_callgraph_files, so if you set it in .opencode/aft.jsonc the value was stripped during config parsing and the default was used. Fixed — the schema now accepts max_callgraph_files (positive integer), and the plugin forwards it to the Rust bridge. Pi was already forwarding correctly.
configure rejects bad max_callgraph_files values instead of silently fixing them
A bad value like 0, -5, "twenty", or null used to be silently clamped to 1 (or ignored entirely for non-numeric types). Typos would slip through. configure now returns invalid_request with a clear message so you see the problem immediately.
Docs
New "Working with large repositories" section in the README explains the file-count thresholds (20K for call-graph, 10K for semantic indexing, unbounded for everything else) and the remediation paths.
v0.15.0
New — unified `@cortexkit/aft` CLI
Setup, doctor, and issue reporting are now handled by a single harness-agnostic CLI. It auto-detects which harnesses (OpenCode, Pi) you have installed and configures each one.
```bash
bunx --bun @cortexkit/aft setup # register AFT in every installed harness
bunx --bun @cortexkit/aft doctor # per-harness health check with auto-fix
bunx --bun @cortexkit/aft doctor --force # also clear the OpenCode plugin cache
bunx --bun @cortexkit/aft doctor --issue # open a sanitized GitHub issue
```
Add `--harness opencode` or `--harness pi` to any command to target one explicitly.
The old `aft-opencode` CLI has been removed — everything it did now lives in the unified CLI, and doctor gains per-harness sections for ONNX Runtime compatibility, storage sizes, log tail, plugin-registration state, and AFT config parse errors.
Pi — native TUI renderers for every AFT tool
Every AFT tool exposed through the Pi plugin now has a custom renderer. Hoisted `write` and `edit` render unified diffs with Pi-style intra-line highlighting. `aft_outline`, `aft_zoom`, `aft_search`, `aft_navigate`, `aft_conflicts`, `aft_safety`, `aft_import`, `aft_transform`, `aft_refactor`, `ast_grep_search`, `ast_grep_replace`, and `lsp_diagnostics` each render with structured headings, severity badges, and grouped summaries matching Pi's existing visual language. Models also receive prompt snippets and guidelines teaching AFT's `filePath` / `oldString` / `newString` parameter shape directly. (Closes #15)
`aft_safety` — checkpoint tolerates deleted files
The Pi agent's repro from v0.14.1 is now fully covered:
- Checkpoint with `filePath` actually checkpoints that file. Both OpenCode and Pi plugins auto-promote `filePath` → `files: [filePath]` for `op: checkpoint` instead of silently dropping it.
- Checkpoint tolerates deleted files in the tracked-file fallback set. Previously, once any file was deleted in a session, `checkpoint` (without explicit `files`) aborted on the first missing path. Now it skips unreadable files, still snapshots everything it can, and reports them in a new `skipped` field. Explicit `files: [...]` still hard-errors on missing entries, since the agent asked for those specifically.
Upgrade
Most users need nothing — OpenCode and Pi will pick up the new plugin and binary on next session start. If you used `bunx @cortexkit/aft-opencode setup` or `doctor` before, switch to `bunx --bun @cortexkit/aft setup` / `doctor` — the old CLI is gone.
Full Changelog: v0.14.1...v0.15.0