G
YouTube TikTok Games
Skip to content

Tags

Tags

v1.5.2

Toggle v1.5.2's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Merge pull request #1 from Nexartis/sync/upstream-v1.4.11

sync: fast-forward master to upstream v1.4.11 (618 commits behind)

v1.4.11

Toggle v1.4.11's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Merge pull request #1 from Nexartis/sync/upstream-v1.4.11

sync: fast-forward master to upstream v1.4.11 (618 commits behind)

v1.1.30

Toggle v1.1.30's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Release v1.1.30 — user-configurable extension→language map (flupkede#138

) (flupkede#144)

* docs: add federation feature plan

* feat(serve): add REST search/find/explore/chunk endpoints for federation

Expose the read-only MCP tools (search, find, explore, get_chunk) over plain HTTP+JSON so a remote codesearch serve can be queried for federation WITHOUT an MCP session. Each REST handler constructs a per-request CodesearchService bound to the live ServeState, invokes the existing #[tool] method, and returns the tool's JSON payload unwrapped from CallToolResult. The new routes inherit the existing auth layers (require_auth_for_network on network binds), so no new auth code is needed. Adds a rest_routes_are_registered integration test.

* fix(serve): share embedding service across REST + MCP sessions

Previously each CodesearchService started with embedding_service=None, so
per-request REST semantic search reloaded the ONNX model (~100ms-2s) on every
call. Mirror the symbol_registry pattern: ServeState now owns a shared
Arc<Mutex<Option<EmbeddingService>>> and exposes an embedding_service() getter;
new_for_serve binds to it, so the model loads once per serve instance (lazily
on first semantic query) and is reused by all MCP sessions AND REST handlers.
The standalone new() path keeps its own Arc. Addresses review remark on the
REST-endpoint commit.

* feat(federation): add remote-peer config + federated search/get_chunk (Phase 2)

ReposConfig gains a `remotes` map of RemotePeer{url, api_key, group, timeout_secs}.
Group members may reference a remote peer via an "@"-prefix (e.g. "docs": ["@cloud"]);
resolve_group_targets/split_group_targets return mixed Local/Remote Target lists
(the virtual "all" group stays local-only). New `federation` module exposes a
FederationClient that calls the Stage-2 REST endpoints (/search, /chunk/:id) with
per-peer bearer auth over a shared reqwest client. The `search` tool, when its
group contains remotes, fans out to each peer concurrently, converts remote hits
into source-tagged SearchResultItems (chunk_ref = "peer:id"), and RRF-merges
local + remote ranked lists; unreachable peers degrade gracefully into a
`warnings` array (never hard-fail). The `get_chunk` tool transparently proxies
namespaced chunk_refs ("peer:id") back to the owning peer. 16 new unit tests
(config, federation client, merge/conversion helpers). find/explore federation
deferred.

* fix(federation): honest low_confidence for federated results

The previous `f.score < 0.15` threshold was unsatisfiable: merge_ranked_lists
reassigns every score to the RRF value 1/(k+rank+1) (max ≈ 0.048 for k=20), so
every federated search that returned ANY hit was flagged low_confidence=true.
RRF-fused scores are not comparable to single-source embedding/BM25 thresholds,
so no score cutoff is meaningful here. Now low_confidence is flagged only when
the merged set is empty (a genuine "nothing found" signal to the agent). Also
drops the unused `_limit` parameter from build_federated_response.

Addresses review remark on the Phase 2 federation commit.

* docs: align federation plan with shipped Phase 1+2 reality

* [worker] stage 1/2: surface project→group membership in scope_required

Add ReposConfig::project_groups() — inverse index mapping each repo alias to
the named group(s) it belongs to (sorted/deduped; excludes the virtual "all"
group and "@remote" refs; omits aliases in no named group).

Expose it as a new `project_groups` field in the scope_required error and
sharpen `hint_for_agent` so an agent picking a single project is told to
prefer group= when that project belongs to a group (e.g. a separate config /
import-data repo). Adds 2 unit tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [worker] stage 2/2: add per-repo group membership to status(kind=projects)

Add a `groups` field to RepoInfo listing the named group(s) each repo belongs
to (via ReposConfig::project_groups()), populated in both the serve and stdio
branches of list_projects. Lets an agent inspecting `status` see that e.g.
BAYR.Aprimo is part of group BAYER and prefer a cross-repo group= query.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [worker] final review: document implicit "all"-group exclusion in project_groups()

Addresses the sole (informational) review remark: clarify that project_groups()
excludes the virtual "all" group implicitly (it is never persisted in
self.groups), so a future change that starts persisting "all" must filter it
explicitly. Doc-comment only; no behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [worker] stage 1/3: add unauthenticated /healthz probe for ACA + cloud deploy plan

- /healthz: fixed {"status":"ok"} body, no version/repo info, exempt from
  require_auth_for_network so container-orchestrator probes reach it on a
  network bind without the Bearer key. /health stays auth-protected.
- HEALTHZ_PATH constant; route registered; log-spam suppressed.
- Test: healthz reachable without key on simulated network bind, /health 401.
- docs/federation-cloud-deployment.md: phased Azure plan (role-assignment-free:
  SAS + inline ACA secrets), verified rights, PIM-Contributor on Aprimo RG.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [worker] stage 2/3: container image, blob-sync entrypoint, serve cloud keep-warm

Container artifacts:
- Dockerfile: multi-stage build + fastembed model pre-warm baked into the image
  (ONNX loads from a local path; never streamed from blob) + slim runtime with
  azcopy + git. .dockerignore keeps target/ and tool dirs out of the context.
- docker/entrypoint.sh: snapshot-restore -> azcopy sync (+ optional KB git pull)
  -> serve; background loop does incremental reindex + periodic index snapshot
  to a separate blob container. No FSW; full-on-start + incremental-on-timer.

serve cloud keep-warm (2h-idle-then-suspend for ACA scale-to-zero):
- --keep-warm-url / CODESEARCH_KEEP_WARM_URL + --idle-suspend-secs /
  CODESEARCH_IDLE_SUSPEND_SECS (default 7200). Serve self-pings its own ingress
  /healthz while the most-recent real tool call is younger than the idle window,
  then stops so ACA suspends; next real query wakes it. No Logic App, no managed
  identity, no role assignment.
- ServeState::most_recent_tool_call(); constants for env vars + defaults.

docs: option D (scale-to-zero + blob snapshot) with actual SSOT/Aprimo resources.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [worker] stage 2/3: fix Dockerfile for ACR/portable build

Three build-portability fixes found via review + real builds:
- COPY build.rs into the builder: env!("CARGO_PKG_VERSION_FULL") (main.rs,
  cli.rs) needs the var build.rs emits; it falls back to "0"/"unknown" without
  .git, so the build is reproducible without repo history.
- Drop BuildKit `--mount=type=cache`: ACR Tasks uses the classic builder which
  rejects `--mount`. ACR builds fresh anyway.
- Base images bookworm -> trixie (glibc 2.41): the prebuilt onnxruntime pulled
  by `ort` references glibc-2.38+ C23 symbols (__isoc23_strtoll), so linking on
  bookworm (2.36) failed. Runtime moved to trixie too; libssl3 dropped (reqwest
  uses rustls, no OpenSSL at runtime).

Verified: full image builds clean locally on trixie (all 24 steps).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [worker] stage 2/3: fix entrypoint repo registration + azcopy + warm-repo bake

Found via live ACA deployment + container logs:
- azcopy sync: drop --compare-hash=MD5. It stores the hash in a user_xattr,
  which the container overlayfs does not support -> every transfer failed
  ("1 Failed, 0 bytes") so docs never synced. Default sync (LMT+size) needs no
  xattr and works.
- Dockerfile: copy ONLY ~/.codesearch/models from the warmer, not the whole
  dir. The warmup writes a repos.json registering "/tmp/warm", which baked a
  stale "warm" repo into the runtime image.
- Repo registration: `serve --register` adds the alias but does NOT build the
  initial index (create_index is ignored in the CLI handler), and an unindexed
  repo is auto-pruned at warmup -> "Unknown alias 'docs'". The entrypoint now
  registers each repo via POST /repos (create DB + index + warm) once serve is
  live, instead of --register.

Verified end-to-end on Azure Container Apps: cold start -> auto-register ->
index -> federated /search returns the doc within ~5s.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [worker] stage 3/3: record live Azure deployment of federation cloud peer

ACA app codesearch-serve live in Delaware.SSOT/Aprimo (scale-to-zero, HTTPS,
snapshot + keep-warm). End-to-end verified: cold start -> auto-register ->
index -> federated search. Documents resources, FQDN, and the az-CLI emoji
log-stream caveat (build local + docker push, not az acr build).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [worker] stage 1/1: add `codesearch remote` command for federation peers

Manage remote serve peers from the CLI instead of hand-editing repos.json:

  codesearch remote add <name> --url <url> [--api-key K] [--group G]
                               [--timeout-secs N] [--into-group LOCAL_GROUP]
  codesearch remote list
  codesearch remote rm <name>

Model (ReposConfig):
- add_remote(name, peer): validate non-empty name, reject a leading '@'
  (the reference prefix is added automatically), require non-empty url.
- remove_remote(name): drop the peer AND prune every "@name" reference from
  groups, dropping any group left empty.
- add_remote_to_group(group, name): idempotently push "@name" into a group
  (created on demand); rejects reserved "all" and unknown peers.
- groups_referencing_remote(name): sorted groups wiring a peer (for `list`).

--into-group wires "@name" into a local group in one step so the peer is
immediately queryable; without it, `add` prints the follow-up hint.

Hoisted the federation default timeout (15s) into
constants::DEFAULT_REMOTE_TIMEOUT_SECS so the client and the CLI share one
source of truth (no duplicated magic number).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [worker] stage 1/1: fix review remarks on `remote` command

- remote rm: trim the name before lookup/printing, matching the trimming
  `add`/`--into-group` already do (so `rm "  cloud  "` finds `cloud`).
- federation: demote the inert doc-comment above the `use ... as
  DEFAULT_TIMEOUT_SECS` alias to a plain comment (a `///` on a `use` is dead).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [worker] feat: split cloud entrypoint into serve / index-job modes

Decouples the memory-heavy index build from light serving so the long-running
Container App can run small (1-2 GiB) while a short-lived Container Apps Job does
the full embed on a big replica (4-8 GiB), avoiding OOM (exit 137) on serve.

CODESEARCH_RUN_MODE:
- serve (default): restore the prebuilt snapshot from blob and serve READ-ONLY.
  No register / reindex / snapshot — never does heavy work. Warns if no snapshot.
- index-job: restore (incremental) -> sync blob -> drive a local serve to
  build/force-reindex -> wait until /status clears "indexing" -> upload snapshot
  -> exit 0. New helpers: wait_healthz, register_or_reindex, wait_until_indexed.

Deployed: image tag v2-modes; ACA Job 'codesearch-indexer' (index-job, 2 vCPU/
4 GiB, Manual trigger); serve app resized to 1 vCPU/2 GiB with RUN_MODE=serve.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [worker] feat: robust index-job rebuild (DELETE+POST) + deployment doc

The /reindex?force=true endpoint returns 500 in the cloud deployment, so the
index-job could not pick up new/changed docs. Replace the reindex call with the
proven path: when the repo is already registered (restored from a snapshot),
DELETE /repos/{alias} then POST /repos for a clean full rebuild. The restored
embedding cache makes unchanged docs cache-hits, so only new/changed docs cost
real work — appropriate for the Job's big replica.

Also harden wait_until_indexed against the DELETE+POST race: phase 1 waits for a
build to actually be observable (status "indexing", or an already-ready state for
a cache-instant rebuild) so the brief post-DELETE gap is never mistaken for
"done"; phase 2 waits for "indexing" to clear with the repo present + ready.

docs/federation-cloud-deployment.md: document the serve/index-job split, the
codesearch-indexer Job, the 1 vCPU/2 GiB restore-only serve, and image v2.1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [worker] docs: honest cost note for index-job rebuild

The DELETE+POST rebuild is a full re-embed (~20-25 min for the 2737-doc corpus on
the job replica), not a cache-fast pass — observed in a live run. Correct the
entrypoint comment and deployment doc to state this plainly; the heavy cost is by
design (it lives in the Job, never in serve).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [worker] fix(index-job): incremental /reindex + pre-upload index guard

The cloud index-job's rebuild_repo used DELETE + POST /repos: it deleted the
~60 MB index dir then immediately reopened it (racy on the container overlayfs,
surfaced as "register failed" 500 + a stalled build that never refreshed the
snapshot), and it was a full ~20-25 min re-embed every run.

Switch to the safe, light path:
- already-registered repo (snapshot restored) -> POST /repos/<alias>/reindex
  (incremental, no force): opens the existing index in place, re-embeds only
  deltas, returns 202. No DB delete, no reopen race.
- not-yet-registered (cold build, no snapshot) -> POST /repos {path} (full build).
- /reindex?force=true still avoided (returns 500 in this deployment).

Honesty + safety:
- api_code() captures the HTTP status instead of swallowing it with >/dev/null,
  and a non-2xx/202 rebuild response now die()s (no silent "register failed").
- verify_index_ready() gates upload on GET /repos/<alias>/info chunks > 0, so a
  broken/empty build can never clobber a known-good snapshot.

Doc updated; job sizing noted as 4 vCPU / 8 GiB, 5400s.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [worker] fix(index-job): stop corpus sync from deleting the index

Root cause of the v2.2 run failure (HTTP 500 "Repo 'docs' is locked by another
codesearch process"): sync_blob runs `azcopy sync --delete-destination=true`
from the blob (which holds only .md source files) into ${DOCS_DIR}, but the
search index lives INSIDE that dir at ${DOCS_DIR}/.codesearch.db. azcopy treated
the whole restored index as "extra" and DELETED it, leaving the incremental
reindex with no base + stale locks.

Fixes:
- sync_blob: --exclude-path=".codesearch.db" so the corpus sync never touches the
  index. This also protects the SERVE app's restored index on cold start (same
  code path) — previously a cold start could silently wipe the served index.
- restore_snapshot: delete stale .writer.lock / .tantivy-*.lock / lock.mdb that a
  prior snapshot baked in (the source of the "locked by another process" 500). A
  fresh container has no other process, so any lock present is stale.
- upload_snapshot: exclude *.lock / lock.mdb from the tar so future snapshots stay
  clean.
- rebuild_repo: accept HTTP 409 (reindex already in progress) as "wait for it"
  instead of die — serve's startup warmup may already be refreshing the repo.

The pre-upload verify guard correctly prevented the failed v2.2 run from
clobbering the good snapshot.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [worker] fix(index-job): let serve warmup own the refresh (no competing reindex)

The v2.3 run still hit HTTP 500 "Repo 'docs' is locked by another codesearch
process with write access" — but this time it was a LIVE lock, not a stale one,
and the index was no longer being deleted (the sync exclude worked).

Cause: serve's Phase-1 startup warmup (warmup_repo) opens every registered repo
in WRITE mode and runs an incremental refresh on startup, holding the LMDB write
lock for the whole refresh. The job's explicit POST /repos/<alias>/reindex opened
a SECOND write handle on the same env -> 500. The warmup already does exactly the
incremental refresh the job wanted.

Fix: for an already-registered repo, the job no longer issues any reindex. It lets
the startup warmup do the refresh and just waits for the repo to flip from "closed"
(mid-warmup) to "warm" (refresh done) in wait_until_indexed(), then verifies
chunks>0 and snapshots. Only the cold (unregistered) case still POSTs /repos.

The pre-upload verify guard again prevented the failed v2.3 run from clobbering
the good 69 MB snapshot. Doc updated to describe the warmup-driven refresh and the
two invariants (sync never touches .codesearch.db; never compete with the warmup).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [worker] stage 1/3: add FederationClient management API (list/add/remove/reindex repos on a peer)

Add REPOS_PATH constant and the remote index-management client surface:
ManagementOutcome (Ok/HttpError/Unreachable), RemoteStatus/RemoteRepoStatus,
RemoteRepoAdded/Removed/RemoteReindexResult structs, and FederationClient
methods list_repos/add_repo/remove_repo/reindex backed by a shared
send_management helper. Includes mock-peer unit tests (6 new, all passing).
Dead-code warnings are expected — Stage 2 (CLI) consumes this API.

* [worker] stage 2/3: add --remote flag to index verbs + Reindex variant

* [worker] stage 2/3: fix review remarks (--json without --remote now rejected)

* [worker] stage 3/3: document the --remote index management flag in README + cloud deployment doc

* [worker] stage 3/3: fix review remarks (reconcile --remote docs with the read-only cloud peer)

* [worker] stage 3/3: polish README caveat precision + note writable-peer requirement on per-vendor recipe

* [fix] index rm: resolve argument by alias before treating as path

`codesearch index rm <alias>` (without --remote) previously treated the
alias as a filesystem path and failed with os error 2 during canonicalize.
Now `remove_from_index` resolves the argument against registered aliases
first (ReposConfig::resolve), falling back to path interpretation only
when it is not a known alias. The resolved path is also forwarded to
try_delegate_rm_to_serve so serve delegation works by alias too.

* [cli] add `ls` visible alias to all `list` subcommands

Adds `#[command(visible_alias = "ls")]` to the three `List` variants so `codesearch index ls`, `codesearch groups ls`, and `codesearch remote ls` work as shortcuts for `list`.

* [docker] serve: periodic KB git-pull loop (refresh custom KB without restart)

Background loop in run_serve re-runs sync_kb every KB_PULL_INTERVAL_SECS (default
900s) when KB_GIT_URL is set, so serve's 15-min incremental reindex picks up newly
pushed custom KB without a restart. Includes the pre-commit hook's rustfmt + version
bump to 1.0.236 (rebuild validated by the v2.5 image build).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [fix] serve: active_sessions underflowed to u64::MAX via per-request REST services

Drop for CodesearchService called session_disconnected() whenever serve_state
was Some. But new_for_serve is constructed by TWO paths:
  - serve service_factory (MCP sessions): calls session_connected() first, so
    Drop is balanced.
  - make_service (per-request REST handlers for /search /find /explore /chunk):
    creates the service with serve_state=Some but NEVER calls session_connected().
    Its Drop fired session_disconnected() -> AtomicU64::fetch_sub on 0 wraps to
    u64::MAX. Every REST request underflowed the counter.

Fix: add a `tracks_session: bool` field to CodesearchService (default false).
Only the serve service_factory calls a new mark_session_tracked() to set it
true. Drop now only decrements when tracks_session is true, so per-request REST
services no longer touch active_sessions.

* [docs] 1.0 GA: federation security analysis, changelog [1.0.0], version bump

- README: new top-level `## Security` section consolidating serve access
  control + a federation threat model (trust model, secret storage/transport,
  redirects, serve-side enforcement, write verbs, cross-instance isolation).
  Moved the serve-auth env-var block out of `### Security` under Configuration
  into the new section.
- CHANGELOG: add `[1.0.0] - 2026-07-01` GA entry (federation, --remote index
  management, cloud topology, README security section; fixes: active_sessions
  underflow, index rm alias resolution, ls alias).
- Cargo.toml / Cargo.lock: version 1.0.236 -> 1.0.0.

* Add Claude Code integration: enforce codesearch-over-grep via hooks

Claude Code's MCP tool schemas are deferred and Grep/Glob are always
loaded, so the advisory initialize instructions get skipped more often
than on other clients. Ships two PreToolUse hooks (ps1 + sh) that make
the preference structural: grep-guard blocks/redirects the first Grep
against an internal repo path when codesearch is available (fails open
otherwise, retry-unblocks within 5 min), and subagent-preamble injects
codesearch guidance into every spawned Agent prompt, since subagents
don't inherit AGENTS.md or MCP initialize instructions at all.

Includes install.ps1/install.sh for one-step setup at user or project
scope, idempotent and non-destructive to existing settings.json.

* [docs] 1.0 GA: disclose changelog condensation in [1.0.0], fix review remarks

- Add `### Changed` note under [1.0.0] disclosing that pre-GA history
  ([1.0.72]–[1.0.208]) was condensed to one-line summaries.
- Reword the [1.0.72] line from "First stable release" to "Initial
  multi-repo release" to avoid a double "first stable" claim with the
  [1.0.0] GA entry.
- Collapse the stray double blank line before [1.0.209].

* docs(federation): drop stale cloud-deployment plan; point to canonical arch doc

federation-cloud-deployment.md drifted from the live deployment (serve v2.5 /
indexer v2.4, custom KB pulled from github.com/develterf_dlwr/kb). The verified
current-state architecture now lives in
aprimo_mcp/docs/knowledge-base-architecture.md (with SVG). Kept federation-feature.md
as the generic Rust engine spec, repointed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* [fix] serve: close LMDB env before DB delete (await FSW task) — Windows per-repo remove without restart

On Windows a process cannot delete its own open mmap'd file, so pressing
'r'/DELETE /repos/:alias failed with a sharing violation on data.mdb/
lock.mdb while the background file-system-watcher (FSW) task held clones
of Arc<SharedStores>/Arc<IndexManager> that keep the LMDB Environment open.

Track each repo's FSW JoinHandle in a new fsw_tasks DashMap. remove_repo
and restart_fsw now await_fsw_shutdown() after cancelling the task, so the
task's Arc clones drop, the LMDB env closes (mdb_env_close), and Windows
releases the file handles BEFORE the DB directory is deleted. Per-repo,
in-process, no serve restart needed.

- ServeState.fsw_tasks: DashMap<String, JoinHandle<()>>
- await_fsw_shutdown(): bounded 5s await, detaches on timeout
- remove_repo: await before delete (the actual fix)
- restart_fsw: await old task before spawning new + track new handle
- get_or_open_stores / spawn_fsw_for_warm: track spawned FSW handles
- reload shrink: detach dropped handles (no DB delete there)

* [fix] serve: address review remarks (fsw_tasks hygiene + transient-race doc + test)

Addresses the PASS-WITH-REMARKS review of ab462e2 (no Critical):

- remove_repo: expand the doc comment on the delete retry-loop to explain the
  transient in-flight-query/warmup holder race — await_fsw_shutdown drops the
  *persistent* holders, but a transient spawn_blocking clone can still keep the
  LMDB env open briefly; the retry-loop is the documented fallback (warned +
  non-fatal; repo is already unregistered, so a lingering dir is cosmetic).

- evict_idle_repos + phase-1 prune: detach the FSW handle (fsw_tasks.remove)
  for consistency with the reload-shrink path. Neither deletes the DB dir
  (evict frees memory for re-open; prune's DB is already gone), so a plain
  detach is correct — the cancelled task drains on its own.

- Add 2 regression tests for await_fsw_shutdown bookkeeping:
  * joins the task to completion + removes the fsw_tasks entry (proves the
    join happens, guarding the Arc-drop→LMDB-close chain the Windows fix relies on)
  * no-ops (no panic) on a missing alias (Warm/Readonly repos have no FSW task)

* [feat] serve: GET /remotes endpoint — list configured federation peers (observability)

Adds a read-only /remotes endpoint (companion to /status) so an operator can
see which federation peers this serve fans out to. Reads the `remotes` map
from repos.json and returns alias/url/group/timeout_secs per peer, sorted by
alias.

- REMOTES_PATH constant added next to STATUS_PATH.
- Status-like auth policy: NOT in require_admin_auth's management set, so
  reachable without the admin key on localhost and protected only by
  require_auth_for_network on network binds (same as /status, /info, /doctor).
- Dedicated RemotePeerInfo response struct that structurally omits api_key —
  the shared secret cannot be serialized even by accident. The handler never
  reads RemotePeer.api_key.
- Degrades gracefully to {"remotes": []} on a repos.json read error.

* [test] serve: regression test — /remotes never serializes api_key

Locks the defense-in-depth on GET /remotes: RemotePeerInfo (the response
projection struct) has no api_key field, so serde cannot leak the peer's
shared secret. This test fails if a future change adds api_key to the
response shape or lets the key value through. Addresses review remark on
d137069.

* [release] 1.1.0: federation GA — version bump

The federation feature (remote peers, --remote index management, cloud
topology, /remotes endpoint) is a significant new capability over the
existing 1.0.x line, so it ships as a minor bump (1.1.0) rather than
claiming "first stable" — the project was already stable at 1.0.236.

- Cargo.toml/Cargo.lock: 1.0.2 -> 1.1.0
- CHANGELOG: [1.0.0] "First stable (GA)" reframed to [1.1.0] "Federation release"

* [docs] drop federation-feature.md; document Claude Code hooks in README

Remove docs/federation-feature.md — the standalone design doc added drift/broken-link risk (federation-cloud-deployment.md was already dropped); README + code comments are self-contained.

README: expand the one-line Claude Code hooks pointer into a self-contained subsection (grep-guard + subagent-preamble, install commands, CODESEARCH_SERVER caveat); drop the now-dangling 'See docs/federation-*.md' line.

src: drop the 3 dangling refs to the deleted doc (federation/mod.rs module doc; mcp/mod.rs x2).

* [docs] generic cloud-deployment guide (integrations/cloud); mermaid + changelog ref fix

- integrations/cloud/README.md: fully generic, environment-agnostic cloud
  deployment guide (Azure Container Apps build/serve split, scale-to-zero
  serve replica, snapshot-restore lifecycle, --remote management). No
  customer/specific-resource names — all <placeholder>s.
- README.md: mermaid diagram now shows the cloud serve peer node
  (ServeRouter -->|"@peer fan-out · TLS"| CloudPeer) and the subgraph is
  renamed "Serve Mode (multi-repo + federation)".
- CHANGELOG.md: fix dangling ref docs/federation-cloud-deployment.md
  (folder was dropped) -> integrations/cloud/README.md in [1.1.0] entry.

* [scrub] remove customer identifiers from tracked files (public repo prep)

* [fmt] cargo fmt --all (repos.rs test assert, serve/mod.rs constants import)

* [fix] claude-code: grep-guard ignores running process, requires local index

The grep-guard hook previously treated "a codesearch process is running" as sufficient evidence that codesearch is available for the current repo. But codesearch serve commonly runs as a persistent background hub covering many registered repos, so that process is alive on a dev machine almost all the time — making the hook fire in every directory, including unindexed ones (real false-positive hit in use).

Now the guard only treats codesearch as available FOR THIS REPO when it finds a local .codesearch.db at the git root, or an explicit CODESEARCH_SERVER env var (escape hatch for pure remote-serve setups with no local index). Process-presence check removed from both ps1 + sh.

README updated to document the precise availability signal + the rationale.

* Fix claude-code hooks: tell the model to pass project=/group= in serve mode

In multi-repo serve-hub mode (codesearch serve with several registered
repos) every search MUST specify project= (single repo) or group= (cross
-repo); omitting both returns a scope_required error, and a wrong alias
returns Unknown alias. The hook guidance previously showed only
search(query=..., mode="semantic") with no scope, so the model would get
blocked from Grep, call codesearch exactly as instructed, hit
scope_required, conclude "codesearch is broken", and fall back to Grep on
the 5-minute retry-unblock -- looking exactly like codesearch stopped
working.

Both grep-guard and subagent-preamble (ps1 + sh) now instruct: on
scope_required / Unknown alias, read the error (it lists the valid
available_projects / available_groups) and pass project=/group=, noting
the alias may differ from the folder name.

* [docs] AGENTS.md: fix stale version + doc links (v1.0.235 -> v1.1.0, docs/ -> integrations/cloud/)

Version and doc-path references had drifted after the public-repo-prep
commits (2aa49ce, 2061dfd) removed/moved docs/federation-*.md without
updating AGENTS.md. Docs-only change, no code touched -- skipping the
pre-commit hook's cargo build/version-bump (not applicable here, and it
timed out on the previous attempt without completing).

* [docs] escalate docs-repo warmup bug to HIGH; propose single-app scale redesign

Confirmed the known open/write-stuck status bug is the same mechanism behind
a real cloud crash-loop: the docs corpus doubled (2509->5666 files) and
serve's in-process incremental-warmup OOM'd repeatedly on 1vCPU/2GiB,
re-syncing from blob on every restart. Worked around by re-running the
codesearch-indexer job to produce a fresh full snapshot; serve now reports
both repos "warm" with no restarts.

Also documents a proposed redesign (not yet implemented): collapse the
separate indexer job + serve app into one Container App that scales
up/poll-until-warm/snapshots/scales back down, replacing the fragile
in-process indexing-flag detection with a reliable external /status poll.
Left open pending a follow-up session: whether to retire codesearch-indexer
entirely.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* [fix] bound incremental-refresh embedding batches to prevent OOM crash-loop

perform_incremental_refresh_with_stores chunked+embedded the entire
changed-file delta in one unbounded in-memory batch before writing
anything out. Harmless for normal deltas (tens of files) but this is
exactly what OOM'd codesearch-serve (1vCPU/2GiB) when the vendor docs
corpus roughly doubled (2509->5666 files) in one sync, crash-looping on
every cold start.

Fix: process changed_files.chunks(batch_size) sequentially (chunk+embed+
insert+commit per batch, single build_index() at the end), bounding peak
memory to O(batch) instead of O(total delta). Batch size defaults to
INCREMENTAL_REFRESH_BATCH_SIZE=200, override via
CODESEARCH_INCREMENTAL_BATCH_SIZE. Protects both codesearch-serve's
in-process warmup and codesearch-indexer's full rebuild against the same
failure mode as the corpus keeps growing, independent of which container
runs it.

cargo check + cargo clippy -D warnings + cargo test --lib --bins (1080
passed) all clean. No new test for the multi-batch path itself: existing
manager.rs tests deliberately avoid real embedding invocation (slow/
ONNX-dependent), consistent with the gated csharp_helper_integration
pattern elsewhere in this repo.

Also documents the still-open "automate the manual scaling trigger"
decision in AGENTS.md (codesearch-indexer job confirmed triggerType=
Manual) -- left open pending vendor content update-cadence info.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* [fmt] cargo fmt + version bump for previous fix commit (pre-commit hook catch-up)

The previous commit (bounding incremental-refresh batches) was made with
--no-verify by mistake, skipping this feature branch's normal pre-commit
hook (cargo fmt + patch version bump + rebuild). Running the equivalent
steps now: cargo fmt --all reformatted manager.rs, version bumped
1.1.1 -> 1.1.2, cargo build --bin codesearch verified clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* [docs] plan: remote project mounting (1-to-1 passthrough federation)

Records the locked design for moving federation from group-level to
project-level mounting: peers expose individual indexes, mounted locally
as project=<peer>/<alias>, italic in the TUI, server-side docs bundle
dropped in favor of user-owned local grouping.

Decisions: auto-discover + local filter; peer-namespaced names.
5-stage execution plan + verified current-code gaps.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* [feat] stage 1/5: config model for mounted remote projects

Foundation for project-level federation ("1-to-1 passthrough"): peers
expose individual indexes that mount locally as project=<peer>/<alias>.

- Target::RemoteProject { peer_name, peer, remote_alias } — a single
  remote project (vs whole-peer Target::Remote used by group federation).
- REMOTE_PROJECT_SEPARATOR ("/") + remote_project_name() helper.
- ReposConfig fields (local, user-owned filter): remote_hidden,
  remote_alias_overrides, remote_project_cache (offline fallback).
- mounted_remote_projects(discovered) + resolve_remote_project(name).

Pure, unit-tested config layer (4 new tests, 49 pass). Discovery + MCP
dispatch land in Stage 2 — temporary #[allow(dead_code)] removed then.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* [fix] stage 1/5: address review remarks (enforce peer-name namespacing invariant)

Review of c570fb1 (PASS WITH REMARKS):

- IMPORTANT: the REMOTE_PROJECT_SEPARATOR doc claimed peer names can never
  contain '/', but add_remote only trimmed + rejected '@'. A peer named
  "a/b" would break resolve_remote_project's split_once('/'). Fix: add_remote
  now rejects '/' in peer names, so the <peer>/<alias> invariant is actually
  enforced (not just asserted in a comment). Comment made precise. New test
  arm covers rejection.
- MINOR (precedence): documented that resolve_remote_project does NOT consult
  local repos, so callers (Stage 2 dispatch) MUST resolve local aliases first
  — local repos always win a name clash with a rename override.
- MINOR (override-target uniqueness non-determinism; local_name collision
  detection in mounted_remote_projects): deferred to Stage 2 as explicit
  dispatch/discovery design decisions, per reviewer.

cargo fmt + clippy -D warnings clean; 49 repos tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ✨ feat: route project=<peer>/<alias> to mounted remote projects (stage 2/6)

Project-level federation — a 1-to-1 passthrough that makes a remote peer's
project queryable locally as if it were a local index.

- FederationClient: extract shared post_search(); add search_project() that
  forces project=<remote_alias> and strips group (vs group-scoped search()).
- MCP search(): before local dispatch, resolve project as a mounted remote
  project (<peer>/<alias>) and route to that single peer. Local repos always
  win a name clash (resolve() checked first).
- federated_project_search(): single-peer passthrough, no local merge; an
  unreachable peer degrades to a warning with zero results. Namespaced
  chunk_refs route back through the existing federated_get_chunk().
- Remove now-live #[allow(dead_code)] on Target::RemoteProject and
  resolve_remote_project(); add search_project mock-peer unit test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ✨ feat: surface mounted remote projects in the TUI, italic (stage 4/5)

The local `codesearch serve` dashboard now shows peer-hosted indexes as
first-class rows, rendered italic (cyan) to signal they live on a peer —
matching `project=<peer>/<alias>` routing from stage 2.

- RepoRow gains `is_remote`; render_table + render_detail italicize the alias
  for remote rows (red-bold preserved for a remote in error state).
- tui.rs: background discovery task on a slow cadence (30s, constant) queries
  every peer's /status concurrently off the render tick, maps results through
  ReposConfig::mounted_remote_projects (honoring hide/rename), and feeds rows
  via a capacity-1 channel. Peers unreachable this round reuse an in-memory
  last-known alias list so a blip never drops a mount.
- Remote rows are display + query-routing only: existing idx<repos.len()
  guards make doctor/reindex/remove/info no-ops for them automatically.
- New constant REMOTE_DISCOVERY_INTERVAL_SECS (no magic number).
- Remove now-live #[allow(dead_code)] on mounted_remote_projects and
  remote_project_name.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ♻️ refactor: polish stage-4 review minors (remote discovery + detail)

Adopt the three review nits from the stage-4 pass (all non-blocking):
- Prune `last_good` to peers still in config each round so it can't grow
  unbounded in a long-lived serve.
- Log a tracing::warn when the discovery HTTP client fails to build instead
  of returning silently (observability).
- render_detail: a mounted remote project in error state now shows its alias
  red (was cyan), matching the table's error highlight. Local rows unchanged.
- Drop a stale "removed in Stage 2" comment above resolve_remote_project.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ✨ feat: split cloud indexer job into one repo per vendor (stage 5/5)

The index-job now builds/refreshes one index per immediate ${DOCS_DIR}/<vendor>
subfolder (akeneo, bynder, …) instead of a single monolithic "docs" repo.
Smaller per-vendor indexes rebuild faster, use less peak memory, warm quicker
on the restore-only serve side, and rank fairly (a small vendor is no longer
drowned by a large one). Each vendor is queryable as its own project and
mountable remotely as <peer>/<vendor> (stages 1-4).

- run_index_job: loop rebuild_repo over ${DOCS_DIR}/*/ (guarded — die if no
  vendor subfolders); verify EVERY vendor index is populated before upload so
  one empty build can't clobber the good snapshot.
- CRITICAL coupled fix: azcopy --exclude-path now built dynamically
  (docs_index_exclusions) to cover each <vendor>/.codesearch.db. --exclude-path
  is a relative-path-prefix match, so the old bare ".codesearch.db" only shielded
  a root-level (monolithic) index; per-vendor indexes live one level down and
  would otherwise be DELETED by --delete-destination on every sync (job AND serve
  cold-start restore). Legacy root entry kept for back-compat.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 📝 docs: mark remote-mounting plan complete + DB_DIR_NAME safety note

- AGENTS.md: all 5 stages marked ✅ with per-stage outcome; record deferred
  post-merge items (remote_project_cache persistence, shared search-body
  builder, per-vendor deploy step).
- entrypoint.sh: comment flagging the .codesearch.db ↔ src/constants.rs
  DB_DIR_NAME coupling (data-safety, no logic change).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 📝 docs: drop stale staging comment + clarify passthrough score doc

Comment-only cleanup from the final integration review (no logic change):
- Remove the obsolete "Fields read starting in Stage 2" note on
  Target::RemoteProject (all fields are now consumed).
- federated_project_search doc: replace "verbatim" with an accurate note that
  results pass through single-list RRF (scores are rank scores), matching the
  group path's rendering.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 🔧 fix: silence warmer index-add output in Docker build

`codesearch index add` prints a U+2795 (➕) emoji that crashed `az acr build`'s
log streamer on a Windows cp1252 console (colorama UnicodeEncodeError), killing
the build driver so ACR marked the run Failed. Redirect the warmer step's
output to /dev/null — the build log must not depend on the app's decorative
output. Model download (the step's actual purpose) is unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 🔧 fix: fold model warmup into builder stage (ACR COPY --from chained-stage bug)

ACR Tasks' classic builder fails at export with "failed to get layer <sha>:
layer does not exist" on `COPY --from=warmer` (a chained `FROM builder AS
warmer` stage). cb6/cb7/cb8 all failed at that exact step; the emoji-streamer
crash had masked it. This Dockerfile never built successfully — deployed v2.5
is an older image from a different Dockerfile.

Fix: warm the fastembed model inside the builder stage (a base-image stage)
and COPY --from=builder, which is proven reliable (binary/lib copies succeed).
Also replace the failure-masking `|| true` with a hard verification that the
model cache actually populated, so a failed download fails the build loudly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 🔧 fix: ship warmed model cache as a tarball (ACR symlink-tree COPY export bug)

Root cause found: cb9 still failed at `COPY --from=builder .../models` with
"layer does not exist", while single-file (codesearch) and small-dir (/out/lib)
copies from the SAME stage succeed. The fastembed/HuggingFace model cache is a
symlink tree (snapshots/ -> blobs/); ACR's classic builder cannot export a
cross-stage COPY of a symlinked directory tree.

Fix: tar the model cache to a single /models.tar.gz in the builder (symlinks
preserved inside the archive), COPY the one file into runtime (structurally like
the proven binary copy), and untar it there. The existing `chown -R app:app
/home/app` fixes ownership of the extracted cache.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(entrypoint): build vendor indexes sequentially to avoid OOM-kill

The index-job submitted all per-vendor build requests at once (rebuild_repo
returns on HTTP 202) and waited once afterward, so serve held every vendor's
embedding model + working set simultaneously and was OOM-killed (SIGKILL) on
the 8 GiB job limit, leaving wait_until_indexed polling a dead process forever.

Build one vendor at a time: submit -> wait_active_build_done -> verify -> next.
Peak memory is now a single index build regardless of vendor count.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ✨ feat: TUI info for remote mounts + disable inapplicable actions

The `i` (info) key now works on mounted remote projects (federation
peers): a new OverlayState::RemoteInfo shows the peer URL and the
peer-reported live status (status/lock/changes/calls/last-call) instead
of local on-disk index stats, which a mount does not have.

When a remote mount is selected, the footer now renders doctor / reindex
/ remove struck-through (CROSSED_OUT) so it is clear those local-index
actions do not apply to a peer-hosted mount. info / reload / quit / nav
stay enabled. The standalone remote TUI is unaffected (its rows are the
peer's own local repos, is_remote=false).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 📝 docs: document project-level mounting + cloud reindex hardening

CHANGELOG: new [Unreleased] section covering mounted remote projects
(project=<peer>/<alias>), the TUI remote-mount info panel + disabled
local-index actions, the per-vendor cloud indexer split, the sequential
build OOM fix, the local BuildKit build workflow, and the grep-guard hook.

README: new "Mounting a peer's projects" subsection under Federation
(project=<peer>/<alias>, italic TUI mounts, `i` info, disabled actions).

AGENTS: Stage 4/5 notes updated (TUI info/disabled + sequential build),
Current state bumped to v1.1.9 with deploy outcome; deferred list refreshed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ✨ feat: flash feedback when a disabled action is pressed on a remote mount

Applies the reviewer's non-blocking UX remark: pressing doctor / reindex
/ remove while a mounted remote project is selected was a silent no-op
(the struck-through footer hint was the only cue). Now it also flashes a
short "don't apply to a remote mount" confirmation, reinforcing which
actions are available on a peer-hosted mount.

Message centralised in one REMOTE_ACTION_NA const (no literal duplication).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* @
🔒️ fix: scrub customer identifier (aprimo→vendor-a) for public push

The pre-push customer-ref gate (blocks [Aa]primo|husqvarna|bayer|… on
pushes to develop/master) flagged 5 residual "aprimo" references after
merging federation into develop: vendor-list examples in AGENTS.md /
CHANGELOG.md, a doc-comment in repos.rs, and test data in
federation/mod.rs. Replaced all with the generic placeholder "vendor-a"
(other vendor names akeneo/bynder/… are not customer identifiers and stay).
The federation namespacing test still passes (arg + assert use the same
token). Full-tree scan now clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@

* ✨ feat: opt-in mounting of individual remote projects (remote_mounts allowlist)

Remote peers no longer auto-expose every project. The local user now
explicitly picks which individual per-vendor indexes to use, via a new
opt-in `remote_mounts` allowlist in repos.json — the single source of
truth for routing, discoverability, TUI display, and group fan-out.

- config: replace opt-out `remote_hidden` with opt-in `remote_mounts`;
  mounted_remote_projects() is allowlist-driven (no discovery arg);
  resolve_remote_project() gates on the allowlist; new group_remote_projects(),
  mount_remote_project()/unmount_remote_project(); reconcile() prunes
  stale/unknown-peer/malformed mounts + orphaned rename overrides.
- routing: `@peer` group fan-out now queries only the mounted <peer>/<alias>
  projects (per-project search_project), never the whole peer; federated_search
  reworked; obsolete whole-peer FederationClient::search removed.
- discoverability: list_projects gains a `remote_projects` array; scope_required
  advertises mounted names as first-class `project=` targets.
- cli: `remote available|mount|unmount|mounts` to inspect a peer and pick.
- tui: rows come from the allowlist; discovery only enriches live status.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 📝 docs: opt-in remote mount selection (remote_mounts allowlist)

Update CHANGELOG (Unreleased), README (Federation → mounting), and
AGENTS.md for the shift from auto-discover/opt-out to the explicit
`remote_mounts` allowlist: new `remote available|mount|unmount|mounts`
CLI, group fan-out restricted to mounted indexes, non-mounted =
unroutable, and mounts surfaced in list_projects/scope_required.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 🐛 fix: prune orphaned remote rename-overrides unconditionally in reconcile

Address reviewer minor: reconcile() dropped orphaned remote_alias_overrides
only when a mount was pruned that round, so a hand-edited removal from
remote_mounts left a stale override that could resurface as a surprise
rename on re-mount. Now retain overrides against the current mounted set
unconditionally.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ✨ feat: show peer index stats in remote-mount info overlay

The TUI `i` overlay on a mounted remote project previously showed only
peer URL + status. It now fetches the peer's on-disk index stats
(chunks / files / db size / model) on demand from GET /repos/{alias}/info
and renders them with a loading / ready / unavailable tri-state, giving
remote mounts parity with the local Info overlay.

- federation: add RemoteRepoInfo + FederationClient::repo_info()
- constants: add REPO_INFO_PATH_SUFFIX ("/info")
- tui_common: OverlayState::RemoteInfo gains RemoteStatsState; render
  chunk/file/db-size/model lines (or placeholder) after status
- tui: build_remote_info_overlay starts Loading; ShowInfo resolves
  peer+remote_alias and spawns an async fetch via the doctor channel;
  recv guard broadened to apply RemoteInfo results

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 🐛 fix: harden remote-mount info fetch against stale/None resolve

Review remarks on 1cea46b:

- Bump doctor_gen UNCONDITIONALLY before resolving the mount, so a
  still-in-flight doctor/remote-info reply (shared channel + counter)
  can never clobber the freshly-opened RemoteInfo overlay via the recv
  guard.
- When resolve_remote_project returns None (misconfig or a config
  reload racing the keypress), render stats as Unavailable instead of
  leaving the overlay stuck on "fetching…" forever.
- Build the base overlay once and clone it (derive Clone on
  OverlayState) rather than building it twice.
- Soften the Unavailable label to "stats unavailable from peer" since
  an HttpError from a reachable peer also lands here (not only
  unreachability).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 📝 docs: note peer index stats in remote-mount info overlay (CHANGELOG)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 🐛 fix: scope federated get_chunk to remote project (fixes ambiguous_chunk_id)

Remote search returned chunk_refs shaped "<peer>:<id>", dropping the remote
project alias. Since the peer is itself multi-repo and chunk_ids are only
unique within one index, every federated get_chunk failed with
ambiguous_chunk_id when the peer hosted more than one project (inriver,
aprimo, bynder, ...).

Client-side fix (the serve /chunk route already honoured ?project=):
- convert_remote_item now namespaces the ref as "<peer>/<alias>:<id>" and
  tags source as "<peer>/<alias>".
- parse_federated_chunk_ref (new, unit-tested) splits peer/alias/id; accepts
  the legacy "<peer>:<id>" shape for backward compatibility.
- FederationClient::get_chunk forwards project=<alias> (and omits group) when
  an alias is present, mirroring search_project; legacy refs still fall back
  to group scope.
- Docs on GetChunkRequest.chunk_ref + inline comments updated.

Tests: parse helper (4 cases), namespaced convert, and a live-peer get_chunk
asserting project=<alias> is forwarded and group omitted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ✅ test: cover legacy no-alias get_chunk group fallback (review minor)

Adds a live-peer test asserting that a non-namespaced chunk_ref
(remote_alias=None) forwards a `group` scope and omits `project`, closing
the coverage gap flagged in the Stage A review.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ✨ feat: split hooks command into `hooks git` and `hooks claude` (+ Claude installer in Rust)

The single `hooks install` (git post-checkout hook) is replaced by two
explicit subcommand groups (hard break, no back-compat alias for the old
`install`):

- `codesearch hooks git install [--path]` — the prior post-checkout worktree
  auto-register hook.
- `codesearch hooks claude install [--project]` — NEW: installs the Claude
  Code PreToolUse guard hooks (Grep -> grep-guard, Agent -> subagent-preamble)
  into ~/.claude (or ./.claude with --project). Rust port of
  integrations/claude-code/install.{sh,ps1}: scripts are embedded via
  include_str! (self-contained binary), settings.json is backed up and merged
  idempotently (keyed by exact command string), and the host shell is detected
  (pwsh on Windows, bash elsewhere).

The top-level command is now `hooks` (alias `hook` kept for muscle memory).
New module src/cli/claude_hooks.rs with unit tests for the settings merge
(empty/idempotent/preserve-unrelated/bad-shape) and host-shell command build.
README updated. Stage B will add a WebSearch/WebFetch guard to GUARD_HOOKS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ✨ feat: add web-guard hook — steer WebSearch/WebFetch to remote doc mounts

New PreToolUse guard (bash + pwsh twins) matching WebSearch|WebFetch: when
repos.json has remote projects mounted (.remote_mounts, e.g. cloud/inriver,
cloud/aprimo), it denies the first web call with guidance to search those
indexed mounts first (compact=false to read inline, then get_chunk). Same
5-minute retry-escape as grep-guard; when no mounts are configured it does
nothing. Detection reads repos.json directly (CODESEARCH_REPOS_CONFIG or
~/.codesearch/repos.json) — no binary spawn, no serve round-trip.

- integrations/claude-code/hooks/web-guard.{sh,ps1} (new)
- claude_hooks.rs: web-guard added to GUARD_HOOKS (embedded via include_str!)
- install.{sh,ps1}: register the WebSearch|WebFetch matcher for parity
- README: three-guard section, `hooks claude install` as the primary path

Also addresses the Stage C review minors: drop the redundant create_dir_all,
add tests for non-object hooks/root shapes + GUARD_HOOKS coverage, and
cross-reference the two documented install paths.

This closes the gap that let me reach for WebSearch instead of the mounted
inriver/aprimo docs — the guard now makes the preference structural.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 📝 docs: make web-guard guidance use get_chunk(chunk_ref=…) explicitly (review minor)

Clarifies the deny message in both web-guard twins: after searching a mount,
read full context via get_chunk with the returned federated `chunk_ref`
("<peer/alias:id>"), not chunk_id — the correct param for remote results.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 📝 docs: align SearchResultItem chunk_ref/source docs with namespaced format (final review remark)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 📝 docs: add remote/federation + index --remote rows to CLI Reference table

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ✅ test: replace fixed sleep with bounded readiness poll in live-peer federation tests

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 📝 test: add remote-mount semantic-findability test scenario (Run 1: PASS)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ✨ feat: serve incrementally reindexes custom-kb on each KB pull

serve mode previously git-pulled the custom-kb repo every
KB_PULL_INTERVAL_SECS but nothing triggered indexing afterward — the
"periodic incremental reindex (REINDEX_INTERVAL_SECS)" the comments
promised does not exist in code. Pulled KB articles therefore only
became searchable on the next cold-start warmup.

The KB pull loop now detects when a pull moves HEAD and fires
POST /repos/custom-kb/reindex (incremental) against the local serve, so
new/changed articles are searchable without a restart. Incremental
refresh re-embeds only the delta and the KB corpus is small, so it fits
the 1-2 GiB serve replica; the heavy DOCS corpus stays index-job-only.

- repos open read-write by default (try_open_stores), so custom-kb on the
  serve's local disk reindexes in-place — no Rust change needed
- fire-and-forget 202; 409 (concurrent/FSW pickup) is expected + harmless
- first pull fires after the interval, after Phase-1 warmup releases the
  KB write lock, so no warmup contention
- fixed the stale REINDEX_INTERVAL_SECS comments (header, env doc, run_serve)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 📝 docs: scope cloud "read-only serve" claims to the custom-kb reindex exception

Follow-up to the serve custom-kb incremental-reindex change. The cloud
docs still described serve as strictly restore-only / never-reindexes,
which is no longer accurate: serve now runs a memory-bounded incremental
reindex of the small custom-kb repo after each KB pull.

- integrations/cloud/README.md: scope the "read-only / never writes"
  statements to the DOCS corpus; document the custom-kb incremental
  reindex as the sole in-process write (fire-and-forget 202, incremental
  only, HEAD-change gated, 409/404 benign). Correct the management-verbs
  note — incremental reindex of a registered repo succeeds; only add /
  reindex --force still require a read-write peer.
- AGENTS.md: note the custom-kb incremental step as the scoped first
  realization of the "incremental in-process on serve" redesign; DOCS
  corpus stays job-only (the OOM that motivated the split). Nuance the
  remote-write-verbs note accordingly.
- entrypoint.sh: distinguish HTTP 404 (custom-kb not yet in the restored
  snapshot — expected during bootstrap) from a genuine failure WARN
  (addresses review remark).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 📝 test: add section F — cross-vendor overlap + isolation scenarios (Run 1)

Complements section B (isolation) with the inverse: a concept shared
across vendors must surface hits from multiple vendors at once via
group="docs" RRF fusion, while domain-specific concepts stay absent from
the opposite domain. 5 scenarios (F1–F5) covering PIM/DAM overlap and
isolation, all executed and passing in Run 1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix: COPY integrations/claude-code/hooks into Docker builder

The cloud image build broke on the release compile:

  error: couldn't read `.../integrations/claude-code/hooks/grep-guard.sh`:
  No such file or directory (os error 2)

src/cli/claude_hooks.rs embeds the six hook scripts at compile time via
include_str!("../../integrations/claude-code/hooks/*"), but the Dockerfile
only copied Cargo.*, build.rs and src/ into the builder, so the hooks
subtree was absent from the build context. This is latent since the
hooks-split feature landed — v2.7 predates it, so this is the first image
build to hit it. Local builds compile because the tree is on disk.

Copy only that subtree (the exact paths include_str! needs) before the
cargo build. Verified no other include_str!/include_bytes! in src/
references paths outside src/.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix: pin shell scripts to LF via .gitattributes (CRLF broke cloud image)

The v2.8 cloud image failed to start: `env: 'bash\r': No such file or
directory`. Root cause: core.autocrlf=true rewrites docker/entrypoint.sh
to CRLF in the Windows working copy, and the Docker build copies the
working copy (not git's LF blob) into the image — so the CRLF shebang was
baked in and the container could not exec bash.

Pin *.sh (and docker/entrypoint.sh explicitly) to eol=lf so the working
copy is always LF regardless of the local autocrlf setting, and the image
can never regress to a CRLF shebang. entrypoint.sh already normalized to
LF in the working copy; the git blob was already LF.

Deployed image tag v2.9 carries the LF fix and is verified live (serve
boots, /healthz 200, "KB auto-pull loop started (git pull + reindex-on-
change...)" present, no bash\r error).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🔧 chore: pre-commit hook does cargo fmt only (drop per-commit version bump + rebuild)

The hook auto-bumped the Cargo.toml patch version and ran `cargo build`
on every feature-branch commit. That blocked each commit for minutes on a
debug build nobody deploys, and made the deployed binary constantly drift
from HEAD (forcing a manual release rebuild to re-sync).

The auto-bump was redundant: build.rs already appends a unique
"+<commit_count>" suffix (git rev-list --count HEAD) to every build, so
each commit is uniquely identifiable without churning the base version.

Now the hook only runs cargo fmt (+ stages reformatting). The base version
is bumped deliberately at release time. Updated RELEASING.md accordingly.
Installed the new hook into .git/hooks/pre-commit (this commit already ran
it — fast, no bump).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🔧 chore: pin extensionless hook scripts to LF in .gitattributes

scripts/pre-commit and .githooks/* are shell scripts without a .sh
extension, so the *.sh rule didn't cover them. On a Windows checkout
(core.autocrlf=true) they become CRLF, and copying scripts/pre-commit
into .git/hooks then yields a `#!/bin/bash\r` shebang that breaks the
hook. Pin them to eol=lf, same as the other shell scripts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ✨ feat(serve): KB near-instant propagation via cheap remote-HEAD poll

The serve-mode KB refresh loop previously did a full `git pull --ff-only`
only every KB_PULL_INTERVAL_SECS (default 900s), so a pushed KB edit took
up to ~15 min to become searchable in the cloud.

Now the loop cheaply polls the remote HEAD every KB_POLL_INTERVAL_SECS
(new, default 30s) via `git ls-remote origin <branch>` — ref advertisement
only, no object transfer — and performs the real pull + incremental reindex
only when the remote SHA actually moved. A pushed edit propagates in
~seconds instead of minutes.

KB_PULL_INTERVAL_SECS (default 900) is retained as a safety-net: it forces
a full pull at least that often even when the cheap poll saw no change or
ls-remote failed, self-healing a missed poll. ls-remote uses the stored
`origin` remote so the PAT never lands on argv. No codesearch core (Rust)
changes — trigger lives entirely in deployment glue where git already runs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: redact platform name -> example-dam to satisfy customer-ref pre-push guard

The local pre-push guard scans tracked files for customer/vendor identifiers
and flagged the bare platform name "aprimo" (as mount `cloud/aprimo` and in
prose) across README, the remote-mount test scenario, and both web-guard hooks.
Replaced every occurrence with the neutral placeholder `example-dam`; no
functional/code change. Line endings preserved (LF for scripts).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 📝 docs: add missing KB-propagation changelog entry + filter_path federation caveat

Audit found two doc gaps: the KB near-instant propagation feature
(commit bd90ec2) had no CHANGELOG entry, and filter_path's documented
zero-result behavior on federated/mounted projects (observed live via
the aprimo_mcp consumer) wasn't captured anywhere in README or
CHANGELOG. Documents the known limitation + client-side over-fetch
workaround until the root cause is isolated with a live hub+peer repro.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix: apply federated filter_path client-side on namespaced result paths

Federated search forwarded filter_path to the peer, which matched it
against its own un-namespaced store paths (and, in serve mode, the wrong
project root via build_semantic_response using self.project_path instead
of the routed alias root). The caller only ever sees the
`<peer>/<alias>/…` namespaced path, so a server-side match dropped every
hit regardless of value — the "0 results" symptom observed live via the
aprimo_mcp consumer.

Fix: stop forwarding filter_path to the peer; over-fetch and post-filter
client-side on the namespaced paths in both federated_project_search
(project passthrough) and federated_search (group fan-out), via a shared
retain_by_filter_path helper + is_meaningful_filter guard. Consumers no
longer need the over-fetch+post-filter workaround.

The underlying server-side root mismatch still affects filter_path on a
LOCAL project routed through serve (non-federated); documented as a
follow-up in CHANGELOG known-limitations. stdio single-repo is unaffected.

Tests: retain_by_filter_path unit tests (matching prefix, none/blank
no-ops, no-match empties). Full lib suite 566 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix: relativise filter_path against the routed project root in serve mode

For search(project=<local-alias>) or a local group served by codesearch
serve, build_semantic_response relativised result paths against the
service's own project_path instead of the ROUTED project's root, so the
absolute stored path never stripped and filter_path dropped every hit
(0 results for any value). Only stdio single-repo — where project_path
IS the repo root — worked.

Fix: pick_filter_root() resolves the correct root per result — the routed
alias's root for single-project routing, the longest matching alias root
for multi/group, and the service project_path only as the stdio fallback.
filter_path is now a repo-relative prefix in every routing mode. This is
the non-federated companion to the client-side federated fix (1241963);
together they close the filter_path scoping gap end to end.

Tests: pick_filter_root unit tests (routed alias, longest-match multi,
stdio fallback). Full lib suite 569 passed. stdio behaviour unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🔒️ fix: scrub customer identifier (aprimo→vendor-a) in mcp tests

Filter_path test fixtures used the real customer alias; replace with the
established generic placeholder so the pre-push customer-ref gate passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix: harden `hooks git install` (windows path, worktree common-dir, chain existing)

The generated post-checkout h…

v1.1.29

Toggle v1.1.29's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Release v1.1.29 — hardened worktree hook install + Linux clippy fix (f…

…lupkede#141)

* docs: add federation feature plan

* feat(serve): add REST search/find/explore/chunk endpoints for federation

Expose the read-only MCP tools (search, find, explore, get_chunk) over plain HTTP+JSON so a remote codesearch serve can be queried for federation WITHOUT an MCP session. Each REST handler constructs a per-request CodesearchService bound to the live ServeState, invokes the existing #[tool] method, and returns the tool's JSON payload unwrapped from CallToolResult. The new routes inherit the existing auth layers (require_auth_for_network on network binds), so no new auth code is needed. Adds a rest_routes_are_registered integration test.

* fix(serve): share embedding service across REST + MCP sessions

Previously each CodesearchService started with embedding_service=None, so
per-request REST semantic search reloaded the ONNX model (~100ms-2s) on every
call. Mirror the symbol_registry pattern: ServeState now owns a shared
Arc<Mutex<Option<EmbeddingService>>> and exposes an embedding_service() getter;
new_for_serve binds to it, so the model loads once per serve instance (lazily
on first semantic query) and is reused by all MCP sessions AND REST handlers.
The standalone new() path keeps its own Arc. Addresses review remark on the
REST-endpoint commit.

* feat(federation): add remote-peer config + federated search/get_chunk (Phase 2)

ReposConfig gains a `remotes` map of RemotePeer{url, api_key, group, timeout_secs}.
Group members may reference a remote peer via an "@"-prefix (e.g. "docs": ["@cloud"]);
resolve_group_targets/split_group_targets return mixed Local/Remote Target lists
(the virtual "all" group stays local-only). New `federation` module exposes a
FederationClient that calls the Stage-2 REST endpoints (/search, /chunk/:id) with
per-peer bearer auth over a shared reqwest client. The `search` tool, when its
group contains remotes, fans out to each peer concurrently, converts remote hits
into source-tagged SearchResultItems (chunk_ref = "peer:id"), and RRF-merges
local + remote ranked lists; unreachable peers degrade gracefully into a
`warnings` array (never hard-fail). The `get_chunk` tool transparently proxies
namespaced chunk_refs ("peer:id") back to the owning peer. 16 new unit tests
(config, federation client, merge/conversion helpers). find/explore federation
deferred.

* fix(federation): honest low_confidence for federated results

The previous `f.score < 0.15` threshold was unsatisfiable: merge_ranked_lists
reassigns every score to the RRF value 1/(k+rank+1) (max ≈ 0.048 for k=20), so
every federated search that returned ANY hit was flagged low_confidence=true.
RRF-fused scores are not comparable to single-source embedding/BM25 thresholds,
so no score cutoff is meaningful here. Now low_confidence is flagged only when
the merged set is empty (a genuine "nothing found" signal to the agent). Also
drops the unused `_limit` parameter from build_federated_response.

Addresses review remark on the Phase 2 federation commit.

* docs: align federation plan with shipped Phase 1+2 reality

* [worker] stage 1/2: surface project→group membership in scope_required

Add ReposConfig::project_groups() — inverse index mapping each repo alias to
the named group(s) it belongs to (sorted/deduped; excludes the virtual "all"
group and "@remote" refs; omits aliases in no named group).

Expose it as a new `project_groups` field in the scope_required error and
sharpen `hint_for_agent` so an agent picking a single project is told to
prefer group= when that project belongs to a group (e.g. a separate config /
import-data repo). Adds 2 unit tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [worker] stage 2/2: add per-repo group membership to status(kind=projects)

Add a `groups` field to RepoInfo listing the named group(s) each repo belongs
to (via ReposConfig::project_groups()), populated in both the serve and stdio
branches of list_projects. Lets an agent inspecting `status` see that e.g.
BAYR.Aprimo is part of group BAYER and prefer a cross-repo group= query.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [worker] final review: document implicit "all"-group exclusion in project_groups()

Addresses the sole (informational) review remark: clarify that project_groups()
excludes the virtual "all" group implicitly (it is never persisted in
self.groups), so a future change that starts persisting "all" must filter it
explicitly. Doc-comment only; no behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [worker] stage 1/3: add unauthenticated /healthz probe for ACA + cloud deploy plan

- /healthz: fixed {"status":"ok"} body, no version/repo info, exempt from
  require_auth_for_network so container-orchestrator probes reach it on a
  network bind without the Bearer key. /health stays auth-protected.
- HEALTHZ_PATH constant; route registered; log-spam suppressed.
- Test: healthz reachable without key on simulated network bind, /health 401.
- docs/federation-cloud-deployment.md: phased Azure plan (role-assignment-free:
  SAS + inline ACA secrets), verified rights, PIM-Contributor on Aprimo RG.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [worker] stage 2/3: container image, blob-sync entrypoint, serve cloud keep-warm

Container artifacts:
- Dockerfile: multi-stage build + fastembed model pre-warm baked into the image
  (ONNX loads from a local path; never streamed from blob) + slim runtime with
  azcopy + git. .dockerignore keeps target/ and tool dirs out of the context.
- docker/entrypoint.sh: snapshot-restore -> azcopy sync (+ optional KB git pull)
  -> serve; background loop does incremental reindex + periodic index snapshot
  to a separate blob container. No FSW; full-on-start + incremental-on-timer.

serve cloud keep-warm (2h-idle-then-suspend for ACA scale-to-zero):
- --keep-warm-url / CODESEARCH_KEEP_WARM_URL + --idle-suspend-secs /
  CODESEARCH_IDLE_SUSPEND_SECS (default 7200). Serve self-pings its own ingress
  /healthz while the most-recent real tool call is younger than the idle window,
  then stops so ACA suspends; next real query wakes it. No Logic App, no managed
  identity, no role assignment.
- ServeState::most_recent_tool_call(); constants for env vars + defaults.

docs: option D (scale-to-zero + blob snapshot) with actual SSOT/Aprimo resources.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [worker] stage 2/3: fix Dockerfile for ACR/portable build

Three build-portability fixes found via review + real builds:
- COPY build.rs into the builder: env!("CARGO_PKG_VERSION_FULL") (main.rs,
  cli.rs) needs the var build.rs emits; it falls back to "0"/"unknown" without
  .git, so the build is reproducible without repo history.
- Drop BuildKit `--mount=type=cache`: ACR Tasks uses the classic builder which
  rejects `--mount`. ACR builds fresh anyway.
- Base images bookworm -> trixie (glibc 2.41): the prebuilt onnxruntime pulled
  by `ort` references glibc-2.38+ C23 symbols (__isoc23_strtoll), so linking on
  bookworm (2.36) failed. Runtime moved to trixie too; libssl3 dropped (reqwest
  uses rustls, no OpenSSL at runtime).

Verified: full image builds clean locally on trixie (all 24 steps).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [worker] stage 2/3: fix entrypoint repo registration + azcopy + warm-repo bake

Found via live ACA deployment + container logs:
- azcopy sync: drop --compare-hash=MD5. It stores the hash in a user_xattr,
  which the container overlayfs does not support -> every transfer failed
  ("1 Failed, 0 bytes") so docs never synced. Default sync (LMT+size) needs no
  xattr and works.
- Dockerfile: copy ONLY ~/.codesearch/models from the warmer, not the whole
  dir. The warmup writes a repos.json registering "/tmp/warm", which baked a
  stale "warm" repo into the runtime image.
- Repo registration: `serve --register` adds the alias but does NOT build the
  initial index (create_index is ignored in the CLI handler), and an unindexed
  repo is auto-pruned at warmup -> "Unknown alias 'docs'". The entrypoint now
  registers each repo via POST /repos (create DB + index + warm) once serve is
  live, instead of --register.

Verified end-to-end on Azure Container Apps: cold start -> auto-register ->
index -> federated /search returns the doc within ~5s.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [worker] stage 3/3: record live Azure deployment of federation cloud peer

ACA app codesearch-serve live in Delaware.SSOT/Aprimo (scale-to-zero, HTTPS,
snapshot + keep-warm). End-to-end verified: cold start -> auto-register ->
index -> federated search. Documents resources, FQDN, and the az-CLI emoji
log-stream caveat (build local + docker push, not az acr build).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [worker] stage 1/1: add `codesearch remote` command for federation peers

Manage remote serve peers from the CLI instead of hand-editing repos.json:

  codesearch remote add <name> --url <url> [--api-key K] [--group G]
                               [--timeout-secs N] [--into-group LOCAL_GROUP]
  codesearch remote list
  codesearch remote rm <name>

Model (ReposConfig):
- add_remote(name, peer): validate non-empty name, reject a leading '@'
  (the reference prefix is added automatically), require non-empty url.
- remove_remote(name): drop the peer AND prune every "@name" reference from
  groups, dropping any group left empty.
- add_remote_to_group(group, name): idempotently push "@name" into a group
  (created on demand); rejects reserved "all" and unknown peers.
- groups_referencing_remote(name): sorted groups wiring a peer (for `list`).

--into-group wires "@name" into a local group in one step so the peer is
immediately queryable; without it, `add` prints the follow-up hint.

Hoisted the federation default timeout (15s) into
constants::DEFAULT_REMOTE_TIMEOUT_SECS so the client and the CLI share one
source of truth (no duplicated magic number).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [worker] stage 1/1: fix review remarks on `remote` command

- remote rm: trim the name before lookup/printing, matching the trimming
  `add`/`--into-group` already do (so `rm "  cloud  "` finds `cloud`).
- federation: demote the inert doc-comment above the `use ... as
  DEFAULT_TIMEOUT_SECS` alias to a plain comment (a `///` on a `use` is dead).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [worker] feat: split cloud entrypoint into serve / index-job modes

Decouples the memory-heavy index build from light serving so the long-running
Container App can run small (1-2 GiB) while a short-lived Container Apps Job does
the full embed on a big replica (4-8 GiB), avoiding OOM (exit 137) on serve.

CODESEARCH_RUN_MODE:
- serve (default): restore the prebuilt snapshot from blob and serve READ-ONLY.
  No register / reindex / snapshot — never does heavy work. Warns if no snapshot.
- index-job: restore (incremental) -> sync blob -> drive a local serve to
  build/force-reindex -> wait until /status clears "indexing" -> upload snapshot
  -> exit 0. New helpers: wait_healthz, register_or_reindex, wait_until_indexed.

Deployed: image tag v2-modes; ACA Job 'codesearch-indexer' (index-job, 2 vCPU/
4 GiB, Manual trigger); serve app resized to 1 vCPU/2 GiB with RUN_MODE=serve.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [worker] feat: robust index-job rebuild (DELETE+POST) + deployment doc

The /reindex?force=true endpoint returns 500 in the cloud deployment, so the
index-job could not pick up new/changed docs. Replace the reindex call with the
proven path: when the repo is already registered (restored from a snapshot),
DELETE /repos/{alias} then POST /repos for a clean full rebuild. The restored
embedding cache makes unchanged docs cache-hits, so only new/changed docs cost
real work — appropriate for the Job's big replica.

Also harden wait_until_indexed against the DELETE+POST race: phase 1 waits for a
build to actually be observable (status "indexing", or an already-ready state for
a cache-instant rebuild) so the brief post-DELETE gap is never mistaken for
"done"; phase 2 waits for "indexing" to clear with the repo present + ready.

docs/federation-cloud-deployment.md: document the serve/index-job split, the
codesearch-indexer Job, the 1 vCPU/2 GiB restore-only serve, and image v2.1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [worker] docs: honest cost note for index-job rebuild

The DELETE+POST rebuild is a full re-embed (~20-25 min for the 2737-doc corpus on
the job replica), not a cache-fast pass — observed in a live run. Correct the
entrypoint comment and deployment doc to state this plainly; the heavy cost is by
design (it lives in the Job, never in serve).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [worker] fix(index-job): incremental /reindex + pre-upload index guard

The cloud index-job's rebuild_repo used DELETE + POST /repos: it deleted the
~60 MB index dir then immediately reopened it (racy on the container overlayfs,
surfaced as "register failed" 500 + a stalled build that never refreshed the
snapshot), and it was a full ~20-25 min re-embed every run.

Switch to the safe, light path:
- already-registered repo (snapshot restored) -> POST /repos/<alias>/reindex
  (incremental, no force): opens the existing index in place, re-embeds only
  deltas, returns 202. No DB delete, no reopen race.
- not-yet-registered (cold build, no snapshot) -> POST /repos {path} (full build).
- /reindex?force=true still avoided (returns 500 in this deployment).

Honesty + safety:
- api_code() captures the HTTP status instead of swallowing it with >/dev/null,
  and a non-2xx/202 rebuild response now die()s (no silent "register failed").
- verify_index_ready() gates upload on GET /repos/<alias>/info chunks > 0, so a
  broken/empty build can never clobber a known-good snapshot.

Doc updated; job sizing noted as 4 vCPU / 8 GiB, 5400s.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [worker] fix(index-job): stop corpus sync from deleting the index

Root cause of the v2.2 run failure (HTTP 500 "Repo 'docs' is locked by another
codesearch process"): sync_blob runs `azcopy sync --delete-destination=true`
from the blob (which holds only .md source files) into ${DOCS_DIR}, but the
search index lives INSIDE that dir at ${DOCS_DIR}/.codesearch.db. azcopy treated
the whole restored index as "extra" and DELETED it, leaving the incremental
reindex with no base + stale locks.

Fixes:
- sync_blob: --exclude-path=".codesearch.db" so the corpus sync never touches the
  index. This also protects the SERVE app's restored index on cold start (same
  code path) — previously a cold start could silently wipe the served index.
- restore_snapshot: delete stale .writer.lock / .tantivy-*.lock / lock.mdb that a
  prior snapshot baked in (the source of the "locked by another process" 500). A
  fresh container has no other process, so any lock present is stale.
- upload_snapshot: exclude *.lock / lock.mdb from the tar so future snapshots stay
  clean.
- rebuild_repo: accept HTTP 409 (reindex already in progress) as "wait for it"
  instead of die — serve's startup warmup may already be refreshing the repo.

The pre-upload verify guard correctly prevented the failed v2.2 run from
clobbering the good snapshot.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [worker] fix(index-job): let serve warmup own the refresh (no competing reindex)

The v2.3 run still hit HTTP 500 "Repo 'docs' is locked by another codesearch
process with write access" — but this time it was a LIVE lock, not a stale one,
and the index was no longer being deleted (the sync exclude worked).

Cause: serve's Phase-1 startup warmup (warmup_repo) opens every registered repo
in WRITE mode and runs an incremental refresh on startup, holding the LMDB write
lock for the whole refresh. The job's explicit POST /repos/<alias>/reindex opened
a SECOND write handle on the same env -> 500. The warmup already does exactly the
incremental refresh the job wanted.

Fix: for an already-registered repo, the job no longer issues any reindex. It lets
the startup warmup do the refresh and just waits for the repo to flip from "closed"
(mid-warmup) to "warm" (refresh done) in wait_until_indexed(), then verifies
chunks>0 and snapshots. Only the cold (unregistered) case still POSTs /repos.

The pre-upload verify guard again prevented the failed v2.3 run from clobbering
the good 69 MB snapshot. Doc updated to describe the warmup-driven refresh and the
two invariants (sync never touches .codesearch.db; never compete with the warmup).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [worker] stage 1/3: add FederationClient management API (list/add/remove/reindex repos on a peer)

Add REPOS_PATH constant and the remote index-management client surface:
ManagementOutcome (Ok/HttpError/Unreachable), RemoteStatus/RemoteRepoStatus,
RemoteRepoAdded/Removed/RemoteReindexResult structs, and FederationClient
methods list_repos/add_repo/remove_repo/reindex backed by a shared
send_management helper. Includes mock-peer unit tests (6 new, all passing).
Dead-code warnings are expected — Stage 2 (CLI) consumes this API.

* [worker] stage 2/3: add --remote flag to index verbs + Reindex variant

* [worker] stage 2/3: fix review remarks (--json without --remote now rejected)

* [worker] stage 3/3: document the --remote index management flag in README + cloud deployment doc

* [worker] stage 3/3: fix review remarks (reconcile --remote docs with the read-only cloud peer)

* [worker] stage 3/3: polish README caveat precision + note writable-peer requirement on per-vendor recipe

* [fix] index rm: resolve argument by alias before treating as path

`codesearch index rm <alias>` (without --remote) previously treated the
alias as a filesystem path and failed with os error 2 during canonicalize.
Now `remove_from_index` resolves the argument against registered aliases
first (ReposConfig::resolve), falling back to path interpretation only
when it is not a known alias. The resolved path is also forwarded to
try_delegate_rm_to_serve so serve delegation works by alias too.

* [cli] add `ls` visible alias to all `list` subcommands

Adds `#[command(visible_alias = "ls")]` to the three `List` variants so `codesearch index ls`, `codesearch groups ls`, and `codesearch remote ls` work as shortcuts for `list`.

* [docker] serve: periodic KB git-pull loop (refresh custom KB without restart)

Background loop in run_serve re-runs sync_kb every KB_PULL_INTERVAL_SECS (default
900s) when KB_GIT_URL is set, so serve's 15-min incremental reindex picks up newly
pushed custom KB without a restart. Includes the pre-commit hook's rustfmt + version
bump to 1.0.236 (rebuild validated by the v2.5 image build).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [fix] serve: active_sessions underflowed to u64::MAX via per-request REST services

Drop for CodesearchService called session_disconnected() whenever serve_state
was Some. But new_for_serve is constructed by TWO paths:
  - serve service_factory (MCP sessions): calls session_connected() first, so
    Drop is balanced.
  - make_service (per-request REST handlers for /search /find /explore /chunk):
    creates the service with serve_state=Some but NEVER calls session_connected().
    Its Drop fired session_disconnected() -> AtomicU64::fetch_sub on 0 wraps to
    u64::MAX. Every REST request underflowed the counter.

Fix: add a `tracks_session: bool` field to CodesearchService (default false).
Only the serve service_factory calls a new mark_session_tracked() to set it
true. Drop now only decrements when tracks_session is true, so per-request REST
services no longer touch active_sessions.

* [docs] 1.0 GA: federation security analysis, changelog [1.0.0], version bump

- README: new top-level `## Security` section consolidating serve access
  control + a federation threat model (trust model, secret storage/transport,
  redirects, serve-side enforcement, write verbs, cross-instance isolation).
  Moved the serve-auth env-var block out of `### Security` under Configuration
  into the new section.
- CHANGELOG: add `[1.0.0] - 2026-07-01` GA entry (federation, --remote index
  management, cloud topology, README security section; fixes: active_sessions
  underflow, index rm alias resolution, ls alias).
- Cargo.toml / Cargo.lock: version 1.0.236 -> 1.0.0.

* Add Claude Code integration: enforce codesearch-over-grep via hooks

Claude Code's MCP tool schemas are deferred and Grep/Glob are always
loaded, so the advisory initialize instructions get skipped more often
than on other clients. Ships two PreToolUse hooks (ps1 + sh) that make
the preference structural: grep-guard blocks/redirects the first Grep
against an internal repo path when codesearch is available (fails open
otherwise, retry-unblocks within 5 min), and subagent-preamble injects
codesearch guidance into every spawned Agent prompt, since subagents
don't inherit AGENTS.md or MCP initialize instructions at all.

Includes install.ps1/install.sh for one-step setup at user or project
scope, idempotent and non-destructive to existing settings.json.

* [docs] 1.0 GA: disclose changelog condensation in [1.0.0], fix review remarks

- Add `### Changed` note under [1.0.0] disclosing that pre-GA history
  ([1.0.72]–[1.0.208]) was condensed to one-line summaries.
- Reword the [1.0.72] line from "First stable release" to "Initial
  multi-repo release" to avoid a double "first stable" claim with the
  [1.0.0] GA entry.
- Collapse the stray double blank line before [1.0.209].

* docs(federation): drop stale cloud-deployment plan; point to canonical arch doc

federation-cloud-deployment.md drifted from the live deployment (serve v2.5 /
indexer v2.4, custom KB pulled from github.com/develterf_dlwr/kb). The verified
current-state architecture now lives in
aprimo_mcp/docs/knowledge-base-architecture.md (with SVG). Kept federation-feature.md
as the generic Rust engine spec, repointed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* [fix] serve: close LMDB env before DB delete (await FSW task) — Windows per-repo remove without restart

On Windows a process cannot delete its own open mmap'd file, so pressing
'r'/DELETE /repos/:alias failed with a sharing violation on data.mdb/
lock.mdb while the background file-system-watcher (FSW) task held clones
of Arc<SharedStores>/Arc<IndexManager> that keep the LMDB Environment open.

Track each repo's FSW JoinHandle in a new fsw_tasks DashMap. remove_repo
and restart_fsw now await_fsw_shutdown() after cancelling the task, so the
task's Arc clones drop, the LMDB env closes (mdb_env_close), and Windows
releases the file handles BEFORE the DB directory is deleted. Per-repo,
in-process, no serve restart needed.

- ServeState.fsw_tasks: DashMap<String, JoinHandle<()>>
- await_fsw_shutdown(): bounded 5s await, detaches on timeout
- remove_repo: await before delete (the actual fix)
- restart_fsw: await old task before spawning new + track new handle
- get_or_open_stores / spawn_fsw_for_warm: track spawned FSW handles
- reload shrink: detach dropped handles (no DB delete there)

* [fix] serve: address review remarks (fsw_tasks hygiene + transient-race doc + test)

Addresses the PASS-WITH-REMARKS review of ab462e2 (no Critical):

- remove_repo: expand the doc comment on the delete retry-loop to explain the
  transient in-flight-query/warmup holder race — await_fsw_shutdown drops the
  *persistent* holders, but a transient spawn_blocking clone can still keep the
  LMDB env open briefly; the retry-loop is the documented fallback (warned +
  non-fatal; repo is already unregistered, so a lingering dir is cosmetic).

- evict_idle_repos + phase-1 prune: detach the FSW handle (fsw_tasks.remove)
  for consistency with the reload-shrink path. Neither deletes the DB dir
  (evict frees memory for re-open; prune's DB is already gone), so a plain
  detach is correct — the cancelled task drains on its own.

- Add 2 regression tests for await_fsw_shutdown bookkeeping:
  * joins the task to completion + removes the fsw_tasks entry (proves the
    join happens, guarding the Arc-drop→LMDB-close chain the Windows fix relies on)
  * no-ops (no panic) on a missing alias (Warm/Readonly repos have no FSW task)

* [feat] serve: GET /remotes endpoint — list configured federation peers (observability)

Adds a read-only /remotes endpoint (companion to /status) so an operator can
see which federation peers this serve fans out to. Reads the `remotes` map
from repos.json and returns alias/url/group/timeout_secs per peer, sorted by
alias.

- REMOTES_PATH constant added next to STATUS_PATH.
- Status-like auth policy: NOT in require_admin_auth's management set, so
  reachable without the admin key on localhost and protected only by
  require_auth_for_network on network binds (same as /status, /info, /doctor).
- Dedicated RemotePeerInfo response struct that structurally omits api_key —
  the shared secret cannot be serialized even by accident. The handler never
  reads RemotePeer.api_key.
- Degrades gracefully to {"remotes": []} on a repos.json read error.

* [test] serve: regression test — /remotes never serializes api_key

Locks the defense-in-depth on GET /remotes: RemotePeerInfo (the response
projection struct) has no api_key field, so serde cannot leak the peer's
shared secret. This test fails if a future change adds api_key to the
response shape or lets the key value through. Addresses review remark on
d137069.

* [release] 1.1.0: federation GA — version bump

The federation feature (remote peers, --remote index management, cloud
topology, /remotes endpoint) is a significant new capability over the
existing 1.0.x line, so it ships as a minor bump (1.1.0) rather than
claiming "first stable" — the project was already stable at 1.0.236.

- Cargo.toml/Cargo.lock: 1.0.2 -> 1.1.0
- CHANGELOG: [1.0.0] "First stable (GA)" reframed to [1.1.0] "Federation release"

* [docs] drop federation-feature.md; document Claude Code hooks in README

Remove docs/federation-feature.md — the standalone design doc added drift/broken-link risk (federation-cloud-deployment.md was already dropped); README + code comments are self-contained.

README: expand the one-line Claude Code hooks pointer into a self-contained subsection (grep-guard + subagent-preamble, install commands, CODESEARCH_SERVER caveat); drop the now-dangling 'See docs/federation-*.md' line.

src: drop the 3 dangling refs to the deleted doc (federation/mod.rs module doc; mcp/mod.rs x2).

* [docs] generic cloud-deployment guide (integrations/cloud); mermaid + changelog ref fix

- integrations/cloud/README.md: fully generic, environment-agnostic cloud
  deployment guide (Azure Container Apps build/serve split, scale-to-zero
  serve replica, snapshot-restore lifecycle, --remote management). No
  customer/specific-resource names — all <placeholder>s.
- README.md: mermaid diagram now shows the cloud serve peer node
  (ServeRouter -->|"@peer fan-out · TLS"| CloudPeer) and the subgraph is
  renamed "Serve Mode (multi-repo + federation)".
- CHANGELOG.md: fix dangling ref docs/federation-cloud-deployment.md
  (folder was dropped) -> integrations/cloud/README.md in [1.1.0] entry.

* [scrub] remove customer identifiers from tracked files (public repo prep)

* [fmt] cargo fmt --all (repos.rs test assert, serve/mod.rs constants import)

* [fix] claude-code: grep-guard ignores running process, requires local index

The grep-guard hook previously treated "a codesearch process is running" as sufficient evidence that codesearch is available for the current repo. But codesearch serve commonly runs as a persistent background hub covering many registered repos, so that process is alive on a dev machine almost all the time — making the hook fire in every directory, including unindexed ones (real false-positive hit in use).

Now the guard only treats codesearch as available FOR THIS REPO when it finds a local .codesearch.db at the git root, or an explicit CODESEARCH_SERVER env var (escape hatch for pure remote-serve setups with no local index). Process-presence check removed from both ps1 + sh.

README updated to document the precise availability signal + the rationale.

* Fix claude-code hooks: tell the model to pass project=/group= in serve mode

In multi-repo serve-hub mode (codesearch serve with several registered
repos) every search MUST specify project= (single repo) or group= (cross
-repo); omitting both returns a scope_required error, and a wrong alias
returns Unknown alias. The hook guidance previously showed only
search(query=..., mode="semantic") with no scope, so the model would get
blocked from Grep, call codesearch exactly as instructed, hit
scope_required, conclude "codesearch is broken", and fall back to Grep on
the 5-minute retry-unblock -- looking exactly like codesearch stopped
working.

Both grep-guard and subagent-preamble (ps1 + sh) now instruct: on
scope_required / Unknown alias, read the error (it lists the valid
available_projects / available_groups) and pass project=/group=, noting
the alias may differ from the folder name.

* [docs] AGENTS.md: fix stale version + doc links (v1.0.235 -> v1.1.0, docs/ -> integrations/cloud/)

Version and doc-path references had drifted after the public-repo-prep
commits (2aa49ce, 2061dfd) removed/moved docs/federation-*.md without
updating AGENTS.md. Docs-only change, no code touched -- skipping the
pre-commit hook's cargo build/version-bump (not applicable here, and it
timed out on the previous attempt without completing).

* [docs] escalate docs-repo warmup bug to HIGH; propose single-app scale redesign

Confirmed the known open/write-stuck status bug is the same mechanism behind
a real cloud crash-loop: the docs corpus doubled (2509->5666 files) and
serve's in-process incremental-warmup OOM'd repeatedly on 1vCPU/2GiB,
re-syncing from blob on every restart. Worked around by re-running the
codesearch-indexer job to produce a fresh full snapshot; serve now reports
both repos "warm" with no restarts.

Also documents a proposed redesign (not yet implemented): collapse the
separate indexer job + serve app into one Container App that scales
up/poll-until-warm/snapshots/scales back down, replacing the fragile
in-process indexing-flag detection with a reliable external /status poll.
Left open pending a follow-up session: whether to retire codesearch-indexer
entirely.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* [fix] bound incremental-refresh embedding batches to prevent OOM crash-loop

perform_incremental_refresh_with_stores chunked+embedded the entire
changed-file delta in one unbounded in-memory batch before writing
anything out. Harmless for normal deltas (tens of files) but this is
exactly what OOM'd codesearch-serve (1vCPU/2GiB) when the vendor docs
corpus roughly doubled (2509->5666 files) in one sync, crash-looping on
every cold start.

Fix: process changed_files.chunks(batch_size) sequentially (chunk+embed+
insert+commit per batch, single build_index() at the end), bounding peak
memory to O(batch) instead of O(total delta). Batch size defaults to
INCREMENTAL_REFRESH_BATCH_SIZE=200, override via
CODESEARCH_INCREMENTAL_BATCH_SIZE. Protects both codesearch-serve's
in-process warmup and codesearch-indexer's full rebuild against the same
failure mode as the corpus keeps growing, independent of which container
runs it.

cargo check + cargo clippy -D warnings + cargo test --lib --bins (1080
passed) all clean. No new test for the multi-batch path itself: existing
manager.rs tests deliberately avoid real embedding invocation (slow/
ONNX-dependent), consistent with the gated csharp_helper_integration
pattern elsewhere in this repo.

Also documents the still-open "automate the manual scaling trigger"
decision in AGENTS.md (codesearch-indexer job confirmed triggerType=
Manual) -- left open pending vendor content update-cadence info.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* [fmt] cargo fmt + version bump for previous fix commit (pre-commit hook catch-up)

The previous commit (bounding incremental-refresh batches) was made with
--no-verify by mistake, skipping this feature branch's normal pre-commit
hook (cargo fmt + patch version bump + rebuild). Running the equivalent
steps now: cargo fmt --all reformatted manager.rs, version bumped
1.1.1 -> 1.1.2, cargo build --bin codesearch verified clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* [docs] plan: remote project mounting (1-to-1 passthrough federation)

Records the locked design for moving federation from group-level to
project-level mounting: peers expose individual indexes, mounted locally
as project=<peer>/<alias>, italic in the TUI, server-side docs bundle
dropped in favor of user-owned local grouping.

Decisions: auto-discover + local filter; peer-namespaced names.
5-stage execution plan + verified current-code gaps.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* [feat] stage 1/5: config model for mounted remote projects

Foundation for project-level federation ("1-to-1 passthrough"): peers
expose individual indexes that mount locally as project=<peer>/<alias>.

- Target::RemoteProject { peer_name, peer, remote_alias } — a single
  remote project (vs whole-peer Target::Remote used by group federation).
- REMOTE_PROJECT_SEPARATOR ("/") + remote_project_name() helper.
- ReposConfig fields (local, user-owned filter): remote_hidden,
  remote_alias_overrides, remote_project_cache (offline fallback).
- mounted_remote_projects(discovered) + resolve_remote_project(name).

Pure, unit-tested config layer (4 new tests, 49 pass). Discovery + MCP
dispatch land in Stage 2 — temporary #[allow(dead_code)] removed then.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* [fix] stage 1/5: address review remarks (enforce peer-name namespacing invariant)

Review of c570fb1 (PASS WITH REMARKS):

- IMPORTANT: the REMOTE_PROJECT_SEPARATOR doc claimed peer names can never
  contain '/', but add_remote only trimmed + rejected '@'. A peer named
  "a/b" would break resolve_remote_project's split_once('/'). Fix: add_remote
  now rejects '/' in peer names, so the <peer>/<alias> invariant is actually
  enforced (not just asserted in a comment). Comment made precise. New test
  arm covers rejection.
- MINOR (precedence): documented that resolve_remote_project does NOT consult
  local repos, so callers (Stage 2 dispatch) MUST resolve local aliases first
  — local repos always win a name clash with a rename override.
- MINOR (override-target uniqueness non-determinism; local_name collision
  detection in mounted_remote_projects): deferred to Stage 2 as explicit
  dispatch/discovery design decisions, per reviewer.

cargo fmt + clippy -D warnings clean; 49 repos tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ✨ feat: route project=<peer>/<alias> to mounted remote projects (stage 2/6)

Project-level federation — a 1-to-1 passthrough that makes a remote peer's
project queryable locally as if it were a local index.

- FederationClient: extract shared post_search(); add search_project() that
  forces project=<remote_alias> and strips group (vs group-scoped search()).
- MCP search(): before local dispatch, resolve project as a mounted remote
  project (<peer>/<alias>) and route to that single peer. Local repos always
  win a name clash (resolve() checked first).
- federated_project_search(): single-peer passthrough, no local merge; an
  unreachable peer degrades to a warning with zero results. Namespaced
  chunk_refs route back through the existing federated_get_chunk().
- Remove now-live #[allow(dead_code)] on Target::RemoteProject and
  resolve_remote_project(); add search_project mock-peer unit test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ✨ feat: surface mounted remote projects in the TUI, italic (stage 4/5)

The local `codesearch serve` dashboard now shows peer-hosted indexes as
first-class rows, rendered italic (cyan) to signal they live on a peer —
matching `project=<peer>/<alias>` routing from stage 2.

- RepoRow gains `is_remote`; render_table + render_detail italicize the alias
  for remote rows (red-bold preserved for a remote in error state).
- tui.rs: background discovery task on a slow cadence (30s, constant) queries
  every peer's /status concurrently off the render tick, maps results through
  ReposConfig::mounted_remote_projects (honoring hide/rename), and feeds rows
  via a capacity-1 channel. Peers unreachable this round reuse an in-memory
  last-known alias list so a blip never drops a mount.
- Remote rows are display + query-routing only: existing idx<repos.len()
  guards make doctor/reindex/remove/info no-ops for them automatically.
- New constant REMOTE_DISCOVERY_INTERVAL_SECS (no magic number).
- Remove now-live #[allow(dead_code)] on mounted_remote_projects and
  remote_project_name.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ♻️ refactor: polish stage-4 review minors (remote discovery + detail)

Adopt the three review nits from the stage-4 pass (all non-blocking):
- Prune `last_good` to peers still in config each round so it can't grow
  unbounded in a long-lived serve.
- Log a tracing::warn when the discovery HTTP client fails to build instead
  of returning silently (observability).
- render_detail: a mounted remote project in error state now shows its alias
  red (was cyan), matching the table's error highlight. Local rows unchanged.
- Drop a stale "removed in Stage 2" comment above resolve_remote_project.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ✨ feat: split cloud indexer job into one repo per vendor (stage 5/5)

The index-job now builds/refreshes one index per immediate ${DOCS_DIR}/<vendor>
subfolder (akeneo, bynder, …) instead of a single monolithic "docs" repo.
Smaller per-vendor indexes rebuild faster, use less peak memory, warm quicker
on the restore-only serve side, and rank fairly (a small vendor is no longer
drowned by a large one). Each vendor is queryable as its own project and
mountable remotely as <peer>/<vendor> (stages 1-4).

- run_index_job: loop rebuild_repo over ${DOCS_DIR}/*/ (guarded — die if no
  vendor subfolders); verify EVERY vendor index is populated before upload so
  one empty build can't clobber the good snapshot.
- CRITICAL coupled fix: azcopy --exclude-path now built dynamically
  (docs_index_exclusions) to cover each <vendor>/.codesearch.db. --exclude-path
  is a relative-path-prefix match, so the old bare ".codesearch.db" only shielded
  a root-level (monolithic) index; per-vendor indexes live one level down and
  would otherwise be DELETED by --delete-destination on every sync (job AND serve
  cold-start restore). Legacy root entry kept for back-compat.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 📝 docs: mark remote-mounting plan complete + DB_DIR_NAME safety note

- AGENTS.md: all 5 stages marked ✅ with per-stage outcome; record deferred
  post-merge items (remote_project_cache persistence, shared search-body
  builder, per-vendor deploy step).
- entrypoint.sh: comment flagging the .codesearch.db ↔ src/constants.rs
  DB_DIR_NAME coupling (data-safety, no logic change).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 📝 docs: drop stale staging comment + clarify passthrough score doc

Comment-only cleanup from the final integration review (no logic change):
- Remove the obsolete "Fields read starting in Stage 2" note on
  Target::RemoteProject (all fields are now consumed).
- federated_project_search doc: replace "verbatim" with an accurate note that
  results pass through single-list RRF (scores are rank scores), matching the
  group path's rendering.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 🔧 fix: silence warmer index-add output in Docker build

`codesearch index add` prints a U+2795 (➕) emoji that crashed `az acr build`'s
log streamer on a Windows cp1252 console (colorama UnicodeEncodeError), killing
the build driver so ACR marked the run Failed. Redirect the warmer step's
output to /dev/null — the build log must not depend on the app's decorative
output. Model download (the step's actual purpose) is unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 🔧 fix: fold model warmup into builder stage (ACR COPY --from chained-stage bug)

ACR Tasks' classic builder fails at export with "failed to get layer <sha>:
layer does not exist" on `COPY --from=warmer` (a chained `FROM builder AS
warmer` stage). cb6/cb7/cb8 all failed at that exact step; the emoji-streamer
crash had masked it. This Dockerfile never built successfully — deployed v2.5
is an older image from a different Dockerfile.

Fix: warm the fastembed model inside the builder stage (a base-image stage)
and COPY --from=builder, which is proven reliable (binary/lib copies succeed).
Also replace the failure-masking `|| true` with a hard verification that the
model cache actually populated, so a failed download fails the build loudly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 🔧 fix: ship warmed model cache as a tarball (ACR symlink-tree COPY export bug)

Root cause found: cb9 still failed at `COPY --from=builder .../models` with
"layer does not exist", while single-file (codesearch) and small-dir (/out/lib)
copies from the SAME stage succeed. The fastembed/HuggingFace model cache is a
symlink tree (snapshots/ -> blobs/); ACR's classic builder cannot export a
cross-stage COPY of a symlinked directory tree.

Fix: tar the model cache to a single /models.tar.gz in the builder (symlinks
preserved inside the archive), COPY the one file into runtime (structurally like
the proven binary copy), and untar it there. The existing `chown -R app:app
/home/app` fixes ownership of the extracted cache.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(entrypoint): build vendor indexes sequentially to avoid OOM-kill

The index-job submitted all per-vendor build requests at once (rebuild_repo
returns on HTTP 202) and waited once afterward, so serve held every vendor's
embedding model + working set simultaneously and was OOM-killed (SIGKILL) on
the 8 GiB job limit, leaving wait_until_indexed polling a dead process forever.

Build one vendor at a time: submit -> wait_active_build_done -> verify -> next.
Peak memory is now a single index build regardless of vendor count.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ✨ feat: TUI info for remote mounts + disable inapplicable actions

The `i` (info) key now works on mounted remote projects (federation
peers): a new OverlayState::RemoteInfo shows the peer URL and the
peer-reported live status (status/lock/changes/calls/last-call) instead
of local on-disk index stats, which a mount does not have.

When a remote mount is selected, the footer now renders doctor / reindex
/ remove struck-through (CROSSED_OUT) so it is clear those local-index
actions do not apply to a peer-hosted mount. info / reload / quit / nav
stay enabled. The standalone remote TUI is unaffected (its rows are the
peer's own local repos, is_remote=false).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 📝 docs: document project-level mounting + cloud reindex hardening

CHANGELOG: new [Unreleased] section covering mounted remote projects
(project=<peer>/<alias>), the TUI remote-mount info panel + disabled
local-index actions, the per-vendor cloud indexer split, the sequential
build OOM fix, the local BuildKit build workflow, and the grep-guard hook.

README: new "Mounting a peer's projects" subsection under Federation
(project=<peer>/<alias>, italic TUI mounts, `i` info, disabled actions).

AGENTS: Stage 4/5 notes updated (TUI info/disabled + sequential build),
Current state bumped to v1.1.9 with deploy outcome; deferred list refreshed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ✨ feat: flash feedback when a disabled action is pressed on a remote mount

Applies the reviewer's non-blocking UX remark: pressing doctor / reindex
/ remove while a mounted remote project is selected was a silent no-op
(the struck-through footer hint was the only cue). Now it also flashes a
short "don't apply to a remote mount" confirmation, reinforcing which
actions are available on a peer-hosted mount.

Message centralised in one REMOTE_ACTION_NA const (no literal duplication).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* @
🔒️ fix: scrub customer identifier (aprimo→vendor-a) for public push

The pre-push customer-ref gate (blocks [Aa]primo|husqvarna|bayer|… on
pushes to develop/master) flagged 5 residual "aprimo" references after
merging federation into develop: vendor-list examples in AGENTS.md /
CHANGELOG.md, a doc-comment in repos.rs, and test data in
federation/mod.rs. Replaced all with the generic placeholder "vendor-a"
(other vendor names akeneo/bynder/… are not customer identifiers and stay).
The federation namespacing test still passes (arg + assert use the same
token). Full-tree scan now clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@

* ✨ feat: opt-in mounting of individual remote projects (remote_mounts allowlist)

Remote peers no longer auto-expose every project. The local user now
explicitly picks which individual per-vendor indexes to use, via a new
opt-in `remote_mounts` allowlist in repos.json — the single source of
truth for routing, discoverability, TUI display, and group fan-out.

- config: replace opt-out `remote_hidden` with opt-in `remote_mounts`;
  mounted_remote_projects() is allowlist-driven (no discovery arg);
  resolve_remote_project() gates on the allowlist; new group_remote_projects(),
  mount_remote_project()/unmount_remote_project(); reconcile() prunes
  stale/unknown-peer/malformed mounts + orphaned rename overrides.
- routing: `@peer` group fan-out now queries only the mounted <peer>/<alias>
  projects (per-project search_project), never the whole peer; federated_search
  reworked; obsolete whole-peer FederationClient::search removed.
- discoverability: list_projects gains a `remote_projects` array; scope_required
  advertises mounted names as first-class `project=` targets.
- cli: `remote available|mount|unmount|mounts` to inspect a peer and pick.
- tui: rows come from the allowlist; discovery only enriches live status.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 📝 docs: opt-in remote mount selection (remote_mounts allowlist)

Update CHANGELOG (Unreleased), README (Federation → mounting), and
AGENTS.md for the shift from auto-discover/opt-out to the explicit
`remote_mounts` allowlist: new `remote available|mount|unmount|mounts`
CLI, group fan-out restricted to mounted indexes, non-mounted =
unroutable, and mounts surfaced in list_projects/scope_required.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 🐛 fix: prune orphaned remote rename-overrides unconditionally in reconcile

Address reviewer minor: reconcile() dropped orphaned remote_alias_overrides
only when a mount was pruned that round, so a hand-edited removal from
remote_mounts left a stale override that could resurface as a surprise
rename on re-mount. Now retain overrides against the current mounted set
unconditionally.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ✨ feat: show peer index stats in remote-mount info overlay

The TUI `i` overlay on a mounted remote project previously showed only
peer URL + status. It now fetches the peer's on-disk index stats
(chunks / files / db size / model) on demand from GET /repos/{alias}/info
and renders them with a loading / ready / unavailable tri-state, giving
remote mounts parity with the local Info overlay.

- federation: add RemoteRepoInfo + FederationClient::repo_info()
- constants: add REPO_INFO_PATH_SUFFIX ("/info")
- tui_common: OverlayState::RemoteInfo gains RemoteStatsState; render
  chunk/file/db-size/model lines (or placeholder) after status
- tui: build_remote_info_overlay starts Loading; ShowInfo resolves
  peer+remote_alias and spawns an async fetch via the doctor channel;
  recv guard broadened to apply RemoteInfo results

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 🐛 fix: harden remote-mount info fetch against stale/None resolve

Review remarks on 1cea46b:

- Bump doctor_gen UNCONDITIONALLY before resolving the mount, so a
  still-in-flight doctor/remote-info reply (shared channel + counter)
  can never clobber the freshly-opened RemoteInfo overlay via the recv
  guard.
- When resolve_remote_project returns None (misconfig or a config
  reload racing the keypress), render stats as Unavailable instead of
  leaving the overlay stuck on "fetching…" forever.
- Build the base overlay once and clone it (derive Clone on
  OverlayState) rather than building it twice.
- Soften the Unavailable label to "stats unavailable from peer" since
  an HttpError from a reachable peer also lands here (not only
  unreachability).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 📝 docs: note peer index stats in remote-mount info overlay (CHANGELOG)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 🐛 fix: scope federated get_chunk to remote project (fixes ambiguous_chunk_id)

Remote search returned chunk_refs shaped "<peer>:<id>", dropping the remote
project alias. Since the peer is itself multi-repo and chunk_ids are only
unique within one index, every federated get_chunk failed with
ambiguous_chunk_id when the peer hosted more than one project (inriver,
aprimo, bynder, ...).

Client-side fix (the serve /chunk route already honoured ?project=):
- convert_remote_item now namespaces the ref as "<peer>/<alias>:<id>" and
  tags source as "<peer>/<alias>".
- parse_federated_chunk_ref (new, unit-tested) splits peer/alias/id; accepts
  the legacy "<peer>:<id>" shape for backward compatibility.
- FederationClient::get_chunk forwards project=<alias> (and omits group) when
  an alias is present, mirroring search_project; legacy refs still fall back
  to group scope.
- Docs on GetChunkRequest.chunk_ref + inline comments updated.

Tests: parse helper (4 cases), namespaced convert, and a live-peer get_chunk
asserting project=<alias> is forwarded and group omitted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ✅ test: cover legacy no-alias get_chunk group fallback (review minor)

Adds a live-peer test asserting that a non-namespaced chunk_ref
(remote_alias=None) forwards a `group` scope and omits `project`, closing
the coverage gap flagged in the Stage A review.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ✨ feat: split hooks command into `hooks git` and `hooks claude` (+ Claude installer in Rust)

The single `hooks install` (git post-checkout hook) is replaced by two
explicit subcommand groups (hard break, no back-compat alias for the old
`install`):

- `codesearch hooks git install [--path]` — the prior post-checkout worktree
  auto-register hook.
- `codesearch hooks claude install [--project]` — NEW: installs the Claude
  Code PreToolUse guard hooks (Grep -> grep-guard, Agent -> subagent-preamble)
  into ~/.claude (or ./.claude with --project). Rust port of
  integrations/claude-code/install.{sh,ps1}: scripts are embedded via
  include_str! (self-contained binary), settings.json is backed up and merged
  idempotently (keyed by exact command string), and the host shell is detected
  (pwsh on Windows, bash elsewhere).

The top-level command is now `hooks` (alias `hook` kept for muscle memory).
New module src/cli/claude_hooks.rs with unit tests for the settings merge
(empty/idempotent/preserve-unrelated/bad-shape) and host-shell command build.
README updated. Stage B will add a WebSearch/WebFetch guard to GUARD_HOOKS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ✨ feat: add web-guard hook — steer WebSearch/WebFetch to remote doc mounts

New PreToolUse guard (bash + pwsh twins) matching WebSearch|WebFetch: when
repos.json has remote projects mounted (.remote_mounts, e.g. cloud/inriver,
cloud/aprimo), it denies the first web call with guidance to search those
indexed mounts first (compact=false to read inline, then get_chunk). Same
5-minute retry-escape as grep-guard; when no mounts are configured it does
nothing. Detection reads repos.json directly (CODESEARCH_REPOS_CONFIG or
~/.codesearch/repos.json) — no binary spawn, no serve round-trip.

- integrations/claude-code/hooks/web-guard.{sh,ps1} (new)
- claude_hooks.rs: web-guard added to GUARD_HOOKS (embedded via include_str!)
- install.{sh,ps1}: register the WebSearch|WebFetch matcher for parity
- README: three-guard section, `hooks claude install` as the primary path

Also addresses the Stage C review minors: drop the redundant create_dir_all,
add tests for non-object hooks/root shapes + GUARD_HOOKS coverage, and
cross-reference the two documented install paths.

This closes the gap that let me reach for WebSearch instead of the mounted
inriver/aprimo docs — the guard now makes the preference structural.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 📝 docs: make web-guard guidance use get_chunk(chunk_ref=…) explicitly (review minor)

Clarifies the deny message in both web-guard twins: after searching a mount,
read full context via get_chunk with the returned federated `chunk_ref`
("<peer/alias:id>"), not chunk_id — the correct param for remote results.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 📝 docs: align SearchResultItem chunk_ref/source docs with namespaced format (final review remark)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 📝 docs: add remote/federation + index --remote rows to CLI Reference table

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ✅ test: replace fixed sleep with bounded readiness poll in live-peer federation tests

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 📝 test: add remote-mount semantic-findability test scenario (Run 1: PASS)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ✨ feat: serve incrementally reindexes custom-kb on each KB pull

serve mode previously git-pulled the custom-kb repo every
KB_PULL_INTERVAL_SECS but nothing triggered indexing afterward — the
"periodic incremental reindex (REINDEX_INTERVAL_SECS)" the comments
promised does not exist in code. Pulled KB articles therefore only
became searchable on the next cold-start warmup.

The KB pull loop now detects when a pull moves HEAD and fires
POST /repos/custom-kb/reindex (incremental) against the local serve, so
new/changed articles are searchable without a restart. Incremental
refresh re-embeds only the delta and the KB corpus is small, so it fits
the 1-2 GiB serve replica; the heavy DOCS corpus stays index-job-only.

- repos open read-write by default (try_open_stores), so custom-kb on the
  serve's local disk reindexes in-place — no Rust change needed
- fire-and-forget 202; 409 (concurrent/FSW pickup) is expected + harmless
- first pull fires after the interval, after Phase-1 warmup releases the
  KB write lock, so no warmup contention
- fixed the stale REINDEX_INTERVAL_SECS comments (header, env doc, run_serve)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 📝 docs: scope cloud "read-only serve" claims to the custom-kb reindex exception

Follow-up to the serve custom-kb incremental-reindex change. The cloud
docs still described serve as strictly restore-only / never-reindexes,
which is no longer accurate: serve now runs a memory-bounded incremental
reindex of the small custom-kb repo after each KB pull.

- integrations/cloud/README.md: scope the "read-only / never writes"
  statements to the DOCS corpus; document the custom-kb incremental
  reindex as the sole in-process write (fire-and-forget 202, incremental
  only, HEAD-change gated, 409/404 benign). Correct the management-verbs
  note — incremental reindex of a registered repo succeeds; only add /
  reindex --force still require a read-write peer.
- AGENTS.md: note the custom-kb incremental step as the scoped first
  realization of the "incremental in-process on serve" redesign; DOCS
  corpus stays job-only (the OOM that motivated the split). Nuance the
  remote-write-verbs note accordingly.
- entrypoint.sh: distinguish HTTP 404 (custom-kb not yet in the restored
  snapshot — expected during bootstrap) from a genuine failure WARN
  (addresses review remark).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 📝 test: add section F — cross-vendor overlap + isolation scenarios (Run 1)

Complements section B (isolation) with the inverse: a concept shared
across vendors must surface hits from multiple vendors at once via
group="docs" RRF fusion, while domain-specific concepts stay absent from
the opposite domain. 5 scenarios (F1–F5) covering PIM/DAM overlap and
isolation, all executed and passing in Run 1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix: COPY integrations/claude-code/hooks into Docker builder

The cloud image build broke on the release compile:

  error: couldn't read `.../integrations/claude-code/hooks/grep-guard.sh`:
  No such file or directory (os error 2)

src/cli/claude_hooks.rs embeds the six hook scripts at compile time via
include_str!("../../integrations/claude-code/hooks/*"), but the Dockerfile
only copied Cargo.*, build.rs and src/ into the builder, so the hooks
subtree was absent from the build context. This is latent since the
hooks-split feature landed — v2.7 predates it, so this is the first image
build to hit it. Local builds compile because the tree is on disk.

Copy only that subtree (the exact paths include_str! needs) before the
cargo build. Verified no other include_str!/include_bytes! in src/
references paths outside src/.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix: pin shell scripts to LF via .gitattributes (CRLF broke cloud image)

The v2.8 cloud image failed to start: `env: 'bash\r': No such file or
directory`. Root cause: core.autocrlf=true rewrites docker/entrypoint.sh
to CRLF in the Windows working copy, and the Docker build copies the
working copy (not git's LF blob) into the image — so the CRLF shebang was
baked in and the container could not exec bash.

Pin *.sh (and docker/entrypoint.sh explicitly) to eol=lf so the working
copy is always LF regardless of the local autocrlf setting, and the image
can never regress to a CRLF shebang. entrypoint.sh already normalized to
LF in the working copy; the git blob was already LF.

Deployed image tag v2.9 carries the LF fix and is verified live (serve
boots, /healthz 200, "KB auto-pull loop started (git pull + reindex-on-
change...)" present, no bash\r error).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🔧 chore: pre-commit hook does cargo fmt only (drop per-commit version bump + rebuild)

The hook auto-bumped the Cargo.toml patch version and ran `cargo build`
on every feature-branch commit. That blocked each commit for minutes on a
debug build nobody deploys, and made the deployed binary constantly drift
from HEAD (forcing a manual release rebuild to re-sync).

The auto-bump was redundant: build.rs already appends a unique
"+<commit_count>" suffix (git rev-list --count HEAD) to every build, so
each commit is uniquely identifiable without churning the base version.

Now the hook only runs cargo fmt (+ stages reformatting). The base version
is bumped deliberately at release time. Updated RELEASING.md accordingly.
Installed the new hook into .git/hooks/pre-commit (this commit already ran
it — fast, no bump).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🔧 chore: pin extensionless hook scripts to LF in .gitattributes

scripts/pre-commit and .githooks/* are shell scripts without a .sh
extension, so the *.sh rule didn't cover them. On a Windows checkout
(core.autocrlf=true) they become CRLF, and copying scripts/pre-commit
into .git/hooks then yields a `#!/bin/bash\r` shebang that breaks the
hook. Pin them to eol=lf, same as the other shell scripts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ✨ feat(serve): KB near-instant propagation via cheap remote-HEAD poll

The serve-mode KB refresh loop previously did a full `git pull --ff-only`
only every KB_PULL_INTERVAL_SECS (default 900s), so a pushed KB edit took
up to ~15 min to become searchable in the cloud.

Now the loop cheaply polls the remote HEAD every KB_POLL_INTERVAL_SECS
(new, default 30s) via `git ls-remote origin <branch>` — ref advertisement
only, no object transfer — and performs the real pull + incremental reindex
only when the remote SHA actually moved. A pushed edit propagates in
~seconds instead of minutes.

KB_PULL_INTERVAL_SECS (default 900) is retained as a safety-net: it forces
a full pull at least that often even when the cheap poll saw no change or
ls-remote failed, self-healing a missed poll. ls-remote uses the stored
`origin` remote so the PAT never lands on argv. No codesearch core (Rust)
changes — trigger lives entirely in deployment glue where git already runs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: redact platform name -> example-dam to satisfy customer-ref pre-push guard

The local pre-push guard scans tracked files for customer/vendor identifiers
and flagged the bare platform name "aprimo" (as mount `cloud/aprimo` and in
prose) across README, the remote-mount test scenario, and both web-guard hooks.
Replaced every occurrence with the neutral placeholder `example-dam`; no
functional/code change. Line endings preserved (LF for scripts).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 📝 docs: add missing KB-propagation changelog entry + filter_path federation caveat

Audit found two doc gaps: the KB near-instant propagation feature
(commit bd90ec2) had no CHANGELOG entry, and filter_path's documented
zero-result behavior on federated/mounted projects (observed live via
the aprimo_mcp consumer) wasn't captured anywhere in README or
CHANGELOG. Documents the known limitation + client-side over-fetch
workaround until the root cause is isolated with a live hub+peer repro.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix: apply federated filter_path client-side on namespaced result paths

Federated search forwarded filter_path to the peer, which matched it
against its own un-namespaced store paths (and, in serve mode, the wrong
project root via build_semantic_response using self.project_path instead
of the routed alias root). The caller only ever sees the
`<peer>/<alias>/…` namespaced path, so a server-side match dropped every
hit regardless of value — the "0 results" symptom observed live via the
aprimo_mcp consumer.

Fix: stop forwarding filter_path to the peer; over-fetch and post-filter
client-side on the namespaced paths in both federated_project_search
(project passthrough) and federated_search (group fan-out), via a shared
retain_by_filter_path helper + is_meaningful_filter guard. Consumers no
longer need the over-fetch+post-filter workaround.

The underlying server-side root mismatch still affects filter_path on a
LOCAL project routed through serve (non-federated); documented as a
follow-up in CHANGELOG known-limitations. stdio single-repo is unaffected.

Tests: retain_by_filter_path unit tests (matching prefix, none/blank
no-ops, no-match empties). Full lib suite 566 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix: relativise filter_path against the routed project root in serve mode

For search(project=<local-alias>) or a local group served by codesearch
serve, build_semantic_response relativised result paths against the
service's own project_path instead of the ROUTED project's root, so the
absolute stored path never stripped and filter_path dropped every hit
(0 results for any value). Only stdio single-repo — where project_path
IS the repo root — worked.

Fix: pick_filter_root() resolves the correct root per result — the routed
alias's root for single-project routing, the longest matching alias root
for multi/group, and the service project_path only as the stdio fallback.
filter_path is now a repo-relative prefix in every routing mode. This is
the non-federated companion to the client-side federated fix (1241963);
together they close the filter_path scoping gap end to end.

Tests: pick_filter_root unit tests (routed alias, longest-match multi,
stdio fallback). Full lib suite 569 passed. stdio behaviour unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🔒️ fix: scrub customer identifier (aprimo→vendor-a) in mcp tests

Filter_path test fixtures used the real customer alias; replace with the
established generic placeholder so the pre-push customer-ref gate passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix: harden `hooks git install` (windows path, worktree common-dir, chain existing)

The generated post-checkout h…

v1.1.28

Toggle v1.1.28's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Release v1.1.28 — filter_path scoping + opt-in remote mounts (flupked…

…e#139)

* docs: add federation feature plan

* feat(serve): add REST search/find/explore/chunk endpoints for federation

Expose the read-only MCP tools (search, find, explore, get_chunk) over plain HTTP+JSON so a remote codesearch serve can be queried for federation WITHOUT an MCP session. Each REST handler constructs a per-request CodesearchService bound to the live ServeState, invokes the existing #[tool] method, and returns the tool's JSON payload unwrapped from CallToolResult. The new routes inherit the existing auth layers (require_auth_for_network on network binds), so no new auth code is needed. Adds a rest_routes_are_registered integration test.

* fix(serve): share embedding service across REST + MCP sessions

Previously each CodesearchService started with embedding_service=None, so
per-request REST semantic search reloaded the ONNX model (~100ms-2s) on every
call. Mirror the symbol_registry pattern: ServeState now owns a shared
Arc<Mutex<Option<EmbeddingService>>> and exposes an embedding_service() getter;
new_for_serve binds to it, so the model loads once per serve instance (lazily
on first semantic query) and is reused by all MCP sessions AND REST handlers.
The standalone new() path keeps its own Arc. Addresses review remark on the
REST-endpoint commit.

* feat(federation): add remote-peer config + federated search/get_chunk (Phase 2)

ReposConfig gains a `remotes` map of RemotePeer{url, api_key, group, timeout_secs}.
Group members may reference a remote peer via an "@"-prefix (e.g. "docs": ["@cloud"]);
resolve_group_targets/split_group_targets return mixed Local/Remote Target lists
(the virtual "all" group stays local-only). New `federation` module exposes a
FederationClient that calls the Stage-2 REST endpoints (/search, /chunk/:id) with
per-peer bearer auth over a shared reqwest client. The `search` tool, when its
group contains remotes, fans out to each peer concurrently, converts remote hits
into source-tagged SearchResultItems (chunk_ref = "peer:id"), and RRF-merges
local + remote ranked lists; unreachable peers degrade gracefully into a
`warnings` array (never hard-fail). The `get_chunk` tool transparently proxies
namespaced chunk_refs ("peer:id") back to the owning peer. 16 new unit tests
(config, federation client, merge/conversion helpers). find/explore federation
deferred.

* fix(federation): honest low_confidence for federated results

The previous `f.score < 0.15` threshold was unsatisfiable: merge_ranked_lists
reassigns every score to the RRF value 1/(k+rank+1) (max ≈ 0.048 for k=20), so
every federated search that returned ANY hit was flagged low_confidence=true.
RRF-fused scores are not comparable to single-source embedding/BM25 thresholds,
so no score cutoff is meaningful here. Now low_confidence is flagged only when
the merged set is empty (a genuine "nothing found" signal to the agent). Also
drops the unused `_limit` parameter from build_federated_response.

Addresses review remark on the Phase 2 federation commit.

* docs: align federation plan with shipped Phase 1+2 reality

* [worker] stage 1/2: surface project→group membership in scope_required

Add ReposConfig::project_groups() — inverse index mapping each repo alias to
the named group(s) it belongs to (sorted/deduped; excludes the virtual "all"
group and "@remote" refs; omits aliases in no named group).

Expose it as a new `project_groups` field in the scope_required error and
sharpen `hint_for_agent` so an agent picking a single project is told to
prefer group= when that project belongs to a group (e.g. a separate config /
import-data repo). Adds 2 unit tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [worker] stage 2/2: add per-repo group membership to status(kind=projects)

Add a `groups` field to RepoInfo listing the named group(s) each repo belongs
to (via ReposConfig::project_groups()), populated in both the serve and stdio
branches of list_projects. Lets an agent inspecting `status` see that e.g.
BAYR.Aprimo is part of group BAYER and prefer a cross-repo group= query.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [worker] final review: document implicit "all"-group exclusion in project_groups()

Addresses the sole (informational) review remark: clarify that project_groups()
excludes the virtual "all" group implicitly (it is never persisted in
self.groups), so a future change that starts persisting "all" must filter it
explicitly. Doc-comment only; no behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [worker] stage 1/3: add unauthenticated /healthz probe for ACA + cloud deploy plan

- /healthz: fixed {"status":"ok"} body, no version/repo info, exempt from
  require_auth_for_network so container-orchestrator probes reach it on a
  network bind without the Bearer key. /health stays auth-protected.
- HEALTHZ_PATH constant; route registered; log-spam suppressed.
- Test: healthz reachable without key on simulated network bind, /health 401.
- docs/federation-cloud-deployment.md: phased Azure plan (role-assignment-free:
  SAS + inline ACA secrets), verified rights, PIM-Contributor on Aprimo RG.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [worker] stage 2/3: container image, blob-sync entrypoint, serve cloud keep-warm

Container artifacts:
- Dockerfile: multi-stage build + fastembed model pre-warm baked into the image
  (ONNX loads from a local path; never streamed from blob) + slim runtime with
  azcopy + git. .dockerignore keeps target/ and tool dirs out of the context.
- docker/entrypoint.sh: snapshot-restore -> azcopy sync (+ optional KB git pull)
  -> serve; background loop does incremental reindex + periodic index snapshot
  to a separate blob container. No FSW; full-on-start + incremental-on-timer.

serve cloud keep-warm (2h-idle-then-suspend for ACA scale-to-zero):
- --keep-warm-url / CODESEARCH_KEEP_WARM_URL + --idle-suspend-secs /
  CODESEARCH_IDLE_SUSPEND_SECS (default 7200). Serve self-pings its own ingress
  /healthz while the most-recent real tool call is younger than the idle window,
  then stops so ACA suspends; next real query wakes it. No Logic App, no managed
  identity, no role assignment.
- ServeState::most_recent_tool_call(); constants for env vars + defaults.

docs: option D (scale-to-zero + blob snapshot) with actual SSOT/Aprimo resources.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [worker] stage 2/3: fix Dockerfile for ACR/portable build

Three build-portability fixes found via review + real builds:
- COPY build.rs into the builder: env!("CARGO_PKG_VERSION_FULL") (main.rs,
  cli.rs) needs the var build.rs emits; it falls back to "0"/"unknown" without
  .git, so the build is reproducible without repo history.
- Drop BuildKit `--mount=type=cache`: ACR Tasks uses the classic builder which
  rejects `--mount`. ACR builds fresh anyway.
- Base images bookworm -> trixie (glibc 2.41): the prebuilt onnxruntime pulled
  by `ort` references glibc-2.38+ C23 symbols (__isoc23_strtoll), so linking on
  bookworm (2.36) failed. Runtime moved to trixie too; libssl3 dropped (reqwest
  uses rustls, no OpenSSL at runtime).

Verified: full image builds clean locally on trixie (all 24 steps).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [worker] stage 2/3: fix entrypoint repo registration + azcopy + warm-repo bake

Found via live ACA deployment + container logs:
- azcopy sync: drop --compare-hash=MD5. It stores the hash in a user_xattr,
  which the container overlayfs does not support -> every transfer failed
  ("1 Failed, 0 bytes") so docs never synced. Default sync (LMT+size) needs no
  xattr and works.
- Dockerfile: copy ONLY ~/.codesearch/models from the warmer, not the whole
  dir. The warmup writes a repos.json registering "/tmp/warm", which baked a
  stale "warm" repo into the runtime image.
- Repo registration: `serve --register` adds the alias but does NOT build the
  initial index (create_index is ignored in the CLI handler), and an unindexed
  repo is auto-pruned at warmup -> "Unknown alias 'docs'". The entrypoint now
  registers each repo via POST /repos (create DB + index + warm) once serve is
  live, instead of --register.

Verified end-to-end on Azure Container Apps: cold start -> auto-register ->
index -> federated /search returns the doc within ~5s.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [worker] stage 3/3: record live Azure deployment of federation cloud peer

ACA app codesearch-serve live in Delaware.SSOT/Aprimo (scale-to-zero, HTTPS,
snapshot + keep-warm). End-to-end verified: cold start -> auto-register ->
index -> federated search. Documents resources, FQDN, and the az-CLI emoji
log-stream caveat (build local + docker push, not az acr build).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [worker] stage 1/1: add `codesearch remote` command for federation peers

Manage remote serve peers from the CLI instead of hand-editing repos.json:

  codesearch remote add <name> --url <url> [--api-key K] [--group G]
                               [--timeout-secs N] [--into-group LOCAL_GROUP]
  codesearch remote list
  codesearch remote rm <name>

Model (ReposConfig):
- add_remote(name, peer): validate non-empty name, reject a leading '@'
  (the reference prefix is added automatically), require non-empty url.
- remove_remote(name): drop the peer AND prune every "@name" reference from
  groups, dropping any group left empty.
- add_remote_to_group(group, name): idempotently push "@name" into a group
  (created on demand); rejects reserved "all" and unknown peers.
- groups_referencing_remote(name): sorted groups wiring a peer (for `list`).

--into-group wires "@name" into a local group in one step so the peer is
immediately queryable; without it, `add` prints the follow-up hint.

Hoisted the federation default timeout (15s) into
constants::DEFAULT_REMOTE_TIMEOUT_SECS so the client and the CLI share one
source of truth (no duplicated magic number).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [worker] stage 1/1: fix review remarks on `remote` command

- remote rm: trim the name before lookup/printing, matching the trimming
  `add`/`--into-group` already do (so `rm "  cloud  "` finds `cloud`).
- federation: demote the inert doc-comment above the `use ... as
  DEFAULT_TIMEOUT_SECS` alias to a plain comment (a `///` on a `use` is dead).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [worker] feat: split cloud entrypoint into serve / index-job modes

Decouples the memory-heavy index build from light serving so the long-running
Container App can run small (1-2 GiB) while a short-lived Container Apps Job does
the full embed on a big replica (4-8 GiB), avoiding OOM (exit 137) on serve.

CODESEARCH_RUN_MODE:
- serve (default): restore the prebuilt snapshot from blob and serve READ-ONLY.
  No register / reindex / snapshot — never does heavy work. Warns if no snapshot.
- index-job: restore (incremental) -> sync blob -> drive a local serve to
  build/force-reindex -> wait until /status clears "indexing" -> upload snapshot
  -> exit 0. New helpers: wait_healthz, register_or_reindex, wait_until_indexed.

Deployed: image tag v2-modes; ACA Job 'codesearch-indexer' (index-job, 2 vCPU/
4 GiB, Manual trigger); serve app resized to 1 vCPU/2 GiB with RUN_MODE=serve.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [worker] feat: robust index-job rebuild (DELETE+POST) + deployment doc

The /reindex?force=true endpoint returns 500 in the cloud deployment, so the
index-job could not pick up new/changed docs. Replace the reindex call with the
proven path: when the repo is already registered (restored from a snapshot),
DELETE /repos/{alias} then POST /repos for a clean full rebuild. The restored
embedding cache makes unchanged docs cache-hits, so only new/changed docs cost
real work — appropriate for the Job's big replica.

Also harden wait_until_indexed against the DELETE+POST race: phase 1 waits for a
build to actually be observable (status "indexing", or an already-ready state for
a cache-instant rebuild) so the brief post-DELETE gap is never mistaken for
"done"; phase 2 waits for "indexing" to clear with the repo present + ready.

docs/federation-cloud-deployment.md: document the serve/index-job split, the
codesearch-indexer Job, the 1 vCPU/2 GiB restore-only serve, and image v2.1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [worker] docs: honest cost note for index-job rebuild

The DELETE+POST rebuild is a full re-embed (~20-25 min for the 2737-doc corpus on
the job replica), not a cache-fast pass — observed in a live run. Correct the
entrypoint comment and deployment doc to state this plainly; the heavy cost is by
design (it lives in the Job, never in serve).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [worker] fix(index-job): incremental /reindex + pre-upload index guard

The cloud index-job's rebuild_repo used DELETE + POST /repos: it deleted the
~60 MB index dir then immediately reopened it (racy on the container overlayfs,
surfaced as "register failed" 500 + a stalled build that never refreshed the
snapshot), and it was a full ~20-25 min re-embed every run.

Switch to the safe, light path:
- already-registered repo (snapshot restored) -> POST /repos/<alias>/reindex
  (incremental, no force): opens the existing index in place, re-embeds only
  deltas, returns 202. No DB delete, no reopen race.
- not-yet-registered (cold build, no snapshot) -> POST /repos {path} (full build).
- /reindex?force=true still avoided (returns 500 in this deployment).

Honesty + safety:
- api_code() captures the HTTP status instead of swallowing it with >/dev/null,
  and a non-2xx/202 rebuild response now die()s (no silent "register failed").
- verify_index_ready() gates upload on GET /repos/<alias>/info chunks > 0, so a
  broken/empty build can never clobber a known-good snapshot.

Doc updated; job sizing noted as 4 vCPU / 8 GiB, 5400s.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [worker] fix(index-job): stop corpus sync from deleting the index

Root cause of the v2.2 run failure (HTTP 500 "Repo 'docs' is locked by another
codesearch process"): sync_blob runs `azcopy sync --delete-destination=true`
from the blob (which holds only .md source files) into ${DOCS_DIR}, but the
search index lives INSIDE that dir at ${DOCS_DIR}/.codesearch.db. azcopy treated
the whole restored index as "extra" and DELETED it, leaving the incremental
reindex with no base + stale locks.

Fixes:
- sync_blob: --exclude-path=".codesearch.db" so the corpus sync never touches the
  index. This also protects the SERVE app's restored index on cold start (same
  code path) — previously a cold start could silently wipe the served index.
- restore_snapshot: delete stale .writer.lock / .tantivy-*.lock / lock.mdb that a
  prior snapshot baked in (the source of the "locked by another process" 500). A
  fresh container has no other process, so any lock present is stale.
- upload_snapshot: exclude *.lock / lock.mdb from the tar so future snapshots stay
  clean.
- rebuild_repo: accept HTTP 409 (reindex already in progress) as "wait for it"
  instead of die — serve's startup warmup may already be refreshing the repo.

The pre-upload verify guard correctly prevented the failed v2.2 run from
clobbering the good snapshot.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [worker] fix(index-job): let serve warmup own the refresh (no competing reindex)

The v2.3 run still hit HTTP 500 "Repo 'docs' is locked by another codesearch
process with write access" — but this time it was a LIVE lock, not a stale one,
and the index was no longer being deleted (the sync exclude worked).

Cause: serve's Phase-1 startup warmup (warmup_repo) opens every registered repo
in WRITE mode and runs an incremental refresh on startup, holding the LMDB write
lock for the whole refresh. The job's explicit POST /repos/<alias>/reindex opened
a SECOND write handle on the same env -> 500. The warmup already does exactly the
incremental refresh the job wanted.

Fix: for an already-registered repo, the job no longer issues any reindex. It lets
the startup warmup do the refresh and just waits for the repo to flip from "closed"
(mid-warmup) to "warm" (refresh done) in wait_until_indexed(), then verifies
chunks>0 and snapshots. Only the cold (unregistered) case still POSTs /repos.

The pre-upload verify guard again prevented the failed v2.3 run from clobbering
the good 69 MB snapshot. Doc updated to describe the warmup-driven refresh and the
two invariants (sync never touches .codesearch.db; never compete with the warmup).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [worker] stage 1/3: add FederationClient management API (list/add/remove/reindex repos on a peer)

Add REPOS_PATH constant and the remote index-management client surface:
ManagementOutcome (Ok/HttpError/Unreachable), RemoteStatus/RemoteRepoStatus,
RemoteRepoAdded/Removed/RemoteReindexResult structs, and FederationClient
methods list_repos/add_repo/remove_repo/reindex backed by a shared
send_management helper. Includes mock-peer unit tests (6 new, all passing).
Dead-code warnings are expected — Stage 2 (CLI) consumes this API.

* [worker] stage 2/3: add --remote flag to index verbs + Reindex variant

* [worker] stage 2/3: fix review remarks (--json without --remote now rejected)

* [worker] stage 3/3: document the --remote index management flag in README + cloud deployment doc

* [worker] stage 3/3: fix review remarks (reconcile --remote docs with the read-only cloud peer)

* [worker] stage 3/3: polish README caveat precision + note writable-peer requirement on per-vendor recipe

* [fix] index rm: resolve argument by alias before treating as path

`codesearch index rm <alias>` (without --remote) previously treated the
alias as a filesystem path and failed with os error 2 during canonicalize.
Now `remove_from_index` resolves the argument against registered aliases
first (ReposConfig::resolve), falling back to path interpretation only
when it is not a known alias. The resolved path is also forwarded to
try_delegate_rm_to_serve so serve delegation works by alias too.

* [cli] add `ls` visible alias to all `list` subcommands

Adds `#[command(visible_alias = "ls")]` to the three `List` variants so `codesearch index ls`, `codesearch groups ls`, and `codesearch remote ls` work as shortcuts for `list`.

* [docker] serve: periodic KB git-pull loop (refresh custom KB without restart)

Background loop in run_serve re-runs sync_kb every KB_PULL_INTERVAL_SECS (default
900s) when KB_GIT_URL is set, so serve's 15-min incremental reindex picks up newly
pushed custom KB without a restart. Includes the pre-commit hook's rustfmt + version
bump to 1.0.236 (rebuild validated by the v2.5 image build).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [fix] serve: active_sessions underflowed to u64::MAX via per-request REST services

Drop for CodesearchService called session_disconnected() whenever serve_state
was Some. But new_for_serve is constructed by TWO paths:
  - serve service_factory (MCP sessions): calls session_connected() first, so
    Drop is balanced.
  - make_service (per-request REST handlers for /search /find /explore /chunk):
    creates the service with serve_state=Some but NEVER calls session_connected().
    Its Drop fired session_disconnected() -> AtomicU64::fetch_sub on 0 wraps to
    u64::MAX. Every REST request underflowed the counter.

Fix: add a `tracks_session: bool` field to CodesearchService (default false).
Only the serve service_factory calls a new mark_session_tracked() to set it
true. Drop now only decrements when tracks_session is true, so per-request REST
services no longer touch active_sessions.

* [docs] 1.0 GA: federation security analysis, changelog [1.0.0], version bump

- README: new top-level `## Security` section consolidating serve access
  control + a federation threat model (trust model, secret storage/transport,
  redirects, serve-side enforcement, write verbs, cross-instance isolation).
  Moved the serve-auth env-var block out of `### Security` under Configuration
  into the new section.
- CHANGELOG: add `[1.0.0] - 2026-07-01` GA entry (federation, --remote index
  management, cloud topology, README security section; fixes: active_sessions
  underflow, index rm alias resolution, ls alias).
- Cargo.toml / Cargo.lock: version 1.0.236 -> 1.0.0.

* Add Claude Code integration: enforce codesearch-over-grep via hooks

Claude Code's MCP tool schemas are deferred and Grep/Glob are always
loaded, so the advisory initialize instructions get skipped more often
than on other clients. Ships two PreToolUse hooks (ps1 + sh) that make
the preference structural: grep-guard blocks/redirects the first Grep
against an internal repo path when codesearch is available (fails open
otherwise, retry-unblocks within 5 min), and subagent-preamble injects
codesearch guidance into every spawned Agent prompt, since subagents
don't inherit AGENTS.md or MCP initialize instructions at all.

Includes install.ps1/install.sh for one-step setup at user or project
scope, idempotent and non-destructive to existing settings.json.

* [docs] 1.0 GA: disclose changelog condensation in [1.0.0], fix review remarks

- Add `### Changed` note under [1.0.0] disclosing that pre-GA history
  ([1.0.72]–[1.0.208]) was condensed to one-line summaries.
- Reword the [1.0.72] line from "First stable release" to "Initial
  multi-repo release" to avoid a double "first stable" claim with the
  [1.0.0] GA entry.
- Collapse the stray double blank line before [1.0.209].

* docs(federation): drop stale cloud-deployment plan; point to canonical arch doc

federation-cloud-deployment.md drifted from the live deployment (serve v2.5 /
indexer v2.4, custom KB pulled from github.com/develterf_dlwr/kb). The verified
current-state architecture now lives in
aprimo_mcp/docs/knowledge-base-architecture.md (with SVG). Kept federation-feature.md
as the generic Rust engine spec, repointed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* [fix] serve: close LMDB env before DB delete (await FSW task) — Windows per-repo remove without restart

On Windows a process cannot delete its own open mmap'd file, so pressing
'r'/DELETE /repos/:alias failed with a sharing violation on data.mdb/
lock.mdb while the background file-system-watcher (FSW) task held clones
of Arc<SharedStores>/Arc<IndexManager> that keep the LMDB Environment open.

Track each repo's FSW JoinHandle in a new fsw_tasks DashMap. remove_repo
and restart_fsw now await_fsw_shutdown() after cancelling the task, so the
task's Arc clones drop, the LMDB env closes (mdb_env_close), and Windows
releases the file handles BEFORE the DB directory is deleted. Per-repo,
in-process, no serve restart needed.

- ServeState.fsw_tasks: DashMap<String, JoinHandle<()>>
- await_fsw_shutdown(): bounded 5s await, detaches on timeout
- remove_repo: await before delete (the actual fix)
- restart_fsw: await old task before spawning new + track new handle
- get_or_open_stores / spawn_fsw_for_warm: track spawned FSW handles
- reload shrink: detach dropped handles (no DB delete there)

* [fix] serve: address review remarks (fsw_tasks hygiene + transient-race doc + test)

Addresses the PASS-WITH-REMARKS review of ab462e2 (no Critical):

- remove_repo: expand the doc comment on the delete retry-loop to explain the
  transient in-flight-query/warmup holder race — await_fsw_shutdown drops the
  *persistent* holders, but a transient spawn_blocking clone can still keep the
  LMDB env open briefly; the retry-loop is the documented fallback (warned +
  non-fatal; repo is already unregistered, so a lingering dir is cosmetic).

- evict_idle_repos + phase-1 prune: detach the FSW handle (fsw_tasks.remove)
  for consistency with the reload-shrink path. Neither deletes the DB dir
  (evict frees memory for re-open; prune's DB is already gone), so a plain
  detach is correct — the cancelled task drains on its own.

- Add 2 regression tests for await_fsw_shutdown bookkeeping:
  * joins the task to completion + removes the fsw_tasks entry (proves the
    join happens, guarding the Arc-drop→LMDB-close chain the Windows fix relies on)
  * no-ops (no panic) on a missing alias (Warm/Readonly repos have no FSW task)

* [feat] serve: GET /remotes endpoint — list configured federation peers (observability)

Adds a read-only /remotes endpoint (companion to /status) so an operator can
see which federation peers this serve fans out to. Reads the `remotes` map
from repos.json and returns alias/url/group/timeout_secs per peer, sorted by
alias.

- REMOTES_PATH constant added next to STATUS_PATH.
- Status-like auth policy: NOT in require_admin_auth's management set, so
  reachable without the admin key on localhost and protected only by
  require_auth_for_network on network binds (same as /status, /info, /doctor).
- Dedicated RemotePeerInfo response struct that structurally omits api_key —
  the shared secret cannot be serialized even by accident. The handler never
  reads RemotePeer.api_key.
- Degrades gracefully to {"remotes": []} on a repos.json read error.

* [test] serve: regression test — /remotes never serializes api_key

Locks the defense-in-depth on GET /remotes: RemotePeerInfo (the response
projection struct) has no api_key field, so serde cannot leak the peer's
shared secret. This test fails if a future change adds api_key to the
response shape or lets the key value through. Addresses review remark on
d137069.

* [release] 1.1.0: federation GA — version bump

The federation feature (remote peers, --remote index management, cloud
topology, /remotes endpoint) is a significant new capability over the
existing 1.0.x line, so it ships as a minor bump (1.1.0) rather than
claiming "first stable" — the project was already stable at 1.0.236.

- Cargo.toml/Cargo.lock: 1.0.2 -> 1.1.0
- CHANGELOG: [1.0.0] "First stable (GA)" reframed to [1.1.0] "Federation release"

* [docs] drop federation-feature.md; document Claude Code hooks in README

Remove docs/federation-feature.md — the standalone design doc added drift/broken-link risk (federation-cloud-deployment.md was already dropped); README + code comments are self-contained.

README: expand the one-line Claude Code hooks pointer into a self-contained subsection (grep-guard + subagent-preamble, install commands, CODESEARCH_SERVER caveat); drop the now-dangling 'See docs/federation-*.md' line.

src: drop the 3 dangling refs to the deleted doc (federation/mod.rs module doc; mcp/mod.rs x2).

* [docs] generic cloud-deployment guide (integrations/cloud); mermaid + changelog ref fix

- integrations/cloud/README.md: fully generic, environment-agnostic cloud
  deployment guide (Azure Container Apps build/serve split, scale-to-zero
  serve replica, snapshot-restore lifecycle, --remote management). No
  customer/specific-resource names — all <placeholder>s.
- README.md: mermaid diagram now shows the cloud serve peer node
  (ServeRouter -->|"@peer fan-out · TLS"| CloudPeer) and the subgraph is
  renamed "Serve Mode (multi-repo + federation)".
- CHANGELOG.md: fix dangling ref docs/federation-cloud-deployment.md
  (folder was dropped) -> integrations/cloud/README.md in [1.1.0] entry.

* [scrub] remove customer identifiers from tracked files (public repo prep)

* [fmt] cargo fmt --all (repos.rs test assert, serve/mod.rs constants import)

* [fix] claude-code: grep-guard ignores running process, requires local index

The grep-guard hook previously treated "a codesearch process is running" as sufficient evidence that codesearch is available for the current repo. But codesearch serve commonly runs as a persistent background hub covering many registered repos, so that process is alive on a dev machine almost all the time — making the hook fire in every directory, including unindexed ones (real false-positive hit in use).

Now the guard only treats codesearch as available FOR THIS REPO when it finds a local .codesearch.db at the git root, or an explicit CODESEARCH_SERVER env var (escape hatch for pure remote-serve setups with no local index). Process-presence check removed from both ps1 + sh.

README updated to document the precise availability signal + the rationale.

* Fix claude-code hooks: tell the model to pass project=/group= in serve mode

In multi-repo serve-hub mode (codesearch serve with several registered
repos) every search MUST specify project= (single repo) or group= (cross
-repo); omitting both returns a scope_required error, and a wrong alias
returns Unknown alias. The hook guidance previously showed only
search(query=..., mode="semantic") with no scope, so the model would get
blocked from Grep, call codesearch exactly as instructed, hit
scope_required, conclude "codesearch is broken", and fall back to Grep on
the 5-minute retry-unblock -- looking exactly like codesearch stopped
working.

Both grep-guard and subagent-preamble (ps1 + sh) now instruct: on
scope_required / Unknown alias, read the error (it lists the valid
available_projects / available_groups) and pass project=/group=, noting
the alias may differ from the folder name.

* [docs] AGENTS.md: fix stale version + doc links (v1.0.235 -> v1.1.0, docs/ -> integrations/cloud/)

Version and doc-path references had drifted after the public-repo-prep
commits (2aa49ce, 2061dfd) removed/moved docs/federation-*.md without
updating AGENTS.md. Docs-only change, no code touched -- skipping the
pre-commit hook's cargo build/version-bump (not applicable here, and it
timed out on the previous attempt without completing).

* [docs] escalate docs-repo warmup bug to HIGH; propose single-app scale redesign

Confirmed the known open/write-stuck status bug is the same mechanism behind
a real cloud crash-loop: the docs corpus doubled (2509->5666 files) and
serve's in-process incremental-warmup OOM'd repeatedly on 1vCPU/2GiB,
re-syncing from blob on every restart. Worked around by re-running the
codesearch-indexer job to produce a fresh full snapshot; serve now reports
both repos "warm" with no restarts.

Also documents a proposed redesign (not yet implemented): collapse the
separate indexer job + serve app into one Container App that scales
up/poll-until-warm/snapshots/scales back down, replacing the fragile
in-process indexing-flag detection with a reliable external /status poll.
Left open pending a follow-up session: whether to retire codesearch-indexer
entirely.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* [fix] bound incremental-refresh embedding batches to prevent OOM crash-loop

perform_incremental_refresh_with_stores chunked+embedded the entire
changed-file delta in one unbounded in-memory batch before writing
anything out. Harmless for normal deltas (tens of files) but this is
exactly what OOM'd codesearch-serve (1vCPU/2GiB) when the vendor docs
corpus roughly doubled (2509->5666 files) in one sync, crash-looping on
every cold start.

Fix: process changed_files.chunks(batch_size) sequentially (chunk+embed+
insert+commit per batch, single build_index() at the end), bounding peak
memory to O(batch) instead of O(total delta). Batch size defaults to
INCREMENTAL_REFRESH_BATCH_SIZE=200, override via
CODESEARCH_INCREMENTAL_BATCH_SIZE. Protects both codesearch-serve's
in-process warmup and codesearch-indexer's full rebuild against the same
failure mode as the corpus keeps growing, independent of which container
runs it.

cargo check + cargo clippy -D warnings + cargo test --lib --bins (1080
passed) all clean. No new test for the multi-batch path itself: existing
manager.rs tests deliberately avoid real embedding invocation (slow/
ONNX-dependent), consistent with the gated csharp_helper_integration
pattern elsewhere in this repo.

Also documents the still-open "automate the manual scaling trigger"
decision in AGENTS.md (codesearch-indexer job confirmed triggerType=
Manual) -- left open pending vendor content update-cadence info.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* [fmt] cargo fmt + version bump for previous fix commit (pre-commit hook catch-up)

The previous commit (bounding incremental-refresh batches) was made with
--no-verify by mistake, skipping this feature branch's normal pre-commit
hook (cargo fmt + patch version bump + rebuild). Running the equivalent
steps now: cargo fmt --all reformatted manager.rs, version bumped
1.1.1 -> 1.1.2, cargo build --bin codesearch verified clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* [docs] plan: remote project mounting (1-to-1 passthrough federation)

Records the locked design for moving federation from group-level to
project-level mounting: peers expose individual indexes, mounted locally
as project=<peer>/<alias>, italic in the TUI, server-side docs bundle
dropped in favor of user-owned local grouping.

Decisions: auto-discover + local filter; peer-namespaced names.
5-stage execution plan + verified current-code gaps.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* [feat] stage 1/5: config model for mounted remote projects

Foundation for project-level federation ("1-to-1 passthrough"): peers
expose individual indexes that mount locally as project=<peer>/<alias>.

- Target::RemoteProject { peer_name, peer, remote_alias } — a single
  remote project (vs whole-peer Target::Remote used by group federation).
- REMOTE_PROJECT_SEPARATOR ("/") + remote_project_name() helper.
- ReposConfig fields (local, user-owned filter): remote_hidden,
  remote_alias_overrides, remote_project_cache (offline fallback).
- mounted_remote_projects(discovered) + resolve_remote_project(name).

Pure, unit-tested config layer (4 new tests, 49 pass). Discovery + MCP
dispatch land in Stage 2 — temporary #[allow(dead_code)] removed then.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* [fix] stage 1/5: address review remarks (enforce peer-name namespacing invariant)

Review of c570fb1 (PASS WITH REMARKS):

- IMPORTANT: the REMOTE_PROJECT_SEPARATOR doc claimed peer names can never
  contain '/', but add_remote only trimmed + rejected '@'. A peer named
  "a/b" would break resolve_remote_project's split_once('/'). Fix: add_remote
  now rejects '/' in peer names, so the <peer>/<alias> invariant is actually
  enforced (not just asserted in a comment). Comment made precise. New test
  arm covers rejection.
- MINOR (precedence): documented that resolve_remote_project does NOT consult
  local repos, so callers (Stage 2 dispatch) MUST resolve local aliases first
  — local repos always win a name clash with a rename override.
- MINOR (override-target uniqueness non-determinism; local_name collision
  detection in mounted_remote_projects): deferred to Stage 2 as explicit
  dispatch/discovery design decisions, per reviewer.

cargo fmt + clippy -D warnings clean; 49 repos tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ✨ feat: route project=<peer>/<alias> to mounted remote projects (stage 2/6)

Project-level federation — a 1-to-1 passthrough that makes a remote peer's
project queryable locally as if it were a local index.

- FederationClient: extract shared post_search(); add search_project() that
  forces project=<remote_alias> and strips group (vs group-scoped search()).
- MCP search(): before local dispatch, resolve project as a mounted remote
  project (<peer>/<alias>) and route to that single peer. Local repos always
  win a name clash (resolve() checked first).
- federated_project_search(): single-peer passthrough, no local merge; an
  unreachable peer degrades to a warning with zero results. Namespaced
  chunk_refs route back through the existing federated_get_chunk().
- Remove now-live #[allow(dead_code)] on Target::RemoteProject and
  resolve_remote_project(); add search_project mock-peer unit test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ✨ feat: surface mounted remote projects in the TUI, italic (stage 4/5)

The local `codesearch serve` dashboard now shows peer-hosted indexes as
first-class rows, rendered italic (cyan) to signal they live on a peer —
matching `project=<peer>/<alias>` routing from stage 2.

- RepoRow gains `is_remote`; render_table + render_detail italicize the alias
  for remote rows (red-bold preserved for a remote in error state).
- tui.rs: background discovery task on a slow cadence (30s, constant) queries
  every peer's /status concurrently off the render tick, maps results through
  ReposConfig::mounted_remote_projects (honoring hide/rename), and feeds rows
  via a capacity-1 channel. Peers unreachable this round reuse an in-memory
  last-known alias list so a blip never drops a mount.
- Remote rows are display + query-routing only: existing idx<repos.len()
  guards make doctor/reindex/remove/info no-ops for them automatically.
- New constant REMOTE_DISCOVERY_INTERVAL_SECS (no magic number).
- Remove now-live #[allow(dead_code)] on mounted_remote_projects and
  remote_project_name.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ♻️ refactor: polish stage-4 review minors (remote discovery + detail)

Adopt the three review nits from the stage-4 pass (all non-blocking):
- Prune `last_good` to peers still in config each round so it can't grow
  unbounded in a long-lived serve.
- Log a tracing::warn when the discovery HTTP client fails to build instead
  of returning silently (observability).
- render_detail: a mounted remote project in error state now shows its alias
  red (was cyan), matching the table's error highlight. Local rows unchanged.
- Drop a stale "removed in Stage 2" comment above resolve_remote_project.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ✨ feat: split cloud indexer job into one repo per vendor (stage 5/5)

The index-job now builds/refreshes one index per immediate ${DOCS_DIR}/<vendor>
subfolder (akeneo, bynder, …) instead of a single monolithic "docs" repo.
Smaller per-vendor indexes rebuild faster, use less peak memory, warm quicker
on the restore-only serve side, and rank fairly (a small vendor is no longer
drowned by a large one). Each vendor is queryable as its own project and
mountable remotely as <peer>/<vendor> (stages 1-4).

- run_index_job: loop rebuild_repo over ${DOCS_DIR}/*/ (guarded — die if no
  vendor subfolders); verify EVERY vendor index is populated before upload so
  one empty build can't clobber the good snapshot.
- CRITICAL coupled fix: azcopy --exclude-path now built dynamically
  (docs_index_exclusions) to cover each <vendor>/.codesearch.db. --exclude-path
  is a relative-path-prefix match, so the old bare ".codesearch.db" only shielded
  a root-level (monolithic) index; per-vendor indexes live one level down and
  would otherwise be DELETED by --delete-destination on every sync (job AND serve
  cold-start restore). Legacy root entry kept for back-compat.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 📝 docs: mark remote-mounting plan complete + DB_DIR_NAME safety note

- AGENTS.md: all 5 stages marked ✅ with per-stage outcome; record deferred
  post-merge items (remote_project_cache persistence, shared search-body
  builder, per-vendor deploy step).
- entrypoint.sh: comment flagging the .codesearch.db ↔ src/constants.rs
  DB_DIR_NAME coupling (data-safety, no logic change).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 📝 docs: drop stale staging comment + clarify passthrough score doc

Comment-only cleanup from the final integration review (no logic change):
- Remove the obsolete "Fields read starting in Stage 2" note on
  Target::RemoteProject (all fields are now consumed).
- federated_project_search doc: replace "verbatim" with an accurate note that
  results pass through single-list RRF (scores are rank scores), matching the
  group path's rendering.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 🔧 fix: silence warmer index-add output in Docker build

`codesearch index add` prints a U+2795 (➕) emoji that crashed `az acr build`'s
log streamer on a Windows cp1252 console (colorama UnicodeEncodeError), killing
the build driver so ACR marked the run Failed. Redirect the warmer step's
output to /dev/null — the build log must not depend on the app's decorative
output. Model download (the step's actual purpose) is unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 🔧 fix: fold model warmup into builder stage (ACR COPY --from chained-stage bug)

ACR Tasks' classic builder fails at export with "failed to get layer <sha>:
layer does not exist" on `COPY --from=warmer` (a chained `FROM builder AS
warmer` stage). cb6/cb7/cb8 all failed at that exact step; the emoji-streamer
crash had masked it. This Dockerfile never built successfully — deployed v2.5
is an older image from a different Dockerfile.

Fix: warm the fastembed model inside the builder stage (a base-image stage)
and COPY --from=builder, which is proven reliable (binary/lib copies succeed).
Also replace the failure-masking `|| true` with a hard verification that the
model cache actually populated, so a failed download fails the build loudly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 🔧 fix: ship warmed model cache as a tarball (ACR symlink-tree COPY export bug)

Root cause found: cb9 still failed at `COPY --from=builder .../models` with
"layer does not exist", while single-file (codesearch) and small-dir (/out/lib)
copies from the SAME stage succeed. The fastembed/HuggingFace model cache is a
symlink tree (snapshots/ -> blobs/); ACR's classic builder cannot export a
cross-stage COPY of a symlinked directory tree.

Fix: tar the model cache to a single /models.tar.gz in the builder (symlinks
preserved inside the archive), COPY the one file into runtime (structurally like
the proven binary copy), and untar it there. The existing `chown -R app:app
/home/app` fixes ownership of the extracted cache.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(entrypoint): build vendor indexes sequentially to avoid OOM-kill

The index-job submitted all per-vendor build requests at once (rebuild_repo
returns on HTTP 202) and waited once afterward, so serve held every vendor's
embedding model + working set simultaneously and was OOM-killed (SIGKILL) on
the 8 GiB job limit, leaving wait_until_indexed polling a dead process forever.

Build one vendor at a time: submit -> wait_active_build_done -> verify -> next.
Peak memory is now a single index build regardless of vendor count.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ✨ feat: TUI info for remote mounts + disable inapplicable actions

The `i` (info) key now works on mounted remote projects (federation
peers): a new OverlayState::RemoteInfo shows the peer URL and the
peer-reported live status (status/lock/changes/calls/last-call) instead
of local on-disk index stats, which a mount does not have.

When a remote mount is selected, the footer now renders doctor / reindex
/ remove struck-through (CROSSED_OUT) so it is clear those local-index
actions do not apply to a peer-hosted mount. info / reload / quit / nav
stay enabled. The standalone remote TUI is unaffected (its rows are the
peer's own local repos, is_remote=false).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 📝 docs: document project-level mounting + cloud reindex hardening

CHANGELOG: new [Unreleased] section covering mounted remote projects
(project=<peer>/<alias>), the TUI remote-mount info panel + disabled
local-index actions, the per-vendor cloud indexer split, the sequential
build OOM fix, the local BuildKit build workflow, and the grep-guard hook.

README: new "Mounting a peer's projects" subsection under Federation
(project=<peer>/<alias>, italic TUI mounts, `i` info, disabled actions).

AGENTS: Stage 4/5 notes updated (TUI info/disabled + sequential build),
Current state bumped to v1.1.9 with deploy outcome; deferred list refreshed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ✨ feat: flash feedback when a disabled action is pressed on a remote mount

Applies the reviewer's non-blocking UX remark: pressing doctor / reindex
/ remove while a mounted remote project is selected was a silent no-op
(the struck-through footer hint was the only cue). Now it also flashes a
short "don't apply to a remote mount" confirmation, reinforcing which
actions are available on a peer-hosted mount.

Message centralised in one REMOTE_ACTION_NA const (no literal duplication).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* @
🔒️ fix: scrub customer identifier (aprimo→vendor-a) for public push

The pre-push customer-ref gate (blocks [Aa]primo|husqvarna|bayer|… on
pushes to develop/master) flagged 5 residual "aprimo" references after
merging federation into develop: vendor-list examples in AGENTS.md /
CHANGELOG.md, a doc-comment in repos.rs, and test data in
federation/mod.rs. Replaced all with the generic placeholder "vendor-a"
(other vendor names akeneo/bynder/… are not customer identifiers and stay).
The federation namespacing test still passes (arg + assert use the same
token). Full-tree scan now clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@

* ✨ feat: opt-in mounting of individual remote projects (remote_mounts allowlist)

Remote peers no longer auto-expose every project. The local user now
explicitly picks which individual per-vendor indexes to use, via a new
opt-in `remote_mounts` allowlist in repos.json — the single source of
truth for routing, discoverability, TUI display, and group fan-out.

- config: replace opt-out `remote_hidden` with opt-in `remote_mounts`;
  mounted_remote_projects() is allowlist-driven (no discovery arg);
  resolve_remote_project() gates on the allowlist; new group_remote_projects(),
  mount_remote_project()/unmount_remote_project(); reconcile() prunes
  stale/unknown-peer/malformed mounts + orphaned rename overrides.
- routing: `@peer` group fan-out now queries only the mounted <peer>/<alias>
  projects (per-project search_project), never the whole peer; federated_search
  reworked; obsolete whole-peer FederationClient::search removed.
- discoverability: list_projects gains a `remote_projects` array; scope_required
  advertises mounted names as first-class `project=` targets.
- cli: `remote available|mount|unmount|mounts` to inspect a peer and pick.
- tui: rows come from the allowlist; discovery only enriches live status.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 📝 docs: opt-in remote mount selection (remote_mounts allowlist)

Update CHANGELOG (Unreleased), README (Federation → mounting), and
AGENTS.md for the shift from auto-discover/opt-out to the explicit
`remote_mounts` allowlist: new `remote available|mount|unmount|mounts`
CLI, group fan-out restricted to mounted indexes, non-mounted =
unroutable, and mounts surfaced in list_projects/scope_required.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 🐛 fix: prune orphaned remote rename-overrides unconditionally in reconcile

Address reviewer minor: reconcile() dropped orphaned remote_alias_overrides
only when a mount was pruned that round, so a hand-edited removal from
remote_mounts left a stale override that could resurface as a surprise
rename on re-mount. Now retain overrides against the current mounted set
unconditionally.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ✨ feat: show peer index stats in remote-mount info overlay

The TUI `i` overlay on a mounted remote project previously showed only
peer URL + status. It now fetches the peer's on-disk index stats
(chunks / files / db size / model) on demand from GET /repos/{alias}/info
and renders them with a loading / ready / unavailable tri-state, giving
remote mounts parity with the local Info overlay.

- federation: add RemoteRepoInfo + FederationClient::repo_info()
- constants: add REPO_INFO_PATH_SUFFIX ("/info")
- tui_common: OverlayState::RemoteInfo gains RemoteStatsState; render
  chunk/file/db-size/model lines (or placeholder) after status
- tui: build_remote_info_overlay starts Loading; ShowInfo resolves
  peer+remote_alias and spawns an async fetch via the doctor channel;
  recv guard broadened to apply RemoteInfo results

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 🐛 fix: harden remote-mount info fetch against stale/None resolve

Review remarks on 1cea46b:

- Bump doctor_gen UNCONDITIONALLY before resolving the mount, so a
  still-in-flight doctor/remote-info reply (shared channel + counter)
  can never clobber the freshly-opened RemoteInfo overlay via the recv
  guard.
- When resolve_remote_project returns None (misconfig or a config
  reload racing the keypress), render stats as Unavailable instead of
  leaving the overlay stuck on "fetching…" forever.
- Build the base overlay once and clone it (derive Clone on
  OverlayState) rather than building it twice.
- Soften the Unavailable label to "stats unavailable from peer" since
  an HttpError from a reachable peer also lands here (not only
  unreachability).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 📝 docs: note peer index stats in remote-mount info overlay (CHANGELOG)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 🐛 fix: scope federated get_chunk to remote project (fixes ambiguous_chunk_id)

Remote search returned chunk_refs shaped "<peer>:<id>", dropping the remote
project alias. Since the peer is itself multi-repo and chunk_ids are only
unique within one index, every federated get_chunk failed with
ambiguous_chunk_id when the peer hosted more than one project (inriver,
aprimo, bynder, ...).

Client-side fix (the serve /chunk route already honoured ?project=):
- convert_remote_item now namespaces the ref as "<peer>/<alias>:<id>" and
  tags source as "<peer>/<alias>".
- parse_federated_chunk_ref (new, unit-tested) splits peer/alias/id; accepts
  the legacy "<peer>:<id>" shape for backward compatibility.
- FederationClient::get_chunk forwards project=<alias> (and omits group) when
  an alias is present, mirroring search_project; legacy refs still fall back
  to group scope.
- Docs on GetChunkRequest.chunk_ref + inline comments updated.

Tests: parse helper (4 cases), namespaced convert, and a live-peer get_chunk
asserting project=<alias> is forwarded and group omitted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ✅ test: cover legacy no-alias get_chunk group fallback (review minor)

Adds a live-peer test asserting that a non-namespaced chunk_ref
(remote_alias=None) forwards a `group` scope and omits `project`, closing
the coverage gap flagged in the Stage A review.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ✨ feat: split hooks command into `hooks git` and `hooks claude` (+ Claude installer in Rust)

The single `hooks install` (git post-checkout hook) is replaced by two
explicit subcommand groups (hard break, no back-compat alias for the old
`install`):

- `codesearch hooks git install [--path]` — the prior post-checkout worktree
  auto-register hook.
- `codesearch hooks claude install [--project]` — NEW: installs the Claude
  Code PreToolUse guard hooks (Grep -> grep-guard, Agent -> subagent-preamble)
  into ~/.claude (or ./.claude with --project). Rust port of
  integrations/claude-code/install.{sh,ps1}: scripts are embedded via
  include_str! (self-contained binary), settings.json is backed up and merged
  idempotently (keyed by exact command string), and the host shell is detected
  (pwsh on Windows, bash elsewhere).

The top-level command is now `hooks` (alias `hook` kept for muscle memory).
New module src/cli/claude_hooks.rs with unit tests for the settings merge
(empty/idempotent/preserve-unrelated/bad-shape) and host-shell command build.
README updated. Stage B will add a WebSearch/WebFetch guard to GUARD_HOOKS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ✨ feat: add web-guard hook — steer WebSearch/WebFetch to remote doc mounts

New PreToolUse guard (bash + pwsh twins) matching WebSearch|WebFetch: when
repos.json has remote projects mounted (.remote_mounts, e.g. cloud/inriver,
cloud/aprimo), it denies the first web call with guidance to search those
indexed mounts first (compact=false to read inline, then get_chunk). Same
5-minute retry-escape as grep-guard; when no mounts are configured it does
nothing. Detection reads repos.json directly (CODESEARCH_REPOS_CONFIG or
~/.codesearch/repos.json) — no binary spawn, no serve round-trip.

- integrations/claude-code/hooks/web-guard.{sh,ps1} (new)
- claude_hooks.rs: web-guard added to GUARD_HOOKS (embedded via include_str!)
- install.{sh,ps1}: register the WebSearch|WebFetch matcher for parity
- README: three-guard section, `hooks claude install` as the primary path

Also addresses the Stage C review minors: drop the redundant create_dir_all,
add tests for non-object hooks/root shapes + GUARD_HOOKS coverage, and
cross-reference the two documented install paths.

This closes the gap that let me reach for WebSearch instead of the mounted
inriver/aprimo docs — the guard now makes the preference structural.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 📝 docs: make web-guard guidance use get_chunk(chunk_ref=…) explicitly (review minor)

Clarifies the deny message in both web-guard twins: after searching a mount,
read full context via get_chunk with the returned federated `chunk_ref`
("<peer/alias:id>"), not chunk_id — the correct param for remote results.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 📝 docs: align SearchResultItem chunk_ref/source docs with namespaced format (final review remark)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 📝 docs: add remote/federation + index --remote rows to CLI Reference table

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ✅ test: replace fixed sleep with bounded readiness poll in live-peer federation tests

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 📝 test: add remote-mount semantic-findability test scenario (Run 1: PASS)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ✨ feat: serve incrementally reindexes custom-kb on each KB pull

serve mode previously git-pulled the custom-kb repo every
KB_PULL_INTERVAL_SECS but nothing triggered indexing afterward — the
"periodic incremental reindex (REINDEX_INTERVAL_SECS)" the comments
promised does not exist in code. Pulled KB articles therefore only
became searchable on the next cold-start warmup.

The KB pull loop now detects when a pull moves HEAD and fires
POST /repos/custom-kb/reindex (incremental) against the local serve, so
new/changed articles are searchable without a restart. Incremental
refresh re-embeds only the delta and the KB corpus is small, so it fits
the 1-2 GiB serve replica; the heavy DOCS corpus stays index-job-only.

- repos open read-write by default (try_open_stores), so custom-kb on the
  serve's local disk reindexes in-place — no Rust change needed
- fire-and-forget 202; 409 (concurrent/FSW pickup) is expected + harmless
- first pull fires after the interval, after Phase-1 warmup releases the
  KB write lock, so no warmup contention
- fixed the stale REINDEX_INTERVAL_SECS comments (header, env doc, run_serve)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 📝 docs: scope cloud "read-only serve" claims to the custom-kb reindex exception

Follow-up to the serve custom-kb incremental-reindex change. The cloud
docs still described serve as strictly restore-only / never-reindexes,
which is no longer accurate: serve now runs a memory-bounded incremental
reindex of the small custom-kb repo after each KB pull.

- integrations/cloud/README.md: scope the "read-only / never writes"
  statements to the DOCS corpus; document the custom-kb incremental
  reindex as the sole in-process write (fire-and-forget 202, incremental
  only, HEAD-change gated, 409/404 benign). Correct the management-verbs
  note — incremental reindex of a registered repo succeeds; only add /
  reindex --force still require a read-write peer.
- AGENTS.md: note the custom-kb incremental step as the scoped first
  realization of the "incremental in-process on serve" redesign; DOCS
  corpus stays job-only (the OOM that motivated the split). Nuance the
  remote-write-verbs note accordingly.
- entrypoint.sh: distinguish HTTP 404 (custom-kb not yet in the restored
  snapshot — expected during bootstrap) from a genuine failure WARN
  (addresses review remark).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 📝 test: add section F — cross-vendor overlap + isolation scenarios (Run 1)

Complements section B (isolation) with the inverse: a concept shared
across vendors must surface hits from multiple vendors at once via
group="docs" RRF fusion, while domain-specific concepts stay absent from
the opposite domain. 5 scenarios (F1–F5) covering PIM/DAM overlap and
isolation, all executed and passing in Run 1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix: COPY integrations/claude-code/hooks into Docker builder

The cloud image build broke on the release compile:

  error: couldn't read `.../integrations/claude-code/hooks/grep-guard.sh`:
  No such file or directory (os error 2)

src/cli/claude_hooks.rs embeds the six hook scripts at compile time via
include_str!("../../integrations/claude-code/hooks/*"), but the Dockerfile
only copied Cargo.*, build.rs and src/ into the builder, so the hooks
subtree was absent from the build context. This is latent since the
hooks-split feature landed — v2.7 predates it, so this is the first image
build to hit it. Local builds compile because the tree is on disk.

Copy only that subtree (the exact paths include_str! needs) before the
cargo build. Verified no other include_str!/include_bytes! in src/
references paths outside src/.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix: pin shell scripts to LF via .gitattributes (CRLF broke cloud image)

The v2.8 cloud image failed to start: `env: 'bash\r': No such file or
directory`. Root cause: core.autocrlf=true rewrites docker/entrypoint.sh
to CRLF in the Windows working copy, and the Docker build copies the
working copy (not git's LF blob) into the image — so the CRLF shebang was
baked in and the container could not exec bash.

Pin *.sh (and docker/entrypoint.sh explicitly) to eol=lf so the working
copy is always LF regardless of the local autocrlf setting, and the image
can never regress to a CRLF shebang. entrypoint.sh already normalized to
LF in the working copy; the git blob was already LF.

Deployed image tag v2.9 carries the LF fix and is verified live (serve
boots, /healthz 200, "KB auto-pull loop started (git pull + reindex-on-
change...)" present, no bash\r error).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🔧 chore: pre-commit hook does cargo fmt only (drop per-commit version bump + rebuild)

The hook auto-bumped the Cargo.toml patch version and ran `cargo build`
on every feature-branch commit. That blocked each commit for minutes on a
debug build nobody deploys, and made the deployed binary constantly drift
from HEAD (forcing a manual release rebuild to re-sync).

The auto-bump was redundant: build.rs already appends a unique
"+<commit_count>" suffix (git rev-list --count HEAD) to every build, so
each commit is uniquely identifiable without churning the base version.

Now the hook only runs cargo fmt (+ stages reformatting). The base version
is bumped deliberately at release time. Updated RELEASING.md accordingly.
Installed the new hook into .git/hooks/pre-commit (this commit already ran
it — fast, no bump).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🔧 chore: pin extensionless hook scripts to LF in .gitattributes

scripts/pre-commit and .githooks/* are shell scripts without a .sh
extension, so the *.sh rule didn't cover them. On a Windows checkout
(core.autocrlf=true) they become CRLF, and copying scripts/pre-commit
into .git/hooks then yields a `#!/bin/bash\r` shebang that breaks the
hook. Pin them to eol=lf, same as the other shell scripts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ✨ feat(serve): KB near-instant propagation via cheap remote-HEAD poll

The serve-mode KB refresh loop previously did a full `git pull --ff-only`
only every KB_PULL_INTERVAL_SECS (default 900s), so a pushed KB edit took
up to ~15 min to become searchable in the cloud.

Now the loop cheaply polls the remote HEAD every KB_POLL_INTERVAL_SECS
(new, default 30s) via `git ls-remote origin <branch>` — ref advertisement
only, no object transfer — and performs the real pull + incremental reindex
only when the remote SHA actually moved. A pushed edit propagates in
~seconds instead of minutes.

KB_PULL_INTERVAL_SECS (default 900) is retained as a safety-net: it forces
a full pull at least that often even when the cheap poll saw no change or
ls-remote failed, self-healing a missed poll. ls-remote uses the stored
`origin` remote so the PAT never lands on argv. No codesearch core (Rust)
changes — trigger lives entirely in deployment glue where git already runs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: redact platform name -> example-dam to satisfy customer-ref pre-push guard

The local pre-push guard scans tracked files for customer/vendor identifiers
and flagged the bare platform name "aprimo" (as mount `cloud/aprimo` and in
prose) across README, the remote-mount test scenario, and both web-guard hooks.
Replaced every occurrence with the neutral placeholder `example-dam`; no
functional/code change. Line endings preserved (LF for scripts).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 📝 docs: add missing KB-propagation changelog entry + filter_path federation caveat

Audit found two doc gaps: the KB near-instant propagation feature
(commit bd90ec2) had no CHANGELOG entry, and filter_path's documented
zero-result behavior on federated/mounted projects (observed live via
the aprimo_mcp consumer) wasn't captured anywhere in README or
CHANGELOG. Documents the known limitation + client-side over-fetch
workaround until the root cause is isolated with a live hub+peer repro.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix: apply federated filter_path client-side on namespaced result paths

Federated search forwarded filter_path to the peer, which matched it
against its own un-namespaced store paths (and, in serve mode, the wrong
project root via build_semantic_response using self.project_path instead
of the routed alias root). The caller only ever sees the
`<peer>/<alias>/…` namespaced path, so a server-side match dropped every
hit regardless of value — the "0 results" symptom observed live via the
aprimo_mcp consumer.

Fix: stop forwarding filter_path to the peer; over-fetch and post-filter
client-side on the namespaced paths in both federated_project_search
(project passthrough) and federated_search (group fan-out), via a shared
retain_by_filter_path helper + is_meaningful_filter guard. Consumers no
longer need the over-fetch+post-filter workaround.

The underlying server-side root mismatch still affects filter_path on a
LOCAL project routed through serve (non-federated); documented as a
follow-up in CHANGELOG known-limitations. stdio single-repo is unaffected.

Tests: retain_by_filter_path unit tests (matching prefix, none/blank
no-ops, no-match empties). Full lib suite 566 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix: relativise filter_path against the routed project root in serve mode

For search(project=<local-alias>) or a local group served by codesearch
serve, build_semantic_response relativised result paths against the
service's own project_path instead of the ROUTED project's root, so the
absolute stored path never stripped and filter_path dropped every hit
(0 results for any value). Only stdio single-repo — where project_path
IS the repo root — worked.

Fix: pick_filter_root() resolves the correct root per result — the routed
alias's root for single-project routing, the longest matching alias root
for multi/group, and the service project_path only as the stdio fallback.
filter_path is now a repo-relative prefix in every routing mode. This is
the non-federated companion to the client-side federated fix (1241963);
together they close the filter_path scoping gap end to end.

Tests: pick_filter_root unit tests (routed alias, longest-match multi,
stdio fallback). Full lib suite 569 passed. stdio behaviour unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🔒️ fix: scrub customer identifier (aprimo→vendor-a) in mcp tests

Filter_path test fixtures used the real customer alias; replace with the
established generic placeholder so the pre-push customer-ref gate passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Test User <test@example.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

v1.0.213

Toggle v1.0.213's commit message
v1.0.213 — API key on serve delegation, reserved 'all' group, MCP age…

…nt discoverability

Shipped in this release (since v1.0.209):
- Fixed (flupkede#132): CLI delegate functions now send CODESEARCH_SERVE_API_KEY when talking to a running serve instance. Required when serve binds to non-localhost (0.0.0.0).
- Added (flupkede#131): Reserved virtual 'all' group that resolves dynamically to every registered repo. Not persisted, not the default, name reserved against manual add/remove.
- Changed (flupkede#130): MCP agent discoverability — added when-to-use (codesearch vs grep/glob) and service-mode-notes guidance blocks; reframed find_impact from 'C# only' to 'C# today, find usages fallback'; fixed stale self-referential tests.
- Docs: CHANGELOG updated, README Security/Groups/Agent-Guidance sections updated.

v1.0.209

Toggle v1.0.209's commit message
v1.0.209 — self-healing active_reindexes fix

Fixes repos stuck showing 'Indexing' in the TUI forever due to leaked
active_reindexes entries from fire-and-forget indexing tasks.

Self-healing via timestamp-based eviction (MAX_INDEXING_SECS, 30 min default,
overridable via CODESEARCH_MAX_INDEXING_SECS).

v1.0.208

Toggle v1.0.208's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Release v1.0.208 — doctor LMDB double-open fix (flupkede#128)

* 🐛 fix: doctor embedding cache check avoids LMDB double-open in serve

Add process-global live cache stats registry (OnceLock<DashMap>) that
mirrors embedding cache entry counts without opening a second LMDB env.
When doctor runs in-process inside serve, it now reads stats from the
live registry instead of calling PersistentEmbeddingCache::open, which
would trip TrackedEnv's double-open guard.

- src/embed/cache.rs: cache_dir_for(), live_stats(), refresh_live_stats()
- src/lmdb_registry.rs: is_open() predicate
- src/cli/doctor.rs: fast-path via live_stats + file-based fallback

* 📝 docs: add Branching & PR workflow section (PRs target develop, not master)

* 📝 docs: add Branching & PR workflow section to AGENTS.develop.md template

* 🚀 release: bump version to 1.0.208 — doctor LMDB double-open fix + gitflow docs

* 🔧 test: ignore flaky test_prepare_text (requires ONNX model, fails on CI)

---------

Co-authored-by: Test User <test@example.com>

v1.0.207

Toggle v1.0.207's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Merge pull request flupkede#123 from flupkede/release/v1.0.207

🚀 release: v1.0.207 (develop → master)

v1.0.162

Toggle v1.0.162's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Release v1.0.162 — Windows flaky test fix (rename_retry) (flupkede#106)

* fix: CI test resilience + protect-master workflow (#58)

* Sync master → develop (tree-sitter) (#60)

* Fix formatting of codesearch index command

* Create codeql.yml

* feat: add tree-sitter grammars for Bash, Ruby, PHP, YAML, JSON

* fix: CI test resilience + protect-master workflow (#58) (#59)

---------

Co-authored-by: flupkede <flupkede@users.noreply.github.com>

* docs: update CHANGELOG — v1.0.132 consolidated release notes (#61)

* [worker] cleanup: AGENTS.md — 73% reduction, removed stale test report and duplicate bug details

* docs: update CHANGELOG — v1.0.132 consolidated release notes (v1.0.97...v1.0.132)

---------

Co-authored-by: flupkede <flupkede@users.noreply.github.com>

* sync: align develop with master — AGENTS.md, Cargo.toml, Cargo.lock (#63)

Co-authored-by: flupkede <flupkede@users.noreply.github.com>

* fix: MCP local mode project/group fallback + QC script fix (#66)

* fix(mcp): ignore project/group params in local/stdio mode instead of erroring

When running MCP in local mode (no serve_state), project/group routing is meaningless because only one DB is available.

Log a warning and fall back to local DB instead of returning an error.

* fix(qc): define YELLOW color in bash QC script

* chore: simplify release workflow — feature-only version bump (#74)

* fix: serve-aware indexing — create DB dir before lock + no silent local duplicate (v1.0.137) (#76)

* fix: create DB directory before acquiring writer lock (serve auto-register)

When `serve` is running and `codesearch index` is run for a repo not yet known
to it, auto-register (POST /repos) failed with a misleading "Database is locked
by another process" 500: SharedStores::new() acquired the writer lock before
the .codesearch.db directory existed, so opening .writer.lock failed with
"path not found". This rolled back the repos.json registration and made the CLI
fall back to a local duplicate index instead of delegating to serve.

- acquire_writer_lock / SharedStores::new now create the DB directory first;
  genuine I/O errors surface distinctly instead of as a lock conflict.
- Serve config writes route through ServeState::persist_config() (honors the
  config path override) — production behavior unchanged, register/remove path
  now hermetically testable.
- Regression guards exercise the brand-new-repo create/register path with the
  DB directory genuinely absent (verified to fail against the pre-fix code).
- CHANGELOG: 1.0.136.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: never silently create a local duplicate index when serve is busy

The CLI probes serve's /health before delegating `index`/`index add`. Any
health failure — including a *timeout* while serve is warming up its repos at
startup — was classified as "serve not running", so the CLI silently created a
local index. That local index is a duplicate serve does not manage and can
cause LMDB file-lock conflicts (and the repo never gets registered with serve).

New behavior via probe_serve_health():
- Responsive  -> delegate as before.
- Connection refused / cannot connect -> serve not running; index locally.
  Detected immediately (no timeout elapses, no retries), so the common
  "no serve -> local" path is NOT slowed down.
- Listening but unresponsive (timeout, retried briefly) -> serve is up but
  busy. The CLI now REFUSES to create a local duplicate and tells the user to
  retry shortly or stop serve first. The fallback is never silent anymore.

Delegation errors are now typed (DelegateError: ServeDown / ServeUnresponsive /
Failed) instead of string-matched. Applies to `index` and `index add` (the
index-creating paths); `index rm` is unchanged.

Tests: probe classification guards (responsive -> Up; listening-but-slow ->
Unresponsive). Rolls into the 1.0.137 release together with the writer-lock fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: flupkede <flupkede@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* sync: align develop with master (post-v1.0.137 release) (#78)

Brings the two files that drifted on master during the v1.0.137 release back
to develop: the updated protect-master.yml (allows release/* branches) and the
CHANGELOG [1.0.135] entry. After this, develop and master trees are identical.

Co-authored-by: flupkede <flupkede@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: strip UNC prefix in repos.json + auto-add on missing DB (v1.0.138) (#79)

Two Windows path-handling bugs that caused spurious "Database not found"
errors and local duplicate indexes:

1. register()/register_with_alias() stored the raw canonicalize() result in
   repos.json. On Windows, canonicalize() returns \?\C:\... (extended-length
   UNC prefix). Downstream .join(".codesearch.db") and Path::exists() calls
   then fail inconsistently (\?\C:\foo\.codesearch.db not found even when
   C:\foo\.codesearch.db exists). 7 repos were affected. Fix: strip_unc()
   removes the prefix before storage. Existing repos.json patched in-place.
   Regression test: register_strips_unc_prefix_from_stored_path.

2. 500 "Database not found" from reindex (alias registered but DB gone) was
   treated as a generic failure -> local fallback -> duplicate index. Fix:
   triggers the same auto-register POST /repos path as 404 (DB recreated by
   serve, no local fallback).

Co-authored-by: flupkede <flupkede@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* sync: align develop with master (post-v1.0.138) (#81)

Co-authored-by: flupkede <flupkede@users.noreply.github.com>

* refactor: central safe_canonicalize() — eliminate raw .canonicalize() across codebase (#82)

ROOT CAUSE OF RECURRING BUG CLASS
Path::canonicalize() on Windows returns \?\C:\... (extended-length UNC
prefix). Any downstream .join(), .exists(), or HashMap key built from that
path behaves inconsistently — the sub-path \?\C:\foo\.codesearch.db may
return false from exists() even when C:\foo\.codesearch.db is present.
This class of bug has silently broken registrations multiple times.

FIX
Introduce safe_canonicalize(path: &Path) -> io::Result<PathBuf> and
strip_unc_prefix(path: PathBuf) -> PathBuf in src/cache/file_meta.rs.
These are the ONLY approved way to canonicalize paths in this codebase.
Exported via crate::cache.

CALL SITES UPDATED (all raw .canonicalize() removed)
- src/cache/file_meta.rs      — central definition + 5 new regression tests
- src/db_discovery/repos.rs   — register, register_with_alias, unregister_path,
                                alias_for_path; local strip_unc() removed
- src/db_discovery/mod.rs     — find_best_database, get_db_path_for_cwd
- src/index/mod.rs            — find_git_root, get_global_db_path,
                                add_to_index, remove_from_index,
                                try_delegate_reindex_to_serve (x2),
                                try_delegate_rm_to_serve
- src/lmdb_registry.rs        — TrackedEnv registry key (eliminates
                                double-open risk when same dir accessed
                                with and without \?\ prefix)
- src/serve/mod.rs            — add_repo_handler, run_serve --register path

POLICY DOCUMENTED
AGENTS.md: "⚠️ Canonical Path Policy — MANDATORY" section with rule,
code example, and pointer to regression tests.

REGRESSION TESTS (6 new in cache/file_meta.rs + 1 existing in repos.rs)
- strip_unc_prefix_removes_windows_unc
- strip_unc_prefix_is_idempotent_on_{plain_path,unix_path}
- safe_canonicalize_on_existing_dir_returns_plain_path
- safe_canonicalize_on_nonexistent_path_returns_error
- register_strips_unc_prefix_from_stored_path (repos.rs — verifies
  fallback path also strips UNC when canonicalize() fails)

407 lib tests pass. clippy -D warnings clean.

Co-authored-by: flupkede <flupkede@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: replace last raw .canonicalize() with safe_canonicalize in get_db_path_smart (#84)

The old normalize_path(&p.canonicalize()...) pattern in get_db_path_smart
was missed in the central safe_canonicalize refactor (v1.0.139). It worked
correctly (normalize_path also strips UNC) but was inconsistent with the
policy. Now all .canonicalize() calls outside safe_canonicalize's own
definition are eliminated.

Co-authored-by: flupkede <flupkede@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: wait for serve warmup instead of refusing; fix 409 on DB-recreate (#86)

PROBLEM 1 — ServeUnresponsive aborted with error instead of waiting
When serve is warming up (opening LMDB for 15+ repos blocks the tokio
runtime, causing /health to time out), the CLI refused with an error.
The user had to retry manually.

FIX: serve_delegate_with_warmup_wait() wraps both try_delegate_reindex_to_serve
and try_delegate_add_to_serve. On ServeUnresponsive it prints
"⏳ serve is starting up, waiting..." and retries every 8s up to 6 times
(~2 min budget). On success it prints "✅ serve is ready, delegating...".
Only exhausting the full budget returns an error.

PROBLEM 2 — 409 Conflict from POST /repos on "Database not found" path
When a registered repo's DB was missing, the CLI tried POST /repos to
recreate it. Serve correctly returned 409 (alias already registered).
The CLI treated 409 as a failure and fell back to local indexing.

FIX: when auto-add returns 409, retry as POST /repos/{alias}/reindex?force=true.
Force reindex uses allow_create=true and creates the DB via serve without
local fallback.

AGENTS.md: document the root cause (tokio blocking during warmup) as a
remaining work item with diagnosis and fix guidance.

Co-authored-by: flupkede <flupkede@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: keep serve responsive during warmup — offload heavy work to spawn_blocking (#88)

PROBLEM
codesearch serve became unresponsive during startup warmup: FileWalker::walk,
VectorStore::build_index (HNSW), and fastembed/ONNX embedding (saturates all
cores) ran synchronously on tokio worker threads. This starved the async
runtime, /health timed out (>3s), and `codesearch index` reported "serve did
not respond in time". The server already returns 202 + spawns background
indexing (accept-and-defer); it just couldn't respond while warming.

FIX
Offload the heavy synchronous warmup work to tokio::task::spawn_blocking, so
the async executor stays responsive (answers /health and accepts POST /repos
immediately, runs the job in the background).
- serve/mod.rs warmup_repo: read stats under .read(); build_index via
  spawn_blocking + Arc clone + blocking_write. Build failure only warns.
- manager.rs perform_incremental_refresh_with_stores: walk, read+chunk+embed,
  and build_index all offloaded.
- manager.rs refresh_index_with_stores: walk + both build_index calls offloaded.

LOCK SAFETY (verified by review)
Every async RwLock guard scope CLOSES before the spawn_blocking that calls
.blocking_write() on the same store — no lock-over-await deadlock. blocking_write
is only ever called inside spawn_blocking (never on an async worker).

Test: test_incremental_refresh_up_to_date_is_noop exercises the refactored walk
path. 408 lib tests pass, clippy -D warnings clean.

Co-authored-by: flupkede <flupkede@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* sync: backfill CHANGELOG 1.0.139-1.0.142 from master (post-release) (#90)

Co-authored-by: flupkede <flupkede@users.noreply.github.com>

* feat: semantic Markdown chunking (tree-sitter-md) + /merge & /release commands (#91)

* chore: add /merge and /release Claude Code slash commands

Codify the project release workflow as two committed slash commands under
.claude/commands/ (force-added past .gitignore, like .claude/CLAUDE.md):

- /merge: README/CHANGELOG freshness checks -> commit -> validate -> push ->
  PR to develop -> auto-merge after CI. No tag.
- /release: /merge, then promote develop -> master via a "Release vX.Y.Z" PR
  (protect-master allows develop), then push the vX.Y.Z tag that triggers
  release.yml. Includes optional post-release develop sync.

Commands document the repo's real conventions: feature->develop->master flow,
master branch protection, and the pre-commit version-bump-on-feature-branches
rule that fixes the release version at the feature commit.

Tooling-only change on a chore/ branch: no version bump, no CHANGELOG entry
(CHANGELOG tracks the shipped binary's behavior).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: address review remarks on /merge and /release commands

- /merge: abort unless on feature/*|features/*|fix/* (the only branches the
  pre-commit hook version-bumps) — closes the gap where running from a
  non-bumping branch silently broke the version/CHANGELOG premise.
- Clarify CHANGELOG heading version math for multi-commit landings (hook bumps
  +1 per commit; verify heading matches Cargo.toml after the final commit).
- Capture PR numbers explicitly (gh pr view --json number) before merge/poll.
- /release: fetch --tags and guard against a double release (stop if the tag
  already exists locally or on origin).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: document /merge and /release workflow in AGENTS.md

Add a Release workflow section describing the two slash commands, the
branch-protection rule, the tag-triggers-release.yml pipeline, and the
feature-branch-only version-bump rule that fixes the release version.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(chunker): semantic Markdown chunking via tree-sitter-md

Markdown and .txt files were indexed as a single whole-file block (the
fallback chunker has no char budget), so a search hit returned an entire
page — real Aprimo docs reached 80 KB in one chunk.

Add the tree-sitter-md *block* grammar and chunk Markdown by heading
section instead: each chunk is one heading plus its own prose/code,
excluding nested subsections (which become their own chunks). The
heading path is carried in the breadcrumb context (File > Title >
Subsection) so embeddings capture each section's place in the document.

Also add split_oversized, a char- and line-aware splitter for the
unstructured paths (Markdown + the generic fallback): a single physical
line longer than the char budget is hard-split on UTF-8 boundaries, so
scraped one-line HTML/markdown can no longer produce an enormous chunk.
The structured code path keeps using split_if_needed unchanged, so code
chunking is unaffected.

- Cargo.toml: add tree-sitter-md 0.5.3
- grammar.rs/language.rs: register Markdown as tree-sitter-supported
- semantic.rs: chunk_markdown + emit_md_section + split_oversized
- tests: section split, nested breadcrumbs, oversized + long-line splits

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* [worker] final review: fix chunk_markdown doc comment

Reference the actual splitter used by the markdown path
(split_oversized, char-aware) instead of split_if_needed
(the code path's line-based splitter).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: document semantic Markdown chunking + correct language table

- CHANGELOG: add [1.0.145] entry for tree-sitter-md block-grammar Markdown
  chunking (sections/headings/code fences).
- README: expand the Supported Languages table to all 15 tree-sitter
  languages and bump the "9 languages" count to 15 — correcting pre-existing
  drift that omitted Shell, Ruby, PHP, YAML, JSON, and (new) Markdown.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(test): sanitize customer ref in markdown chunking fixtures

The pre-push customer-ref guard flagged "aprimo" in two semantic.rs test
fixtures (a frontmatter URL and a comment). Replaced with generic
example.com / "real-world scraped docs" — the test assertions never
reference either, so behavior is unchanged. Realign CHANGELOG heading to
the post-bump version (1.0.146).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: flupkede <flupkede@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Stale-path relocation, index prune, derived alias (v1.0.152) (#92)

* [worker] stage 1/5: capture git remote identity per repo

Add RepoMeta.git_remote (serde default, backward compatible) and a
best-effort git_remote_url() helper. Populate it in register() and
register_with_alias() so every registered repo records its
remote.origin.url for later relocation of moved/renamed folders.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* @
[worker] stage 2/5: relocate moved repos + reconcile pass + index prune

- Best-effort git relocation: try_relocate() walks to nearest existing
  ancestor and bounded-depth scans for a git root with matching
  remote.origin.url; unambiguous single match rewrites repos.json.
- ServeState::reconcile_all_paths() runs at startup before phase 1/2/3;
  relocates or warns+skips missing paths (never crashes).
- Existence guards added to phase-2 SCIP and phase-3 prewarm consumers.
- New `codesearch index prune` command: relocate-first, else unregister
  stale aliases, with summary output.
- CODESEARCH_RELOCATE_MAX_DEPTH env (default 3).
- Unit tests for capture-on-register and try_relocate (renamed leaf,
  path-exists, no-remote, ambiguous).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@

* @
[worker] stage 3/5: remove user-settable --alias, always derive

- Drop `--alias`/`-a` from `index add` subcommand and the legacy
  `index --add` flag path. Alias is always derived from the directory
  name via ReposConfig::register().
- add_to_index() loses its `alias` parameter; legacy current-dir local
  DBs are now auto-registered with a derived alias.
- Serve delegation always sends None so serve derives the alias too.
- Replace test_cli_index_add_accepts_alias_flag with
  test_cli_index_add_rejects_alias_flag + parses_without_alias.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@

* @
[worker] stage 4/5: tolerate hand-edited repos.json via reconcile()

- ReposConfig::reconcile() runs from load_from() on both new and legacy
  parse paths (in-memory only, no disk write):
  1. drop entries with empty/blank alias keys
  2. drop orphan repos_meta entries with no matching repo
  3. prune group members referencing unknown aliases; drop empty groups
- Never renames existing alias keys (would break group refs); a
  non-standard hand-edited alias is tolerated as-is. Never crashes.
- Unit tests for empty-key, group-pruning/empty-group, and orphan-meta.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@

* @
[worker] stage 5/5: docs + tighten reconcile() visibility

- Document stale-path relocation, `index prune`, derived-alias policy,
  and repos.json reconcile() in AGENTS.md and .claude/CLAUDE.md.
- reconcile() is now pub(crate) (only used internally + same-module tests).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@

* @
[worker] final review: use DB_DIR_NAME constant in relocation scan skip-list

Replace hardcoded ".codesearch.db" literal with crate::constants::DB_DIR_NAME
in is_skippable_scan_dir (no-hardcoded-config-strings rule).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@

* @
[worker] tests: extract testable prune_stale/relocate_missing + expand coverage

Refactor for testability (no behavior change):
- Add pure ReposConfig::relocate_missing() -> (relocated, unresolved) and
  prune_stale() -> (relocated, removed); no disk I/O, no logging.
- prune_index() and ServeState::reconcile_all_paths() now delegate to these,
  removing duplicated relocate-loop logic.

New unit tests (8):
- register_derives_alias_from_directory_name
- try_relocate_finds_renamed_parent (parent-level rename within depth)
- try_relocate_none_beyond_max_depth (depth bound enforced)
- relocate_missing_rewrites_only_moved_repos
- prune_stale_removes_unrelocatable_entries (+ group cleanup)
- prune_stale_relocates_then_keeps_relocatable_entries
- load_from_applies_reconcile_to_hand_edited_file (load-path reconcile)

24 repos lib tests pass; clippy clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@

* @
docs: README + CHANGELOG for relocation, index prune, derived alias

- README: document `codesearch index prune`, automatic relocation of
  moved/renamed repos (CODESEARCH_RELOCATE_MAX_DEPTH), the alias-always-
  derived policy (no --alias flag), and hand-edited repos.json tolerance.
- CHANGELOG: consolidated 1.0.149 entry (Added/Changed/Fixed).
- README language table + alias example updates (pre-existing).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@

* @
[worker] address review remarks: align CHANGELOG version + restore log path

- CHANGELOG entry retitled to 1.0.151 to match the shipped Cargo.toml
  version (pre-commit bumps patch by 1 on this commit).
- reconcile warn for unresolved repos again includes the missing path for
  diagnostics (lost during the relocate_missing extraction).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@

---------

Co-authored-by: flupkede <flupkede@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Auto-prune stale repos during Phase 1 warmup (v1.0.153) (#94)

* feat: auto-prune stale repos during Phase 1 warmup

When a repo's database or path no longer exists (e.g. folder moved),
Phase 1 now automatically unregisters the alias from repos.json instead
of logging a warning and leaving the stale entry forever.

Prune conditions (safe — only missing-db / path-gone, not transient errors):
- .codesearch.db directory does not exist at registered path
- Registered path itself no longer exists
- Alias resolves to nothing in config

Side effects per pruned alias:
- stop_fsw + evict from DashMap + remove last_access timer
- unregister_alias (removes from repos, repos_meta, groups)
- persist via config.save()

Closes: stale repos.json entries after folder reorganization

* fix: add missing YELLOW color variable in qc.sh

* bump version to 1.0.153 — align with CHANGELOG

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: flupkede <flupkede@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: AGENTS.md plan for fixes-strict-scoping-and-reaper (#95)

Follow-up on PR #42 + #43 audit. Two gaps identified:
- No automated tests for new Warm/Write state semantics, zombie-proof reaper, or /status endpoint
- No HTTP timeouts in standalone TUI reqwest calls

Co-authored-by: flupkede <flupkede@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: AGENTS.md plan for serve single-instance guard (#96)

Co-authored-by: flupkede <flupkede@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: Windows 8.3 short-name path mismatch in relocation tests + CHANGELOG (#98)

Squash merge fix/windows-8dot3-path-relocation → develop

* sync: align develop with master after v1.0.154 release (#100)

* Fix formatting of codesearch index command

* Create codeql.yml

* feat: add tree-sitter grammars for Bash, Ruby, PHP, YAML, JSON

* fix: CI test resilience + protect-master workflow (#58) (#59)

* release: v1.0.132 — tree-sitter expansion, LMDB stability, CI hardening (#64)

* fix: CI test resilience + protect-master workflow (#58)

* Sync master → develop (tree-sitter) (#60)

* Fix formatting of codesearch index command

* Create codeql.yml

* feat: add tree-sitter grammars for Bash, Ruby, PHP, YAML, JSON

* fix: CI test resilience + protect-master workflow (#58) (#59)

---------

Co-authored-by: flupkede <flupkede@users.noreply.github.com>

* docs: update CHANGELOG — v1.0.132 consolidated release notes (#61)

* [worker] cleanup: AGENTS.md — 73% reduction, removed stale test report and duplicate bug details

* docs: update CHANGELOG — v1.0.132 consolidated release notes (v1.0.97...v1.0.132)

---------

Co-authored-by: flupkede <flupkede@users.noreply.github.com>

* sync: align develop with master — AGENTS.md, Cargo.toml, Cargo.lock (#63)

Co-authored-by: flupkede <flupkede@users.noreply.github.com>

---------

Co-authored-by: flupkede <flupkede@users.noreply.github.com>

* Release v1.0.135: MCP local mode fix + QC fix + release branch support (#72)

* Release v1.0.134: MCP local mode fix + QC script fix (closes #65)

* ci: allow release/* branches to target master PRs

* docs: add CHANGELOG entry for v1.0.135 (MCP local mode fix) (#73)

* Release v1.0.137 — serve-aware indexing fixes (#77)

* fix: CI test resilience + protect-master workflow (#58)

* Sync master → develop (tree-sitter) (#60)

* Fix formatting of codesearch index command

* Create codeql.yml

* feat: add tree-sitter grammars for Bash, Ruby, PHP, YAML, JSON

* fix: CI test resilience + protect-master workflow (#58) (#59)

---------

Co-authored-by: flupkede <flupkede@users.noreply.github.com>

* docs: update CHANGELOG — v1.0.132 consolidated release notes (#61)

* [worker] cleanup: AGENTS.md — 73% reduction, removed stale test report and duplicate bug details

* docs: update CHANGELOG — v1.0.132 consolidated release notes (v1.0.97...v1.0.132)

---------

Co-authored-by: flupkede <flupkede@users.noreply.github.com>

* sync: align develop with master — AGENTS.md, Cargo.toml, Cargo.lock (#63)

Co-authored-by: flupkede <flupkede@users.noreply.github.com>

* fix: MCP local mode project/group fallback + QC script fix (#66)

* fix(mcp): ignore project/group params in local/stdio mode instead of erroring

When running MCP in local mode (no serve_state), project/group routing is meaningless because only one DB is available.

Log a warning and fall back to local DB instead of returning an error.

* fix(qc): define YELLOW color in bash QC script

* chore: simplify release workflow — feature-only version bump (#74)

* fix: serve-aware indexing — create DB dir before lock + no silent local duplicate (v1.0.137) (#76)

* fix: create DB directory before acquiring writer lock (serve auto-register)

When `serve` is running and `codesearch index` is run for a repo not yet known
to it, auto-register (POST /repos) failed with a misleading "Database is locked
by another process" 500: SharedStores::new() acquired the writer lock before
the .codesearch.db directory existed, so opening .writer.lock failed with
"path not found". This rolled back the repos.json registration and made the CLI
fall back to a local duplicate index instead of delegating to serve.

- acquire_writer_lock / SharedStores::new now create the DB directory first;
  genuine I/O errors surface distinctly instead of as a lock conflict.
- Serve config writes route through ServeState::persist_config() (honors the
  config path override) — production behavior unchanged, register/remove path
  now hermetically testable.
- Regression guards exercise the brand-new-repo create/register path with the
  DB directory genuinely absent (verified to fail against the pre-fix code).
- CHANGELOG: 1.0.136.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: never silently create a local duplicate index when serve is busy

The CLI probes serve's /health before delegating `index`/`index add`. Any
health failure — including a *timeout* while serve is warming up its repos at
startup — was classified as "serve not running", so the CLI silently created a
local index. That local index is a duplicate serve does not manage and can
cause LMDB file-lock conflicts (and the repo never gets registered with serve).

New behavior via probe_serve_health():
- Responsive  -> delegate as before.
- Connection refused / cannot connect -> serve not running; index locally.
  Detected immediately (no timeout elapses, no retries), so the common
  "no serve -> local" path is NOT slowed down.
- Listening but unresponsive (timeout, retried briefly) -> serve is up but
  busy. The CLI now REFUSES to create a local duplicate and tells the user to
  retry shortly or stop serve first. The fallback is never silent anymore.

Delegation errors are now typed (DelegateError: ServeDown / ServeUnresponsive /
Failed) instead of string-matched. Applies to `index` and `index add` (the
index-creating paths); `index rm` is unchanged.

Tests: probe classification guards (responsive -> Up; listening-but-slow ->
Unresponsive). Rolls into the 1.0.137 release together with the writer-lock fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: flupkede <flupkede@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: flupkede <flupkede@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Release v1.0.138 — strip UNC paths + auto-add on missing DB (#80)

* fix: CI test resilience + protect-master workflow (#58)

* Sync master → develop (tree-sitter) (#60)

* Fix formatting of codesearch index command

* Create codeql.yml

* feat: add tree-sitter grammars for Bash, Ruby, PHP, YAML, JSON

* fix: CI test resilience + protect-master workflow (#58) (#59)

---------

Co-authored-by: flupkede <flupkede@users.noreply.github.com>

* docs: update CHANGELOG — v1.0.132 consolidated release notes (#61)

* [worker] cleanup: AGENTS.md — 73% reduction, removed stale test report and duplicate bug details

* docs: update CHANGELOG — v1.0.132 consolidated release notes (v1.0.97...v1.0.132)

---------

Co-authored-by: flupkede <flupkede@users.noreply.github.com>

* sync: align develop with master — AGENTS.md, Cargo.toml, Cargo.lock (#63)

Co-authored-by: flupkede <flupkede@users.noreply.github.com>

* fix: MCP local mode project/group fallback + QC script fix (#66)

* fix(mcp): ignore project/group params in local/stdio mode instead of erroring

When running MCP in local mode (no serve_state), project/group routing is meaningless because only one DB is available.

Log a warning and fall back to local DB instead of returning an error.

* fix(qc): define YELLOW color in bash QC script

* chore: simplify release workflow — feature-only version bump (#74)

* fix: serve-aware indexing — create DB dir before lock + no silent local duplicate (v1.0.137) (#76)

* fix: create DB directory before acquiring writer lock (serve auto-register)

When `serve` is running and `codesearch index` is run for a repo not yet known
to it, auto-register (POST /repos) failed with a misleading "Database is locked
by another process" 500: SharedStores::new() acquired the writer lock before
the .codesearch.db directory existed, so opening .writer.lock failed with
"path not found". This rolled back the repos.json registration and made the CLI
fall back to a local duplicate index instead of delegating to serve.

- acquire_writer_lock / SharedStores::new now create the DB directory first;
  genuine I/O errors surface distinctly instead of as a lock conflict.
- Serve config writes route through ServeState::persist_config() (honors the
  config path override) — production behavior unchanged, register/remove path
  now hermetically testable.
- Regression guards exercise the brand-new-repo create/register path with the
  DB directory genuinely absent (verified to fail against the pre-fix code).
- CHANGELOG: 1.0.136.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: never silently create a local duplicate index when serve is busy

The CLI probes serve's /health before delegating `index`/`index add`. Any
health failure — including a *timeout* while serve is warming up its repos at
startup — was classified as "serve not running", so the CLI silently created a
local index. That local index is a duplicate serve does not manage and can
cause LMDB file-lock conflicts (and the repo never gets registered with serve).

New behavior via probe_serve_health():
- Responsive  -> delegate as before.
- Connection refused / cannot connect -> serve not running; index locally.
  Detected immediately (no timeout elapses, no retries), so the common
  "no serve -> local" path is NOT slowed down.
- Listening but unresponsive (timeout, retried briefly) -> serve is up but
  busy. The CLI now REFUSES to create a local duplicate and tells the user to
  retry shortly or stop serve first. The fallback is never silent anymore.

Delegation errors are now typed (DelegateError: ServeDown / ServeUnresponsive /
Failed) instead of string-matched. Applies to `index` and `index add` (the
index-creating paths); `index rm` is unchanged.

Tests: probe classification guards (responsive -> Up; listening-but-slow ->
Unresponsive). Rolls into the 1.0.137 release together with the writer-lock fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: flupkede <flupkede@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* sync: align develop with master (post-v1.0.137 release) (#78)

Brings the two files that drifted on master during the v1.0.137 release back
to develop: the updated protect-master.yml (allows release/* branches) and the
CHANGELOG [1.0.135] entry. After this, develop and master trees are identical.

Co-authored-by: flupkede <flupkede@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: strip UNC prefix in repos.json + auto-add on missing DB (v1.0.138) (#79)

Two Windows path-handling bugs that caused spurious "Database not found"
errors and local duplicate indexes:

1. register()/register_with_alias() stored the raw canonicalize() result in
   repos.json. On Windows, canonicalize() returns \?\C:\... (extended-length
   UNC prefix). Downstream .join(".codesearch.db") and Path::exists() calls
   then fail inconsistently (\?\C:\foo\.codesearch.db not found even when
   C:\foo\.codesearch.db exists). 7 repos were affected. Fix: strip_unc()
   removes the prefix before storage. Existing repos.json patched in-place.
   Regression test: register_strips_unc_prefix_from_stored_path.

2. 500 "Database not found" from reindex (alias registered but DB gone) was
   treated as a generic failure -> local fallback -> duplicate index. Fix:
   triggers the same auto-register POST /repos path as 404 (DB recreated by
   serve, no local fallback).

Co-authored-by: flupkede <flupkede@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: flupkede <flupkede@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Release v1.0.139 — central safe_canonicalize() for all path ops (#83)

* fix: CI test resilience + protect-master workflow (#58)

* Sync master → develop (tree-sitter) (#60)

* Fix formatting of codesearch index command

* Create codeql.yml

* feat: add tree-sitter grammars for Bash, Ruby, PHP, YAML, JSON

* fix: CI test resilience + protect-master workflow (#58) (#59)

---------

Co-authored-by: flupkede <flupkede@users.noreply.github.com>

* docs: update CHANGELOG — v1.0.132 consolidated release notes (#61)

* [worker] cleanup: AGENTS.md — 73% reduction, removed stale test report and duplicate bug details

* docs: update CHANGELOG — v1.0.132 consolidated release notes (v1.0.97...v1.0.132)

---------

Co-authored-by: flupkede <flupkede@users.noreply.github.com>

* sync: align develop with master — AGENTS.md, Cargo.toml, Cargo.lock (#63)

Co-authored-by: flupkede <flupkede@users.noreply.github.com>

* fix: MCP local mode project/group fallback + QC script fix (#66)

* fix(mcp): ignore project/group params in local/stdio mode instead of erroring

When running MCP in local mode (no serve_state), project/group routing is meaningless because only one DB is available.

Log a warning and fall back to local DB instead of returning an error.

* fix(qc): define YELLOW color in bash QC script

* chore: simplify release workflow — feature-only version bump (#74)

* fix: serve-aware indexing — create DB dir before lock + no silent local duplicate (v1.0.137) (#76)

* fix: create DB directory before acquiring writer lock (serve auto-register)

When `serve` is running and `codesearch index` is run for a repo not yet known
to it, auto-register (POST /repos) failed with a misleading "Database is locked
by another process" 500: SharedStores::new() acquired the writer lock before
the .codesearch.db directory existed, so opening .writer.lock failed with
"path not found". This rolled back the repos.json registration and made the CLI
fall back to a local duplicate index instead of delegating to serve.

- acquire_writer_lock / SharedStores::new now create the DB directory first;
  genuine I/O errors surface distinctly instead of as a lock conflict.
- Serve config writes route through ServeState::persist_config() (honors the
  config path override) — production behavior unchanged, register/remove path
  now hermetically testable.
- Regression guards exercise the brand-new-repo create/register path with the
  DB directory genuinely absent (verified to fail against the pre-fix code).
- CHANGELOG: 1.0.136.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: never silently create a local duplicate index when serve is busy

The CLI probes serve's /health before delegating `index`/`index add`. Any
health failure — including a *timeout* while serve is warming up its repos at
startup — was classified as "serve not running", so the CLI silently created a
local index. That local index is a duplicate serve does not manage and can
cause LMDB file-lock conflicts (and the repo never gets registered with serve).

New behavior via probe_serve_health():
- Responsive  -> delegate as before.
- Connection refused / cannot connect -> serve not running; index locally.
  Detected immediately (no timeout elapses, no retries), so the common
  "no serve -> local" path is NOT slowed down.
- Listening but unresponsive (timeout, retried briefly) -> serve is up but
  busy. The CLI now REFUSES to create a local duplicate and tells the user to
  retry shortly or stop serve first. The fallback is never silent anymore.

Delegation errors are now typed (DelegateError: ServeDown / ServeUnresponsive /
Failed) instead of string-matched. Applies to `index` and `index add` (the
index-creating paths); `index rm` is unchanged.

Tests: probe classification guards (responsive -> Up; listening-but-slow ->
Unresponsive). Rolls into the 1.0.137 release together with the writer-lock fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: flupkede <flupkede@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* sync: align develop with master (post-v1.0.137 release) (#78)

Brings the two files that drifted on master during the v1.0.137 release back
to develop: the updated protect-master.yml (allows release/* branches) and the
CHANGELOG [1.0.135] entry. After this, develop and master trees are identical.

Co-authored-by: flupkede <flupkede@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: strip UNC prefix in repos.json + auto-add on missing DB (v1.0.138) (#79)

Two Windows path-handling bugs that caused spurious "Database not found"
errors and local duplicate indexes:

1. register()/register_with_alias() stored the raw canonicalize() result in
   repos.json. On Windows, canonicalize() returns \?\C:\... (extended-length
   UNC prefix). Downstream .join(".codesearch.db") and Path::exists() calls
   then fail inconsistently (\?\C:\foo\.codesearch.db not found even when
   C:\foo\.codesearch.db exists). 7 repos were affected. Fix: strip_unc()
   removes the prefix before storage. Existing repos.json patched in-place.
   Regression test: register_strips_unc_prefix_from_stored_path.

2. 500 "Database not found" from reindex (alias registered but DB gone) was
   treated as a generic failure -> local fallback -> duplicate index. Fix:
   triggers the same auto-register POST /repos path as 404 (DB recreated by
   serve, no local fallback).

Co-authored-by: flupkede <flupkede@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* sync: align develop with master (post-v1.0.138) (#81)

Co-authored-by: flupkede <flupkede@users.noreply.github.com>

* refactor: central safe_canonicalize() — eliminate raw .canonicalize() across codebase (#82)

ROOT CAUSE OF RECURRING BUG CLASS
Path::canonicalize() on Windows returns \?\C:\... (extended-length UNC
prefix). Any downstream .join(), .exists(), or HashMap key built from that
path behaves inconsistently — the sub-path \?\C:\foo\.codesearch.db may
return false from exists() even when C:\foo\.codesearch.db is present.
This class of bug has silently broken registrations multiple times.

FIX
Introduce safe_canonicalize(path: &Path) -> io::Result<PathBuf> and
strip_unc_prefix(path: PathBuf) -> PathBuf in src/cache/file_meta.rs.
These are the ONLY approved way to canonicalize paths in this codebase.
Exported via crate::cache.

CALL SITES UPDATED (all raw .canonicalize() removed)
- src/cache/file_meta.rs      — central definition + 5 new regression tests
- src/db_discovery/repos.rs   — register, register_with_alias, unregister_path,
                                alias_for_path; local strip_unc() removed
- src/db_discovery/mod.rs     — find_best_database, get_db_path_for_cwd
- src/index/mod.rs            — find_git_root, get_global_db_path,
                                add_to_index, remove_from_index,
                                try_delegate_reindex_to_serve (x2),
                                try_delegate_rm_to_serve
- src/lmdb_registry.rs        — TrackedEnv registry key (eliminates
                                double-open risk when same dir accessed
                                with and without \?\ prefix)
- src/serve/mod.rs            — add_repo_handler, run_serve --register path

POLICY DOCUMENTED
AGENTS.md: "⚠️ Canonical Path Policy — MANDATORY" section with rule,
code example, and pointer to regression tests.

REGRESSION TESTS (6 new in cache/file_meta.rs + 1 existing in repos.rs)
- strip_unc_prefix_removes_windows_unc
- strip_unc_prefix_is_idempotent_on_{plain_path,unix_path}
- safe_canonicalize_on_existing_dir_returns_plain_path
- safe_canonicalize_on_nonexistent_path_returns_error
- register_strips_unc_prefix_from_stored_path (repos.rs — verifies
  fallback path also strips UNC when canonicalize() fails)

407 lib tests pass. clippy -D warnings clean.

Co-authored-by: flupkede <flupkede@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: remove stale strip_unc() from repos.rs (merged from master, superseded by central safe_canonicalize)

---------

Co-authored-by: flupkede <flupkede@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Release v1.0.140 — last raw .canonicalize() eliminated (#85)

* fix: CI test resilience + protect-master workflow (#58)

* Sync master → develop (tree-sitter) (#60)

* Fix formatting of codesearch index command

* Create codeql.yml

* feat: add tree-sitter grammars for Bash, Ruby, PHP, YAML, JSON

* fix: CI test resilience + protect-master workflow (#58) (#59)

---------

Co-authored-by: flupkede <flupkede@users.noreply.github.com>

* docs: update CHANGELOG — v1.0.132 consolidated release notes (#61)

* [worker] cleanup: AGENTS.md — 73% reduction, removed stale test report and duplicate bug details

* docs: update CHANGELOG — v1.0.132 consolidated release notes (v1.0.97...v1.0.132)

---------

Co-authored-by: flupkede <flupkede@users.noreply.github.com>

* sync: align develop with master — AGENTS.md, Cargo.toml, Cargo.lock (#63)

Co-authored-by: flupkede <flupkede@users.noreply.github.com>

* fix: MCP local mode project/group fallback + QC script fix (#66)

* fix(mcp): ignore project/group params in local/stdio mode instead of erroring

When running MCP in local mode (no serve_state), project/group routing is meaningless because only one DB is available.

Log a warning and fall back to local DB instead of returning an error.

* fix(qc): define YELLOW color in bash QC script

* chore: simplify release workflow — feature-only version bump (#74)

* fix: serve-aware indexing — create DB dir before lock + no silent local duplicate (v1.0.137) (#76)

* fix: create DB directory before acquiring writer lock (serve auto-register)

When `serve` is running and `codesearch index` is run for a repo not yet known
to it, auto-register (POST /repos) failed with a misleading "Database is locked
by another process" 500: SharedStores::new() acquired the writer lock before
the .codesearch.db directory existed, so opening .writer.lock failed with
"path not found". This rolled back the repos.json registration and made the CLI
fall back to a local duplicate index instead of delegating to serve.

- acquire_writer_lock / SharedStores::new now create the DB directory first;
  genuine I/O errors surface distinctly instead of as a lock conflict.
- Serve config writes route through ServeState::persist_config() (honors the
  config path override) — production behavior unchanged, register/remove path
  now hermetically testable.
- Regression guards exercise the brand-new-repo create/register path with the
  DB directory genuinely absent (verified to fail against the pre-fix code).
- CHANGELOG: 1.0.136.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: never silently create a local duplicate index when serve is busy

The CLI probes serve's /health before delegating `index`/`index add`. Any
health failure — including a *timeout* while serve is warming up its repos at
startup — was classified as "serve not running", so the CLI silently created a
local index. That local index is a duplicate serve does not manage and can
cause LMDB file-lock conflicts (and the repo never gets registered with serve).

New behavior via probe_serve_health():
- Responsive  -> delegate as before.
- Connection refused / cannot connect -> serve not running; index locally.
  Detected immediately (no timeout elapses, no retries), so the common
  "no serve -> local" path is NOT slowed down.
- Listening but unresponsive (timeout, retried briefly) -> serve is up but
  busy. The CLI now REFUSES to create a local duplicate and tells the user to
  retry shortly or stop serve first. The fallback is never silent anymore.

Delegation errors are now typed (DelegateError: ServeDown / ServeUnresponsive /
Failed) instead of string-matched. Applies to `index` and `index add` (the
index-creating paths); `index rm` is unchanged.

Tests: probe classification guards (responsive -> Up; listening-but-slow ->
Unresponsive). Rolls into the 1.0.137 release together with the writer-lock fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: flupkede <flupkede@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* sync: align develop with master (post-v1.0.137 release) (#78)

Brings the two files that drifted on master during the v1.0.137 release back
to develop: the updated protect-master.yml (allows release/* branches) and the
CHANGELOG [1.0.135] entry. After this, develop and master trees are identical.

Co-authored-by: flupkede <flupkede@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: strip UNC prefix in repos.json + auto-add on missing DB (v1.0.138) (#79)

Two Windows path-handling bugs that caused spurious "Database not found"
errors and local duplicate indexes:

1. register()/register_with_alias() stored the raw canonicalize() result in
   repos.json. On Windows, canonicalize() returns \?\C:\... (extended-length
   UNC prefix). Downstream .join(".codesearch.db") and Path::exists() calls
   then fail inconsistently (\?\C:\foo\.codesearch.db not found even when
   C:\foo\.codesearch.db exists). 7 repos were affected. Fix: strip_unc()
   removes the prefix before storage. Existing repos.json patched in-place.
   Regression test: register_strips_unc_prefix_from_stored_path.

2. 500 "Database not found" from reindex (alias registered but DB gone) was
   treated as a generic failure -> local fallback -> duplicate index. Fix:
   triggers the same auto-register POST /repos path as 404 (DB recreated by
   serve, no local fallback).

Co-authored-by: flupkede <flupkede@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* sync: align develop with master (post-v1.0.138) (#81)

Co-authored-by: flupkede <flupkede@users.noreply.github.com>

* refactor: central safe_canonicalize() — eliminate raw .canonicalize() across codebase (#82)

ROOT CAUSE OF RECURRING BUG CLASS
Path::canonicalize() on Windows returns \?\C:\... (extended-length UNC
prefix). Any downstream .join(), .exists(), or HashMap key built from that
path behaves inconsistently — the sub-path \?\C:\foo\.codesearch.db may
return false from exists() even when C:\foo\.codesearch.db is present.
This class of bug has silently broken registrations multiple times.

FIX
Introduce safe_canonicalize(path: &Path) -> io::Result<PathBuf> and
strip_unc_prefix(path: PathBuf) -> PathBuf in src/cache/file_meta.rs.
These are the ONLY approved way to canonicalize paths in this codebase.
Exported via crate::cache.

CALL SITES UPDATED (all raw .canonicalize() removed)
- src/cache/file_meta.rs      — central definition + 5 new regression tests
- src/db_discovery/repos.rs   — register, register_with_alias, unregister_path,
                                alias_for_path; local strip_unc() removed
- src/db_discovery/mod.rs     — find_best_database, get_db_path_for_cwd
- src/index/mod.rs            — find_git_root, get_global_db_path,
                                add_to_index, remove_from_index,
                                try_delegate_reindex_to_serve (x2),
                                try_delegate_rm_to_serve
- src/lmdb_registry.rs        — TrackedEnv registry key (eliminates
                                double-open risk when same dir accessed
                                with and without \?\ prefix)
- src/serve/mod.rs            — add_repo_handler, run_serve --register path

POLICY DOCUMENTED
AGENTS.md: "⚠️ Canonical Path Policy — MANDATORY" section with rule,
code example, and pointer to regression tests.

REGRESSION TESTS (6 new in cache/file_meta.rs + 1 existing in repos.rs)
- strip_unc_prefix_removes_windows_unc
- strip_unc_prefix_is_idempotent_on_{plain_path,unix_path}
- safe_canonicalize_on_existing_dir_returns_plain_path
- safe_canonicalize_on_nonexistent_path_returns_error
- register_strips_unc_prefix_from_stored_path (repos.rs — verifies
  fallback path also strips UNC when canonicalize() fails)

407 lib tests pass. clippy -D warnings clean.

Co-authored-by: flupkede <flupkede@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: replace last raw .canonicalize() with safe_canonicalize in get_db_path_smart (#84)

The old normalize_path(&p.canonicalize()...) pattern in get_db_path_smart
was missed in the central safe_canonicalize refactor (v1.0.139). It worked
correctly (normalize_path also strips UNC) but was inconsistent with the
policy. Now all .canonicalize() calls outside safe_canonicalize's own
definition are eliminated.

Co-authored-by: flupkede <flupkede@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: flupkede <flupkede@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Release v1.0.141 — serve warmup wait + 409 DB-recreate fix (#87)

* fix: CI test resilience + protect-master workflow (#58)

* Sync master → develop (tree-sitter) (#60)

* Fix formatting of codesearch index command

* Create codeql.yml

* feat: add tree-sitter grammars for Bash, Ruby, PHP, YAML, JSON

* fix: CI test resilience + protect-master workflow (#58) (#59)

---------

Co-authored-by: flupkede <flupkede@users.noreply.github.com>

* docs: update CHANGELOG — v1.0.132 consolidated release notes (#61)

* [worker] cleanup: AGENTS.md — 73% reduction, removed stale test report and duplicate bug details

* docs: update CHANGELOG — v1.0.132 consolidated release notes (v1.0.97...v1.0.132)

---------

Co-authored-by: flupkede <flupkede@users.noreply.github.com>

* sync: align develop with master — AGENTS.md, Cargo.toml, Cargo.lock (#63)

Co-authored-by: flupkede <flupkede@users.noreply.github.com>

* fix: MCP local mode project/group fallback + QC script fix (#66)

* fix(mcp): ignore project/group params in local/stdio mode instead of erroring

When running MCP in local mode (no serve_state), project/group routing is meaningless because only one DB is available.

Log a warning and fall back to local DB instead of returning an error.

* fix(qc): define YELLOW color in bash QC script

* chore: simplify release workflow — feature-only version bump (#74)

* fix: serve-aware indexing — create DB dir before lock + no silent local duplicate (v1.0.137) (#76)

* fix: create DB directory before acquiring writer lock (serve auto-register)

When `serve` is running and `codesearch index` is run for a repo not yet known
to it, auto-register (POST /repos) failed with a misleading "Database is locked
by another process" 500: SharedStores::new() acquired the writer lock before
the .codesearch.db directory existed, so opening .writer.lock failed with
"path not found". This rolled back the repos.json registration and made the CLI
fall back to a local duplicate index instead of delegating to serve.

- acquire_writer_lock / SharedStores::new now create the DB directory first;
  genuine I/O errors surface distinctly instead of as a lock conflict.
- Serve config writes route through ServeState::persist_config() (honors the
  config path override) — production behavior unchanged, register/remove path
  now hermetically testable.
- Regression guards exercise the brand-new-repo create/register path with the
  DB directory genuinely absent (verified to fail against the pre-fix code).
- CHANGELOG: 1.0.136.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: never silently create a local duplicate index when serve is busy

The CLI probes serve's /health before delegating `index`/`index add`. Any
health failure — including a *timeout* while serve is warming up its repos at
startup — was classified as "serve not running", so the CLI silently created a
local index. That local index is a duplicate serve does not manage and can
cause LMDB file-lock conflicts (and the repo never gets registered with serve).

New behavior via probe_serve_health():
- Responsive  -> delegate as before.
- Connection refused / cannot connect -> serve not running; index locally.
  Detected immediately (no timeout elapses, no retries), so the common
  "no serve -> local" path is NOT slowed down.
- Listening but unresponsive (timeout, retried briefly) -> serve is up but
  busy. The CLI now REFUSES to create a local duplicate and tells the user to
  retry shortly or stop serve first. The fallback is never silent anymore.

Delegation errors are now typed (DelegateError: ServeDown / ServeUnresponsive /
Failed) instead of string-matched. Applies to `index` and `index add` (the
index-creating paths); `index rm` is unchanged.

Tests: probe classification guards (responsive -> Up; listening-but-slow ->
Unresponsive). Rolls into the 1.0.137 release together with the writer-lock fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: flupkede <flupkede@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* sync: align develop with master (post-v1.0.137 release) (#78)

Brings the two files that drifted on master during the v1.0.137 release back
to develop: the updated protect-master.yml (allows release/* branches) and the
CHANGELOG [1.0.135] entry. After this, develop and master trees are identical.

Co-authored-by: flupkede <flupkede@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: strip UNC prefix in repos.json + auto-add on missing DB (v1.0.138) (#79)

Two Windows path-handling bugs that caused spurious "Database not found"
errors and local duplicate indexes:

1. register()/register_with_alias() stored the raw canonicalize() result in
   repos.json. On Windows, canonicalize() returns \?\C:\... (extended-length
   UNC prefix). Downstream .join(".codesearch.db") and Path::exists() calls
   then fail inconsistently (\?\C:\foo\.codesearch.db not found even when
   C:\foo\.codesearch.db exists). 7 repos were affected. Fix: strip_unc()
   removes the prefix before storage. Existing repos.json patched in-place.
   Regression test: register_strips_unc_prefix_from_stored_path.

2. 500 "Database not found" from reindex (alias registered but DB gone) was
   treated as a generic failure -> local fallback -> duplicate index. Fix:
   triggers the same auto-register POST /repos path as 404 (DB recreated by
   serve, no local fallback).

Co-authored-by: flupkede <flupkede@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* sync: align develop with master (post-v1.0.138) (#81)

Co-authored-by: flupkede <flupkede@users.noreply.github.com>

* refactor: central safe_canonicalize() — eliminate raw .canonicalize() across codebase (#82)

ROOT CAUSE OF RECURRING BUG CLASS
Path::canonicalize() on Windows returns \?\C:\... (extended-length UNC
prefix). Any downstream .join(), .exists(), or HashMap key built from that
path behaves inconsistently — the sub-path \?\C:\foo\.codesearch.db may
return false from exists() even when C:\foo\.codesearch.db is present.
This class of bug has silently broken registrations multiple times.

FIX
Introduce safe_canonicalize(path: &Path) -> io::Result<PathBuf> and
strip_unc_prefix(path: PathBuf) -> PathBuf in src/cache/file_meta.rs.
These are the ONLY approved way to canonicalize paths in this codebase.
Exported via crate::cache.

CALL SITES UPDATED (all raw .canonicalize() removed)
- src/cache/file_meta.rs      — central definition + 5 new regression tests
- src/db_discovery/repos.rs   — register, register_with_alias, unregister_path,
                                alias_for_path; local strip_unc() removed
- src/db_discovery/mod.rs     — find_best_database, get_db_path_for_cwd
- src/index/mod.rs            — find_git_root, get_global_db_path,
                                add_to_index, remove_from_index,
                                try_delegate_reindex_to_serve (x2),
                                try_delegate_rm_to_serve
- src/lmdb_registry.rs        — TrackedEnv registry key (eliminates
                                double-open risk when same dir accessed
                                with and without \?\ prefix)
- src/serve/mod.rs            — add_repo_handler, run_serve --register path

POLICY DOCUMENTED
AGENTS.md: "⚠️ Canonical Path Policy — MANDATORY" section with rule,
code example, and pointer to regression tests.

REGRESSION TESTS (6 new in cache/file_meta.rs + 1 existing in repos.rs)
- strip_unc_prefix_removes_windows_unc
- strip_unc_prefix_is_idempotent_on_{plain_path,unix_path}
- safe_canonicalize_on_existing_dir_returns_plain_path
- safe_canonicalize_on_nonexistent_path_returns_error
- register_strips_unc_prefix_from_stored_path (repos.rs — verifies
  fallback path also strips UNC when canonicalize() fails)

407 lib tests pass. clippy -D warnings clean.

Co-authored-by: flupkede <flupkede@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: replace last raw .canonicalize() with safe_canonicalize in get_db_path_smart (#84)

The old normalize_path(&p.canonicalize()...) pattern in get_db_path_smart
was missed in the central safe_canonicalize refactor (v1.0.139). It worked
correctly (normalize_path also strips UNC) but was inconsistent with the
policy. Now all .canonicalize() calls outside safe_canonicalize's own
definition are eliminated.

Co-authored-by: flupkede <flupkede@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: wait for serve warmup instead of refusing; fix 409 on DB-recreate (#86)

PROBLEM 1 — ServeUnresponsive aborted with error instead of waiting
When serve is warming up (opening LMDB for 15+ repos blocks the tokio
runtime, causing /health to time out), the CLI refused with an error.
The user had to retry manually.

FIX: serve_delegate_with_warmup_wait() wraps both try_delegate_reindex_to_serve
and try_delegate_add_to_serve. On ServeUnresponsive it prints
"⏳ serve is starting up, waiting..." and retries every 8s up to 6 times
(~2 min budget). On success it prints "✅ serve is ready, delegating...".
Only exhausting the full budget returns an error.

PROBLEM 2 — 409 Conflict from POST /repos on "Database not found" path
When a registered repo's DB was missing, the CLI tried POST /repos to
recreate it. Serve correctly returned 409 (alias already registered).
The CLI treated 409 as a failure and fell back to local indexing.

FIX: when auto-add returns 409, retry as POST /repos/{alias}/reindex?force=true.
Force reindex uses allow_create=true and creates the DB via serve without
local fallback.

AGENTS.md: document the root cause (tokio blocking during warmup) as a
remaining work item with diagnosis and fix guidance.

Co-authored-by: flupkede <flupkede@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: flupkede <flupkede@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Release v1.0.142 — serve responsive during warmup (spawn_blocking) (#89)

* fix: CI test resilience + protect-master workflow (#58)

* Sync master → develop (tree-sitter) (#60)

* Fix formatting of codesearch index command

* Create codeql.yml

* feat: add tree-sitter grammars for Bash, Ruby, PHP, YAML, JSON

* fix: CI test resilience + protect-master workflow (#58) (#59)

---------

Co-authored-by: flupkede <flupkede@users.noreply.github.com>

* docs: update CHANGELOG — v1.0.132 consolidated release notes (#61)

* [worker] cleanup: AGENTS.md — 73% reduction, removed stale test report and duplicate bug details

* docs: update CHANGELOG — v1.0.132 consolidated release notes (v1.0.97...v1.0.132)

---------

Co-authored-by: flupkede <flupkede@users.noreply.github.com>

* sync: align develop with master — AGENTS.md, Cargo.toml, Cargo.lock (#63)

Co-authored-by: flupkede <flupkede@users.noreply.github.com>

* fix: MCP local mode project/group fallback + QC script fix (#66)

* fix(mcp): ignore project/group params in local/stdio mode instead of erroring

When running MCP in local mode (no serve_state), project/group routing is meaningless because only one DB is available.

Log a warning and fall back to local DB instead of returning an error.

* fix(qc): define YELLOW color in bash QC script

* chore: simplify release workflow — feature-only version bump (#74)

* fix: serve-aware indexing — create DB dir before lock + no silent local duplicate (v1.0.137) (#76)

* fix: create DB directory before acquiring writer lock (serve auto-register)

When `serve` is running and `codesearch index` is run for a repo not yet known
to it, auto-register (POST /repos) failed with a misleading "Database is locked
by another process" 500: SharedStores::new() acquired the writer lock before
the .codesearch.db directory existed, so opening .writer.lock failed with
"path not found". This rolled back the repos.json registration and made the CLI
fall back to a local duplicate index instead of delegating to serve.

- acquire_writer_lock / SharedStores::new now create the DB directory first;
  genuine I/O errors surface distinctly instead of as a lock conflict.
- Serve config writes route through ServeState::persist_config() (honors the
  config path override) — production behavior unchanged, register/remove path
  now hermetically testable.
- Regression guards exercise the brand-new-repo create/register path with the
  DB directory genuinely absent (verified to fail against the pre-fix code).
- CHANGELOG: 1.0.136.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: never silently create a local duplicate index when serve is busy

The CLI probes serve's /health before delegating `index`/`index add`. Any
health failure — including a *timeout* while serve is warming up its repos at
startup …