# v0.23.0

## Highlights

### `aft_search` overhaul — better recall, hybrid lexical lane, source provenance

The biggest semantic-search change since the feature was introduced. Three coordinated improvements landed together:

- **Query-shape classifier + per-shape weighting.** The query is now classified as identifier-like (`HashMap`, `useState`), path-like (`src/utils/auth.ts`), error-message-shaped, mixed, or natural-language. Each shape gets a tailored treatment instead of one-size-fits-all cosine ranking.

- **File-summary chunks for small files.** Files with two or fewer top-level exports now get a synthetic file-summary chunk that embeds the path, exported names, and signatures together. Generic-file queries like "where is the rate limiter" used to return zero results when the answer was a short single-export module; now they surface the file directly. Per the new built-in eval harness on this codebase: generic-file P@5 went from `0.000` → `0.333`, identifier P@5 went from `0.600` → `0.800`, overall from `0.607` → `0.750`.

- **Hybrid lexical lane.** A second retrieval lane runs alongside the existing semantic lane and contributes results that exact-token matches the query. Each result now carries a `source` tag — `"semantic"` (embedding match only), `"lexical"` (trigram exact-token match the embedding lane missed), or `"hybrid"` (both lanes agreed — strongest signal). The lexical lane especially helps for path-shaped queries and error messages where embeddings underperform.

The `aft_search` tool description was rewritten around concrete "when to use / when not to use" triggers so agents reach for it for the right shapes of question. Score floor was removed (was suppressing valid 0.30-0.45 hits); Markdown/HTML heading-only chunks no longer outrank code chunks for code-flavored queries.

Bumped semantic-index `chunking_version` to `2`. The old V1 cache deserializes with a serde default and lazily backfills file-summary chunks on the first v0.23 run per project — no manual reindex needed. Total chunk count roughly doubles after backfill (file-summary chunks add one synthetic chunk per qualifying file).

### Resolver: refuse stale `@cortexkit/aft-<platform>` packages on version mismatch

A workspace that upgraded the AFT plugin (e.g. v0.19.5 → v0.22.x) while a stale `@cortexkit/aft-<platform>` was still hoisted in `node_modules` — common with bun's `.bun/install/cache` keeping multiple versions — could see the resolver silently pick the older binary instead of the version-matched cached one. The wrong-version binary still passed basic protocol but emitted pre-rename behavior (in the original repro: `bgb-` task slugs that don't match the plugin's `bash-` regex, producing tool-result mismatches).

The resolver now invokes `--version` on the npm-resolved binary before returning it. If the version doesn't match the plugin's `expectedVersion`, it logs a warning and falls through to PATH lookup so a locally-built or correctly-installed binary can take over. Both plugin entry points now plumb their `PLUGIN_VERSION` explicitly into `findBinary()`.

### Pi v0.74.0 — migrated to `@earendil-works` package scope

Pi's coding-agent project moved from the `@mariozechner/*` npm scope to `@earendil-works/*` as of Pi v0.74.0; the old packages now carry a "please use @earendil-works/pi-coding-agent instead going forward" deprecation notice. AFT's Pi plugin and Pi RPC test harness now declare the new scope directly. Pi v0.74.0 also switched its embedded type-schema runtime from `@sinclair/typebox@0.34` to `typebox@1.x` — AFT's tool definitions migrated alongside it. No agent-visible API changes.

### Pi RPC end-to-end test harness

New `tests/pi-rpc/` workspace with a JSONL RPC client, aimock-driven mock OpenAI-compatible provider, and a real Pi process spawn helper modeled on the magic-context Pi runner pattern. Sixteen scenarios cover hoisted reads, permission asks, semantic search, foreground bash, background bash with completion notifications, and post-completion drain across Pi restarts. Wired into the reusable E2E workflow so CI now blocks on Pi behavior the same way it blocks on OpenCode.

The harness already paid for itself during this release cycle: it caught the resolver version-mismatch bug above and a real Pi-side bug where `drainCompletions` bailed entirely when the RPC envelope omitted `sessionID` (Pi's RPC mode does not always send one). Pi now forwards an empty params object to Rust so the binary uses its `__default__` session namespace and the drain still works.

### Permission asks: Pi external-directory parity, OpenCode subagent grep parity

- Pi's hoisted `read`, `write`, `edit`, `apply_patch`, `grep`, and `glob` now ask `external_directory` permission for paths outside the project root, mirroring OpenCode's behavior. Previously Pi would silently allow reads/writes anywhere on the filesystem when the agent passed an absolute out-of-project path.
- OpenCode hoisted `grep` now asks `external_directory` permission too. Already present on `read`/`write`/`edit`/etc, but `grep` had been left out — agents could still read sensitive files outside the project via repeated grep calls.

## Fixes

### `apply_patch` rolls back surviving files when one hunk fails

Previously, `apply_patch` would commit successful per-file changes even when a later hunk in the same patch failed. The combined effect was a partial application that left the workspace half-edited. Now: any hunk failure rolls back the entire patch atomically using the existing checkpoint/restore path.

### Parser: TypeScript `export` symbol range no longer leaks across replacements

`aft_edit { mode: "symbol", operation: "replace" }` for `export function foo() {...}` now includes the leading `export` keyword in the symbol range, so a replacement string that itself starts with `export` no longer produces `export export function foo() {}` and get rolled back.

### `aft doctor lsp <file>` no longer mistakes push frames for the response

The CLI's NDJSON request matcher was indexing responses by arrival order, so a `configure_warnings` push frame arriving between request and response would be treated as the `lsp_inspect` response and the actual response would be discarded. Matcher now keys by request `id` and skips push frames, fixing [#34](https://github.com/cortexkit/aft/issues/34).

### Cleanup of `dryRun` dead branches in 16 Rust command files

The `dryRun` removal in v0.22.0 took the parameter off the agent-facing schemas but left dead branches in command handler code. Cleaned up across `aft_import`, `aft_refactor`, `aft_transform`, `write`, `edit`, `apply_patch`, and related batch helpers. `ast_grep_replace` still supports `dryRun` (workspace-wide AST replacement legitimately benefits from a preview pass).

### Session-id threading in Rust log lines

Per-request Rust log lines now carry the originating `[ses_xxx]` session prefix when the request supplied one. Maintenance and watcher events that are not session-scoped (file invalidation, symbol cache pre-warm, configure-time setup) intentionally remain untagged. Helps correlate semantic refreshes, checkpoints, and format runs back to the triggering session when debugging.

### Smaller things

- `aft_zoom` Pi rendering: improved single-symbol display.
- `aft_outline` URL fetching: better content-type negotiation (HTML, Markdown, GitHub README API media types).
- Plugin auto-update checker: reduced log noise on startup; cross-instance dedup honored on disk for plugin developers running multiple OpenCode windows.

## Known issues (planned for v0.24)

- **Cache-reuse across worktrees**: when the same git repo is checked out at multiple paths (e.g. parallel worker worktrees), each worktree currently builds its own semantic / search / symbol caches even though git root commit is shared. The v0.24 plan (already spec'd) adds content-hash freshness fallback and migrates semantic to relative paths so worktrees can share the same cache.
- **Lock contention** when multiple bridges spawn simultaneously for the same project root logs `failed to acquire semantic cache lock: timed out`. Cosmetic — they all converge to the correct state — but should be cleaned up alongside the cache-reuse work.

**Full Changelog:** https://github.com/cortexkit/aft/compare/v0.22.1...v0.23.0
