## Highlights

### LSP correctness, freshness and isolation

- Post-edit diagnostics now wait for version-proven publishes per server/root key. Stale cached diagnostics from open-time publishes can no longer satisfy a freshness check started after the edit. Workspace-pull diagnostics now respect a wall-clock timeout and cancellation when supported.
- `textDocument/diagnostic` pull responses no longer over-claim `complete: true` when only some servers responded. File-mode push-only freshness is now keyed per-file, so a fresh publish for `a.ts` can't whitewash stale state for `b.ts`.
- `aft doctor lsp <file>` now reports successful inspections correctly instead of printing `lsp_inspect failed` when other server output arrives mid-stream (#34).

### LSP auto-install — supply-chain trust + redirect + bomb resistance

- npm and GitHub auto-installs now write `.aft-installed` metadata (version + sha256) and validate it on every cache hit. Mismatched binaries are quarantined instead of being trusted on path existence alone.
- npm version pins go through safe-version validation. GitHub asset downloads are constrained to a hostname allowlist and follow no redirects. Extracted archives are size-capped (256 MB download, 1 GiB extracted).
- Project config can no longer inject `lsp.servers`, `lsp.versions`, `lsp.auto_install`, `lsp.grace_days`, or `lsp.disabled`. Those keys are now user-config only.
- ZIP extraction on Windows uses direct `tar.exe` instead of shelling out to PowerShell.

### Bash subsystem — permissions, shell selection, kill race

- `bash_permissions` now scans redirect targets including dynamic ones like `echo > $OUTFILE`, so commands like `echo hi > /tmp/foo` correctly ask for `external_directory` permission instead of silently bypassing it.
- POSIX shell resolution honors `$BASH`, falls back to `which(bash)`, then `/bin/sh` — previously hardcoded.
- `bash_kill` now reads the exit marker before setting `Killing` status. If the child finished cleanly between the kill request and the registry update, the real exit code wins.
- Failed-spawn bundle cleanup deletes wrapper/marker files instead of leaving them in the background-task directory.
- Windows bash uses `.bat` wrappers (not `.ps1`) and captures `%ERRORLEVEL%` correctly. Backgrounded-task previews are reconstructed from disk after replay.
- Bash task slug renamed from `bgb-` to `bash-`.

### Subagent bash — no more 5-second auto-promotion

OpenCode subagent sessions (spawned worker turns) no longer convert `background: true` into a background task with no waiting model on the other end. `background: true` is silently converted to `background: false`, and foreground bash polls until the command terminates or its `timeout` fires. Primary sessions keep the existing auto-promotion behavior.

### Search, semantic, configure — atomicity + ordering

- Search-index persistence writes through temp files and atomic rename. Trigram cache no longer drops on partial write.
- Semantic-index refresh is non-destructive and re-detects newly added files. Stale data no longer leaks into the warm cache after the index is invalidated.
- File watcher now respects `.gitignore` rules instead of a hardcoded skip list, so build outputs like `dist/`, `node_modules/`, framework caches don't trigger constant cache invalidation. Live rebuild on `.gitignore` changes.
- Watcher path matching canonicalizes paths to handle macOS `/var` vs `/private/var` and broken symlink chains on Linux.

### aft-bridge — transport + ONNX install + pool

- NDJSON stream uses `StringDecoder` for safe multi-byte UTF-8 handling. Bridge timeouts reject sibling pending requests with an explicit abort error before killing the process.
- `checkVersion()` hard-fails on `success: false` or missing version instead of being silently swallowed.
- ONNX install splits cleanup into a pre-lock staging-dir sweep (cleans abandoned attempts by dead PIDs) and a post-lock target verification. Failed copies hard-fail and remove the partial install. Symlinks are recreated after the real files.
- Bridge pool LRU cleanup skips bridges with pending requests instead of killing in-flight work.
- `BridgePool` and `BinaryBridge` accept a `logger?: Logger` option for per-instance logger override.

### Plugin orchestration

- Background-bash completion wake-ups now preserve `{ providerID, modelID, variant }` from the last real assistant message so synthetic prompts don't bust the provider's prefix cache.
- `grep` and `glob` now ask `external_directory` permission for out-of-project paths, with brace-aware include splitting.
- Windows path normalization matches OpenCode's native handling so AFT-submitted patterns work with granular `~/projects/personal/**`-style permission rules.
- `bash_status` lookup falls back to disk when the in-memory registry has been cleared by a bridge restart. Persisted task GC deletes delivered-terminal tasks and quarantines corrupt JSON.

### Formatter timeout — honor it for shell-launched hanging tools

When a configured formatter hung (deadlocked linter, stuck network probe, etc.), `formatter_timeout_secs` could silently turn into the natural exit time of the underlying process. The timeout path killed only the immediate child; orphaned grandchildren kept the stdout/stderr pipes open, and the wait blocked until they exited on their own. On Unix, the child now spawns in its own session and the timeout path kills the entire process group, so `formatter_timeout_secs` is enforced as advertised.

### Code-symbol editing

- TypeScript / JavaScript / TSX `aft_edit { mode: "symbol", operation: "replace" }` for `export` declarations now includes the leading `export` keyword in the symbol range. Replacements that themselves contain `export` no longer produce `export export function foo() {}` and get rolled back.
- Pi UI for `aft_zoom` now renders the zoom result instead of showing `No zoom result available` for single-symbol calls.

### Removed `dryRun` from mutation tools (kept on `ast_grep_replace`)

`aft_import`, `aft_refactor`, `aft_transform`, `write`, `edit`, `apply_patch` no longer accept `dryRun: true`. Use `aft_safety checkpoint` and `aft_safety undo` for rollback. `ast_grep_replace` keeps `dryRun` because workspace-wide AST replacement genuinely benefits from a preview pass.

**Full Changelog:** https://github.com/cortexkit/aft/compare/v0.21.0...v0.22.0
