June 23, 2026

v1.0.0-beta: unified discovery + ACT loop-guard

Tagging v1

Tagging v1.0.0-beta. The headline rework unifies find_tools and find_skills on a single shared escalation cascade inside SearchableAbility._discover: strip stopwords → exact name/title match (pinned, outside the cap; ambiguous bare names are refused and surfaced) → bm25 on name/title only via a trigram FTS5 index → vector KNN over full prose at the fine-tuned ceiling → honest not_found. The first rung that yields wins per row. find_skills inherits the cascade verbatim and keeps only what is its own — playbook-content payload and the index-health probe. Two single-purpose indexes replace the old hybrid: names/titles → bm25 (FTS5 trigram), full prose → vec0 KNN.

Earlier in the arc, the merged hybrid (RRF + 0.075 relevance floor) was replaced with a keyword grammar and independent retrievers. Queries are +term/-term/bare; build_keyword_query() is the single chokepoint that emits a quoted FTS5 MATCH string and the positive-only text for embedding. FTS5 was rebuilt on the trigram tokenizer with the name as a keyword-only row, so +docs matches chalie_docs directly. RRF_K, rrf_merge, _hybrid_search, _fts_only_search and the floor are gone — keyword and vector run independently, top results are deduped (keyword first) and returned together. abilities.sqlite/skills.sqlite were rebuilt and the ability SHA sidecar now folds in the name. Net −224 LOC for that pass.

The global 6-result backstop was removed. The union is already deduped by the existing dict.fromkeys pass and _append_active dedups against active_tools, so the only invariant left is the per-entry _RESULT_CAP. find_tools/find_skills query descriptions were trimmed to the contract shape — the discoverable roster and usage guidance already live in the tool description (get_summary). The find_tools roster migration to the tool description is query-first. Routing collisions were also resolved: bash now owns ssh into server/machine and file_permissions’ ssh-key example was generalised to change permissions on a file. Net −669 LOC across the unification pass.

A new parametrized feature test drives the real run() against the real abilities.sqlite and real ONNX embeddings with zero mocks, asserting that implicit prose queries a model actually emits (no +/- markers, no tool name) surface chalie_docs via the independent vector path. It failed on the pre-rework RRF+floor code with injected=0 and passes now — pinning the regression that triggered the rework.

file_write had a stuck-loop bug: the read-required guard queried tool_calls by the input row id (proc._uid), but reads are recorded under mp.anchor — the assistant step row, which diverges from _uid after the first step of a turn. Reads on later steps never satisfied the guard, so the model could loop indefinitely on read-required. Fixed by querying reads via ActTrail.fetch_by_turn(channel, turn_id) so any read in the turn clears the guard, and by expanduser()-ing the recorded read path so a ~-path read matches the target. Backstop: ToolDispatcher now appends a one-shot loop-guard steer when a tool returns an error envelope identical to one already on the turn’s trail — the ACT loop has no iteration cap, so this is a soft brake (re-check schema via find_tools, else escalate to the user), not a hard cap. Self-no-op on empty suffix; success path short-circuits before any query. Feature tests drive the real dispatcher with zero mocks: a read under a later step row clears the guard, a ~-path read clears it, and a second identical erroring call carries the steer while the first does not.

A pre-release lint/type pass on rc-0.9.0 guarded HTTPError.response (which can be None) before .status_code in the search fetcher, annotated the unifi login body as dict[str, str | bool], and dropped unused typing imports from telemetry and the seed-thinking test. The message-flow docs were also updated to note the repeat-error loop-guard soft brake.

  • v1.0.0-beta tagged

  • find_tools and find_skills now share one precise→broad cascade: stopwords → exact name → bm25 on name/title → vector on prose → not_found, first rung wins per row

  • Keyword grammar (+/-/bare) replaces RRF + 0.075 floor; FTS5 rebuilt on the trigram tokenizer with names as keyword rows; keyword and vector run independently with no fusion

  • Global 6-result discovery cap removed; only the per-entry _RESULT_CAP remains; find_tools roster moved to the tool description (get_summary)

  • file_write read-guard now queries ActTrail.fetch_by_turn(channel, turn_id) and expanduser()s the recorded read path, fixing the multi-step read-required loop

  • ToolDispatcher appends a one-shot loop-guard steer on repeat errors (soft brake, no iteration cap) — re-check schema via find_tools or escalate to the user

  • Pre-release lint pass: None-safe HTTPError.response, unifi login body typed dict[str, str | bool], unused typing imports dropped

  • Feature tests drive real run() and the real dispatcher against real SQLite + ONNX with zero mocks