# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [0.40.0] - 2026-05-29

### Added

- `alc-adviser` agent: Design Consultation mode. When the query expresses a build intent ("I want to build ...", "How should I combine ..."), the adviser switches from lookup mode to design consultation, searching existing packages as building blocks, finding reference implementations, and returning a `### Design Proposal` with architecture sketch and package combination suggestions. No Rust changes required — the mode uses existing MCP tools (`alc_pkg_list`, `alc_hub_search`, `alc_hub_info`).
- `PkgEntity` now carries a `type_source` field (`"explicit"` | `"auto_detected_runnable"` | `"auto_detected_library"`) recording the provenance of `pkg_type`. Backward-compatible via `#[serde(default)]`; legacy entries deserialize as `None`. Path A (`parse_from_init_lua`) and Path B (`LUA_TYPE_AUTODETECT` Lua snippet) both populate `type_source` independently.
- `alc_pkg_doctor`: new `unmarked_library` diagnostic bucket (11 total). Flags packages auto-detected as libraries (no `M.run`, no `M.meta.type`) and suggests adding `M.meta.type = "library"` for explicit declaration. Packages with `M.meta.type` explicitly set are unaffected; legacy entries without `type_source` are never flagged.
- `alc_pkg_list`: each package entry now includes an optional `warnings` array. When a package is auto-detected as a library, the entry carries a suggestion to declare `M.meta.type = "library"` explicitly. Field is omitted for entries without warnings.

### Fixed

- `alc_pkg_doctor`: alive symlinks under `~/.algocline/packages/` that are not registered in `installed.json`, `alc.toml`, or `alc.local.toml` are now reported (additive entries into `unmarked_library` or `unregistered_pkg`; JSON shape unchanged).
- `alc_pkg_doctor`: alive-symlink `type_source` detection now uses the same `eval_simple` + `LUA_TYPE_AUTODETECT` runtime path as `alc_pkg_list`, eliminating a static-parse divergence that caused 5 auto-detected library packages to be misclassified as `unregistered_pkg` instead of `unmarked_library`.

## [0.39.0] - 2026-05-28

### Added

- `PkgType` enum (`"runnable"` | `"library"`) introduced in `algocline-core`. Package authors can declare `M.meta.type = "runnable"` or `M.meta.type = "library"` in `init.lua`. When `M.meta.type` is absent the type is auto-detected at runtime: the Lua VM path uses `type(pkg.run) == "function"` (canonical; used by `alc_pkg_list`, `alc_advice`, and `alc_eval`); the offline `build_index` / `alc_hub_reindex` path uses a Rust text-scan (`detect_has_run`) as a mirror. A package without `M.run` defaults to `Library`.
- `pkg_type` field added to `PkgEntity` (serialized as `"type"` on the wire, `null` for legacy entries) and `ManifestEntry` (`installed.json`). Both are backward-compatible via `#[serde(default)]`.
- `alc_pkg_list` now includes the `type` field in each package entry.
- `alc_hub_reindex` / `alc_hub_search` now include the `type` field in `hub_index.json` entries and search results.
- `alc_advice`: library packages (resolved `type = "library"`) are rejected before `M.run` is invoked. The error message names the package and suggests `alc_run` with custom import code as an alternative.
- `alc_eval`: library packages are rejected before any LLM call is initiated. The error message names the package and suggests using a runnable package instead.
- Lua auto-detect snippet (`LUA_TYPE_AUTODETECT` constant in `resolve.rs`) is the single shared source for the `type(pkg.run)` detection logic injected by all runtime paths, preventing per-path divergence.
- `parse_meta` internal return type refactored from a 5-element tuple to a named `ParsedMeta` struct, making the parser self-documenting and future field additions non-breaking.

### Fixed

- `alc_hub_search`: pre-type-system `hub_index.json` entries (no `"type"` field) no longer produce `"type": null` in search results. `merge()` in `hub.rs` now defaults a missing `pkg_type` to `PkgType::Runnable` on both the remote-index path and the local-only fallback path. Packages with an explicit `"type"` value are unaffected. (backward compat fix: `hub.rs` `merge()`)

### Security

- Session ID generation now uses `rand::rng()` (ChaCha12 CSPRNG) instead of `RandomState`-based hashing (`gen_session_id` in `algocline-engine` and `gen_pool_sid` in pool dispatch). The previous `RandomState` path provided no cryptographic randomness guarantee and derived entropy solely from the per-process ASLR seed.
- App-layer session IDs (`alc-sess-*`) now include a `rand::rng().random::<u32>()` suffix in addition to the millisecond timestamp, eliminating guaranteed collisions for concurrent calls within the same millisecond.

## [0.38.6] - 2026-05-27

### Changed

- Bump `BUNDLED_SOURCES` tag for `algocline-swarm-frame` from v0.5.0 to v0.7.0 (5 spike examples + test infrastructure)

## [0.38.5] - 2026-05-26

### Added

- `M.meta.tags` support: package authors can declare `M.meta.tags = {"tag1", "tag2"}` in `init.lua`. Tags are parsed during `alc_hub_reindex`, projected into `hub_index.json`, and searchable via `alc_hub_search` (case-insensitive substring match against tags).
- `alc-coder` / `alc-refiner` agents now accept optional enrichment inputs (`reference_docs`, `negative_examples` for coder; `existing_tracker`, `reference_docs` for refiner) to provide richer context during package implementation and review.

### Fixed

- `alc-coder` agent now explicitly prohibits generating `LICENSE` files from LLM memory, preventing incorrect license text from being written into packages.
- `alc_hub_search` tag projection: fixed field inclusion so that `M.meta.tags` values appear correctly in search results.

## [0.38.4] - 2026-05-26

### Added

- `crates/algocline-app/src/service/lua/gendoc/docs/lint.lua` — new `W_META_LEGACY_M_VERSION` warning. When a pkg's `init.lua` defines a top-level `M.VERSION` field, `alc_hub_gendoc` now surfaces a non-blocking warning naming the canonical form (`M.meta.version` per `pkg-author-conventions.md` §2.1). The flag is collected by `extract.lua` (new `pkg_info.identity.legacy_m_version : boolean`) so lint emits regardless of whether `M.VERSION` and `M.meta.version` are kept in sync. Safe-to-remove when no external reference exists.

### Docs

- `docs/pkg-author-conventions.md` §2.1 — added `alc_shapes_compat` to the `M.meta` table as a **Recommended** field, closing a long-standing doc / implementation drift. The field has been first-class in `gendoc.rs` since the alc_shapes compat era (extract + SemVer-range validate + bundled-alc_shapes range mismatch error + undeclared warning) and is included in the `pkg_scaffold` template; only the convention doc was missing it. A Notes paragraph documents the existing gendoc enforcement behaviour (range mismatch → error; absent → warning).
- `docs/pkg-author-conventions.md` §5 — added `W_META_LEGACY_M_VERSION` row to the lint rules table.

### Internal

- `crates/algocline-engine/src/execution/registry.rs` — added `SessionRegistryV2::spawn_gc_task(ttl: Duration, interval: Duration)`, mirroring the legacy `SessionRegistry::spawn_gc_task` eviction contract (Option A: legacy parity). The GC task runs at the given `interval`, holds a write lock for the full check-and-remove sequence, and evicts only sessions that are both (a) idle beyond `ttl` (wall-clock ms via the new `last_active` field) and (b) have zero live broadcast subscribers (`bus_tx.receiver_count() == 0`), making eviction safe under concurrent `observe()` calls (Crux: concurrent GC eviction safety).
- `crates/algocline-engine/src/execution/record.rs` — added `pub(crate) last_active: Arc<AtomicI64>` to `SessionRecord`. Updated at driver-loop entry via `Ordering::Relaxed` (single writer = driver\_loop, single reader = GC), matching the `Session.last_activity_ms` pattern in legacy `session.rs` (Crux: legacy parity eviction behavior). Resolves debt #2.
- `crates/algocline-engine/src/execution/driver.rs` — `driver_loop` now accepts and stores the `last_active: Arc<AtomicI64>` parameter, writing `now_ms()` on entry to seed the GC idle clock.
- `crates/algocline-app/src/service/mod.rs` — `AppService::new` now wires `execution_registry.spawn_gc_task(Duration::from_secs(10800), Duration::from_secs(60))` immediately after constructing the V2 registry, matching the legacy 3-hour TTL / 60-second interval. Resolves debt #4.
- `crates/algocline-app/src/service/execution_service_impl.rs` — `observe_sink_free_when_no_subscribers` test rewritten to construct `SessionRegistryV2` directly with a sub-second TTL (100 ms) and interval (50 ms), then assert `ObserveError::NotFound` after a deterministic sleep that guarantees at least one GC tick post-TTL expiry. The previous dual-accept (`Ok` or `NotFound`) is replaced by a strict `NotFound` assertion (Crux: TTL override for test determinism). Resolves debt #3.

- `crates/algocline-engine/src/bridge/fork.rs` — removed redundant `.into_iter()` call on `results` (a `Vec<Option<Result<...>>>`) in the `strategy_names.iter().zip(results.into_iter()).enumerate()` chain; `zip` accepts `impl IntoIterator` directly, so the explicit conversion was flagged as `clippy::useless_conversion`. Iteration order, item count, and `(name, result)` pairing are unchanged. Verified that `results` satisfies `IntoIterator` (not the `Iterator` trait itself) at the call site, confirming the implicit conversion path through `zip`'s bound produces identical item types.
- `crates/algocline-app/src/service/logging.rs` — replaced `sort_by(|a, b| b.1.cmp(&a.1))` with `sort_by_key(|b| std::cmp::Reverse(b.1))` to clear a `clippy::unnecessary_sort_by` warning found in the same workspace-wide audit pass. Sort order (newest first) unchanged.
- `crates/algocline-engine/src/execution/registry.rs` — `SessionRegistryV2::observe()`:
  added `tracing::warn!(target = "session.observe", ...)` in the `try_read()` `Err` arm
  to surface lock contention as an observable log event. `NotFound` return is preserved
  (no breaking change). Resolves debt #1 (tracing-missing-on-err, Outline §1-2-6).
- `crates/algocline-engine/src/execution/driver.rs` — introduced `pub(crate) struct DriverContext` to group the six shared `Arc`/token arguments of `driver_loop` (`state`, `bus_tx`, `cancel_token`, `resp_txs`, `last_active`, `metrics`). Field declaration order reproduces the original flat-argument drop order so that `Arc` reference-count sequencing is identical before and after the refactor (Crux: Arc ownership order preserved). `driver_loop` signature narrows from seven arguments to three (`ctx: DriverContext`, `exec_task`, `llm_rx`); the `mut`-owned `exec_task` and `llm_rx` remain flat to avoid borrow-split complexity inside the `select!` loop. All call sites in `driver.rs` updated to `ctx.*` field access (mechanical rename, no logic change).
- `crates/algocline-engine/src/execution/registry.rs` — `spawn_v2()` Arc-clone block condensed into a single `DriverContext { ... }` constructor; `driver_loop` invocation updated to the three-argument form. No behaviour change; pure internal refactor with no public API surface affected (`DriverContext` is `pub(crate)`).

- `crates/algocline-core/src/metrics.rs` — added `ExecutionMetrics::usage_aggregate() -> Option<TokenUsage>`. Returns `None` when `llm_calls == 0` (no LLM call occurred in this run) and `Some(TokenUsage { prompt_tokens, completion_tokens })` otherwise, preserving the `None` vs `Some(zero-valued)` wire-shape invariant. Mutex poison returns `None` with `tracing::warn!`, matching the existing lock-guard convention in `metrics.rs`.
- `crates/algocline-engine/src/session.rs` — `Session::into_driver_parts` now moves `ExecutionMetrics` out as a fourth tuple element instead of dropping it. All v2 callers updated accordingly.
- `crates/algocline-engine/src/execution/registry.rs` — `SessionRegistryV2::spawn_v2` wraps the metrics in `Arc<ExecutionMetrics>` and clones it into both `DriverContext.metrics` and `SessionRecord.metrics`, establishing a single shared counter per session that persists across `alc_continue` calls.
- `crates/algocline-engine/src/execution/driver.rs` — `DriverContext` gained a `pub(crate) metrics: Arc<ExecutionMetrics>` field. The v2 `Done` branch now returns `usage: ctx.metrics.usage_aggregate()` instead of the previous `usage: None` hardcode. Three test-only `DriverContext` constructors updated with `metrics: Arc::new(ExecutionMetrics::new())`.
- `crates/algocline-engine/src/execution/record.rs` — `SessionRecord` gained a `pub(crate) metrics: Arc<ExecutionMetrics>` field to enable `resume` to feed per-response token usage into the same accumulator as the driver loop.
- `crates/algocline-engine/src/execution/driver.rs` — v2 `Paused` branch (`llm_rx.recv()` arm) now calls `ctx.metrics.create_observer().on_paused(&queries)` after building the `Vec<LlmQuery>` from the outgoing requests. This increments `SessionStatus.llm_calls`, which is the gate for `usage_aggregate()` to return `Some(...)` rather than `None`.
- `crates/algocline-engine/src/execution/registry.rs` — `SessionRegistryV2::resume` now threads per-response `usage` from `ResumePayload` through to `record.metrics.create_observer().on_response_fed(...)` for both `Single` and `Batch` variants. The intermediate collection is widened from `Vec<(String, String)>` to `Vec<(String, String, Option<TokenUsage>)>` to carry usage to the observer call.
- `crates/algocline-app/src/service/tests/unit.rs` — added integration test `test_usage_accumulates_across_alc_continue` verifying that usage sums across consecutive `alc_continue` calls within the same run-id rather than resetting.
- `tests/e2e.rs` — added `test_alc_run_usage_populated` end-to-end test asserting that `alc_run` with an `alc.llm()` call returns a non-null `usage` field in the `ExecutionResult`.

### Added

- `.github/workflows/ci.yml` — GitHub Actions CI workflow for all pushes and pull requests. Runs `just ci` (`fmt-check lua-fmt-check clippy test check-invariants check-agent-index`) on `ubuntu-latest` with `dtolnay/rust-toolchain@stable`, `Swatinem/rust-cache@v2` for dependency caching, and stylua 2.3.1 pinned via `cargo install --locked --version 2.3.1 stylua`. On test failure, insta `.snap.new` files are uploaded as a GitHub Actions artifact (`if: failure()`) to aid snapshot regression diagnosis.
- `README.md` — CI status badge linking to the new `ci.yml` workflow.

### Notes

- Patch bump (0.38.3 → 0.38.4). New `W_META_LEGACY_M_VERSION` lint warning is additive (no existing pkg emits an error from it). The `usage_aggregate()` addition on `ExecutionMetrics` is additive `pub fn`. `SessionRegistryV2::spawn_gc_task` is additive on a `pub` struct (`spec/usage` path was already public). The `DriverContext` refactor is `pub(crate)` only — no SemVer-visible surface change. CI workflow + Lua lint integration are repository infrastructure, not consumer-facing API.

## [0.38.3] - 2026-05-23

### Changed

- `plugins/alc/agents/alc-coder.md` — added `### Spec Authoring Conventions` section after Driver Loop step 5, codifying two `mlua-lspec` runtime-shape regularities the static check cannot catch: top-of-file `lust` destructuring, and the `M.spec.entries` stub form for smoke / no-`alc.llm` packages. Includes the `lust-global-assumption` anti-pattern explaining the one guaranteed retry these omissions cause.
- `plugins/alc/skills/alc-wake/SKILL.md` — Post-impl install procedure now branches by `/alc-build --location=` value. Global mode keeps the existing `cp → /tmp → git init → alc_pkg_install` synthetic-single-pkg path. Collection mode (new) covers (B-1) collection-local use via the 0.38.2 `alc_pkg_test project_root` fix without any install step, and (B-2) global registration via `alc_pkg_link` (in-tree dev) or `alc_pkg_install url=file://<collection_root>` (published-source semantics). Adds an explicit guard against forking the SoT by `cp`'ing to `/tmp`.

### Notes

- Patch bump (0.38.2 → 0.38.3). Plugin-side documentation (Agent / Skill body) only — no Rust source changes. Required because `plugins/alc/` ships as part of the Claude Code Plugin Marketplace catalog; the marketplace consumer view of this repo needs the doc updates surfaced at a tagged release.

## [0.38.2] - 2026-05-23

### Fixed

- `alc_pkg_test` `pkg=<name>` mode now honors the `project_root` argument for package discovery. Previously the parameter only fed `auto_search_paths` (the `require()` resolution side) and was ignored when locating `<pkg>/init.lua` itself, so packages living in a user collection repo (`/alc-build --location=collection` layout: any repo with `alc.toml` at its root holding flat `<root>/<pkg>/` directories) could not be tested via `pkg=<name>` and required the `code_file=<absolute spec> search_paths=[<root>]` workaround. The new search order is `<project_root>/<name>/init.lua` → cwd-ancestor `alc.local.toml` (variant) → `~/.algocline/packages/<name>/init.lua` (global); existing global / variant paths are preserved unchanged. (Surfaced by the alc-coder dispatch verifying the `--location=collection` mechanism in `plugins/alc/`.)

### Notes

- Patch bump (0.38.1 → 0.38.2). Bug fix only; no API additions or removals. The `pkg_resolve_init_path` signature added an optional `project_root` parameter — this is a `pub(crate)` function so it is not a SemVer-visible change for crates.io consumers.

## [0.38.1] - 2026-05-21

### Added

- `plugins/alc/` — Claude Code Plugin Marketplace plugin for algocline package development (adviser / coder / refiner agents + `alc-build` / `alc-wake` skills). Bundled `mcp.json` registers `algocline` and `git-reader` MCP servers automatically when the plugin is enabled. Distributed under MIT OR Apache-2.0 (matches the crate license).
- `.claude-plugin/marketplace.json` — Marketplace catalog so this repository can be registered as a Claude Code Plugin Marketplace source. Users can install bundled plugins with `/plugin marketplace add ynishi/algocline` followed by `/plugin install alc@algocline`.

### Changed

- README MCP register example simplified to the minimum form (`{"command": "alc"}`), with a one-line note that `env` is only needed when one of the documented variables is set.
- Bundled `algocline` entry in `plugins/alc/mcp.json` aligned with the same minimum form.

### Docs

- Documented the `ALC_CARD_SINKS` environment variable in README (Card subscriber backends as a `|`-separated URI list; v1 only accepts `file:///absolute/path`; duplicate URIs are first-wins; malformed entries are logged and skipped). The variable has been implemented and tested in `algocline-engine` for a while but was missing from the public docs.
- Added a Quick-start subsection in README that explains how to install algocline as a Claude Code plugin (`/plugin marketplace add ynishi/algocline` followed by `/plugin install alc@algocline`), so users discover the bundled plugin path without diving into `plugins/alc/`.

### Notes

- Patch bump (0.38.0 -> 0.38.1). The only crates.io-scoped change is the README docs addition for `ALC_CARD_SINKS`; binary / library APIs are unchanged. The `plugins/alc/` and `.claude-plugin/` additions are distributed via the Claude Code Plugin Marketplace and are not part of the crates.io publish set.

## [0.38.0] - 2026-05-21

### Changed

- Bumped bundled `algocline-swarm-frame` from `v0.3.0` to `v0.5.0` in `BUNDLED_SOURCES` (`src/init.rs`). Adds `swarm_aggregate_plugin` and Rich Verdict 2-layer separation. `alc init` / `alc update` will pull the new tag.
- `alc_eval`: scenario-side `provider` field now takes precedence over the auto-wired algocline provider. Falls back to auto-wire (`ef.providers.algocline { strategy = "..." }`) when scenario does not specify one. Enables 0-cost replay providers (e.g. `ef.providers.recorded { log_path = "~/.algocline/logs/s-<id>.json" }`) and `mock.recording` / `mock.map` via inline scenario without changing the MCP wire shape. The `strategy` argument is still required — borrow any installed package name (e.g. `cot`) when running a pure-replay smoke; the borrowed strategy module is loaded but never invoked because the scenario-side provider supersedes it. Smoke: `alc_eval(strategy="cot", scenario=<inline with provider=ef.providers.recorded {...}>)` returns `response.model = "recorded"`, `stats.auto.llm_calls = 0`. (Lua-level change in `crates/algocline-engine/src/prelude.lua`; no MCP wire change.)

### Added

- `alc_pkg_doctor` gains a tenth bucket `unregistered_pkg`: physical directories under `~/.algocline/packages/` that contain `init.lua` but are not registered in `installed.json`, `alc.toml`, or `alc.local.toml`. The `suggestion` field for this bucket is an `array<string>` (Clippy-style multi-line with install / link / remove / source-note options) rather than the `string` used by all other buckets. `target_filter = Some(name)` for a physically-present-but-unregistered package now routes to this bucket instead of returning an error. Path-dep entries in `alc.toml` / `alc.local.toml` that point inside `packages_dir` are skipped via canonical path comparison to avoid false positives.
- `alc_pkg_test` gains `auto_search_paths` parameter (optional `bool`, default `true`) and two new response fields. When `true` (or omitted), the Lua VM's `package.path` is automatically prepended with directories resolved from all three registry sources: installed packages under `~/.algocline/packages/`, `alc.toml` linked-global path entries, and `alc.local.toml` linked-variant path entries. Auto-resolved directories are placed before any caller-supplied `search_paths` without deduplicating or reordering caller entries. Set `auto_search_paths: false` to suppress auto-resolution entirely (zero auto-resolved paths injected, registry I/O skipped). New response fields: `resolved_search_paths` — array of `{name, search_dir, source}` objects mapping each resolved package to its directory and source (`"installed"` / `"alc.toml"` / `"alc.local.toml"`); always present as an empty array when `auto_search_paths` is false. `search_path_warnings` — array of strings surfacing canonicalization or I/O errors encountered during resolution; present only when non-empty (additive). (breaking for trait implementors only; `EngineApi::pkg_test` gains one parameter)
- `[setting.<target>]` tables in `alc.toml` / `alc.local.toml` / `~/.algocline/config.toml` — schemaless per-target configuration. Target names and field names are arbitrary; algocline core does not validate or enforce any per-target schema.
- MCP tool `alc_setting_resolve(target?)` — returns `{resolved, sources}` with per-field layer attribution (`env > project > global`) in a single call. Omit `target` to receive all configured targets.
- `alc init` now creates `~/.algocline/config.toml` from a commented template when the file is absent. Re-runs are idempotent (existing file is never overwritten).
- `alc info` JSON output gains a `settings` field surfacing the resolved `[setting.*]` tables and per-field source attribution. Parse errors are surfaced as `settings_error` (non-fatal; info remains usable as a doctor tool).

### Notes

- Breaking for `EngineApi` trait implementors only (a new required method `setting_resolve` is added; only `AppService` exists upstream). MCP wire shape is additive.
- env override naming: `ALC_SETTING_<TARGET>_<FIELD>` (uppercase snake). No legacy short aliases (e.g. `ALC_JOURNAL_PATH`) are provided.

## [0.37.0] - 2026-05-17

### Changed: bundled swarm-frame bumped to `v0.3.0`

`BUNDLED_SOURCES` in `src/init.rs` now points `algocline-swarm-frame` at tag `v0.3.0` (was `v0.2.0`). `alc init` and `alc update` will pull the new tag on next invocation.

### Added

- `alc_pkg_test`: Run mlua-lspec tests against a package's spec directory (`<pkg>/spec/*_spec.lua`), a single `code_file`, or inline `code`. Returns JSON `{passed, failed, pending, total, duration_ms, spec_files[]}` with per-file and per-test breakdown. `pkg` / `code_file` / `code` are mutually exclusive — exactly one must be provided. (breaking for trait implementors only; `EngineApi::pkg_test` has no default impl)
- **swarm-frame v0.2.0 as bundled Hub Collection**: Added `https://github.com/ynishi/algocline-swarm-frame` at tag `v0.2.0` as a third bundled source, auto-installed via `alc init` / `alc update`. Contributes 3 swarm orchestration packages (`swarm_frame`, `swarm_frame_algocline`, `swarm_aggregate_plugin`) to the algocline Hub. The BUNDLED_SOURCES entry carries `tag = "v0.2.0"` (pinned release); the AUTO_INSTALL_SOURCES entry tracks the URL without a tag (main-branch resolution). See `docs/pkg-author-conventions.md` for the collection-only install convention.
- `alc.env` stdlib (Phase 1) — host-owned readonly env snapshot exposed to the Lua VM.
  - `alc.env.KEY` returns the env value as a string, or nil if absent.
  - `alc.env:get(key, default)` returns the value or a fallback default.
  - `alc.env:use{"FOO", "BAR"}` declare-at-use proxy: undeclared keys return nil regardless of env contents.
  - Source priority (strict three-layer merge): `inject > dotenv > os.env`. OS env access is opt-in via `ctx.env.allow_os = true`.
  - dotenv file parsing via the `dotenvy` crate (new dependency); path supplied as absolute path in `ctx.env.dotenv`.
  - Optional allowlist filter via `alc.toml [env.allow]`; omitting the section allows all resolved keys.
  - Writes always error: `alc.env.X = 1` raises `"alc.env is readonly"` at the Lua runtime level.
  - Additive: existing `ctx` fields (e.g. `qwen_env`) are unchanged.
- `alc_pkg_doctor`: added two new diagnostic buckets — `missing_meta` (installed pkg with init.lua but no `M.meta.name`) and `missing_hub_index` (Collection-mode project_root with 2+ pkg dirs but missing hub_index.json). JSON output now contains seven top-level arrays (additive; existing five buckets unchanged).
- `alc_pkg_doctor`: added `stale_cache` bucket (8th verdict) — emits one entry per `~/.algocline/hub_cache/{hash}.json` file whose mtime exceeds `CACHE_TTL_SECS` (3600s). Same TTL discipline as `HubCacheLookup::Stale`, surfaced read-only. JSON output now contains eight top-level arrays (additive; existing seven buckets unchanged). `target_filter=Some` skips the entire pass (hub cache files are not per-package).
- `alc_pkg_doctor`: added `spec_missing` bucket (9th verdict) — emits one entry per installed pkg whose `spec/` directory exists but contains zero `*_spec.lua` files. Aligns with `alc_pkg_test`'s spec discovery convention. Narrow scope: pkgs without a `spec/` directory are silently skipped (opt-in). JSON output now contains nine top-level arrays (additive; existing eight buckets unchanged).
- `docs/pkg-author-conventions.md`: added §10 Cards quick reference (`alc_card_find` predicate cheatsheet + AND/OR/NOT examples) and §11 Pre-publish verification workflow (4-step pre-push checklist `alc_pkg_test` → `alc_hub_dist` → `alc_pkg_doctor` → `alc_hub_search local_indices` + failure-mode table).
- `alc.state.list(namespace) -> string[]`: List keys under the dispatched state layout `{state_root}/{namespace}/*.json`. Returns sorted, `.bak`/`.tmp` excluded.
- `alc.state.show(namespace, key) -> table`: Read full JSON content of `{state_root}/{namespace}/{key}.json`. Typed error (message contains `not found`) when the key is absent.
- `alc.state.reset(namespace, key, opts?) -> table`: Atomically mutate the state file. Creates `{key}.json.bak`, removes specified steps from `data.completed_steps` and fields from `data`, writes via tempfile + rename. Returns `{ok, backup_path, steps_removed, fields_removed}`.
- `StateError` enum (`thiserror`-derived) in `algocline-engine::state` with variants `KeyNotFound`, `UnsafeSegment`, `IoBackup`, `IoWrite`, `Serde`, `ShapeInvalid`. Used by the three new `JsonFileStore` inherent methods (`list_dispatched`, `show_dispatched`, `reset_dispatched_with_backup`).

### Notes

- **No MCP tool** is added (no `alc_state_list` / `alc_state_show` / `alc_state_reset`). Callers run these via `mcp__algocline__alc_run` with a short Lua snippet.
- **No `EngineApi` trait method** is added. The implementation lives entirely inside the `algocline-engine` crate as inherent methods on `JsonFileStore`.
- Replaces the rolled-back design from issue `1778990062-86012` (`alc_state_*` MCP tool + `OrchState` types) which leaked application terminology (`orch`) into the shared `algocline-core` / `-engine` / `-app` / `-mcp` crates. The new design keeps all application terms in caller packages (`coding_orch`, `incubator`, etc.).
- The `agent-profiles` repo's `workspace-pipeline.md §orch state.json 編集 SOP` section will be updated separately to point at the Lua snippet path; that change lives outside this repo.

## [0.36.0] - 2026-05-15

### Changed: bundled-packages bumped to `v0.24.0`

`BUNDLED_SOURCES` in `src/init.rs` now points `algocline-bundled-packages`
at tag `v0.24.0` (was `v0.23.0`). `alc init` and the `AUTO_INSTALL_SOURCES`
seed picked up by `pkg_install` will fetch this newer tag.

### Changed — **BREAKING**: `pkg_install` single-package mode removed

`SourceKind::Single` and the staging `init.lua` auto-detection in `pkg_install`
have been removed. Hub publishing now requires `hub_index.json` (1 entry is
sufficient for single-package repos).

**Before** (v0.35.x and earlier):

```
# Single-package repo (init.lua at repo root) auto-detected:
alc_pkg_install({ url: "github.com/user/my-pkg" })
# → installed as ~/.algocline/packages/my-pkg/
# Response: { mode: "single", installed: ["my-pkg"] }
```

**After** (v0.36.0+):

```
# Collection-layout repo with hub_index.json:
alc_pkg_install({ url: "github.com/user/my-pkg-collection" })
# → installed as ~/.algocline/packages/<each-subdir>/
# Response: { mode: "collection", installed: [...] }
```

**Migrate (1-pkg authors)**:

1. Move `init.lua` from repo root to `<repo>/<pkg_name>/init.lua` (nested layout).
2. Add a minimal `alc.toml` at repo root with a `[hub]` section.
3. Run `alc_hub_dist` from a Claude Code / rmcp MCP session:
   ```
   alc_hub_dist(
     source_dir = ".",
     output_path = "hub_index.json",
     projections = ["hub", "narrative"]
   )
   ```
4. `git add hub_index.json docs/ && git commit && git push`.

See `docs/pkg-author-conventions.md §7` for the full step-by-step guide.

### Changed: evalframe v0.4.0 integrated into Hub as a Collection source

`evalframe` v0.4.0 now ships `hub_index.json` at the repo root and uses the
standard Collection layout (`evalframe/<pkg>/init.lua`). It is re-integrated
into `BUNDLED_SOURCES` (tag `v0.4.0`) and `AUTO_INSTALL_SOURCES` so that
`alc init` and `pkg_install` auto-routes pick it up alongside
`algocline-bundled-packages`.

`evalframe` packages are listed as system packages (`SYSTEM_PACKAGES`) and are
therefore excluded from `pkg_list` user-facing output while still being
discoverable via `alc_hub_search`.

### Added: `alc_hub_search` accepts `local_indices` parameter

`alc_hub_search` now accepts an optional `local_indices: Vec<String>` field.
Each path is read as a `hub_index.json` file and its packages are merged into
the search results after the remote-fetched results (same deduplication via
`seen_names`). Parse or I/O failures are surfaced as `warnings` entries in the
response rather than hard errors, so a missing or malformed local file does not
abort the search. This is useful for pre-push verification of a new
`hub_index.json` before committing.

## [0.35.0] - 2026-05-14

### Added

- **`ExecutionService` trait — pure Service-layer execution API** (`algocline-core::execution`).
  A new `ExecutionService` trait with six verbs (`spawn`, `state`, `resume`, `cancel`,
  `observe`, `await_terminal`) provides a wire-concept-free API surface for session
  lifecycle management. All types live under `algocline-core::execution` with zero
  dependency on `rmcp::*`, `progressToken`, `_meta`, or any other MCP wire concept,
  enforcing invariant 1 (transport-independent domain layer) at the type-system level.
  The trait coexists with the legacy `EngineApi` trait; `AppService` implements both.

- **`SessionRegistryV2` + cooperative cancellation (4-checkpoint driver loop)**
  (`algocline-engine::execution`). A new `SessionRecord` struct bundles a per-session
  `broadcast::Sender<ProgressEvent>` (capacity 256), a `CancellationToken`, and a
  `JoinHandle` for the background driver task. `SessionRegistryV2` manages a
  `HashMap<SessionId, Arc<SessionRecord>>` with a clone-then-release lock pattern to
  avoid holding the map lock across `.await` points (K-4). The driver loop checks
  cancellation at exactly four checkpoints:

  - **A** — before the Lua chunk begins (`cancel_token.is_cancelled()`)
  - **B** — before publishing a pause notification (`cancel_token.is_cancelled()`)
  - **C** — at the resume entry path (`cancel_token.is_cancelled()`)
  - **D** — long-IO wait via `tokio::select!` racing `cancel_token.cancelled()`
    against `exec_task` / `llm_rx.recv()`

  No `JoinHandle::abort()` or process-kill path is introduced; shutdown is always
  cooperative. Sessions return immediately from `spawn` while the driver runs in the
  background (`tokio::spawn`), resolving debt #1778655047-40955 (blocking
  `wait_event` before `SessionId` return).

- **`ProgressEvent` broadcast bus — sink-free multi-observer fan-out**
  (`algocline-core::execution::progress`). A `tokio::sync::broadcast` channel with
  capacity 256 carries a tagged enum of seven variants (`StateTransition`,
  `LlmCallBegin`, `LlmCallEnd`, `PauseRequested`, `ResumeReceived`,
  `CancelRequested`, `Tick`). The `observe` verb returns a `broadcast::Receiver`
  wrapped in `ObserverHandle` as a synchronous call — no pre-registered sink is
  required. Zero-observer sessions accept `send()` calls without error (broadcast
  standard behaviour). Slow observers receive `RecvError::Lagged(n)` rather than
  silent drops, preserving loss observability. Multiple independent subscribers each
  receive the full event stream without affecting one another.

- **`alc_v2_run`, `alc_v2_state`, `alc_v2_resume`, `alc_v2_cancel` MCP tools**
  (`algocline-mcp`). Four new MCP tools that expose the `ExecutionService` API over
  the MCP wire with zero wire-concept leakage into the Service layer. All four tools
  coexist with the legacy `alc_run` / `alc_continue` path under a `_v2` prefix;
  neither the wire shape nor the behaviour of existing tools is altered.

  - **`alc_v2_run`** — spawns a new execution session via `ExecutionService::spawn`.
    Accepts `code` (Lua source), optional `session_id` (caller-supplied idempotency
    key), and optional `ctx` (JSON context). When the caller's MCP request carries
    `_meta.progressToken`, a `ProgressForwarder` task is spawned to stream
    `ProgressEvent` notifications back to the caller via
    `Peer::notify_progress`; if no token is present no forwarder is spawned and no
    progress notification is emitted for that request (Crux invariant 2).
    Returns `{ session_id, status, result?, error? }`.

  - **`alc_v2_state`** — returns the current `ExecutionStateV2` snapshot for a
    session (`ExecutionService::state`). Read-only; annotated
    `read_only_hint = true, idempotent_hint = true`.

  - **`alc_v2_resume`** — resumes a paused session by injecting a `ResumePayload`
    (`Single { response, usage?, query_id }` or `Batch(Vec<QueryResponse>)`) via
    `ExecutionService::resume`. Returns the updated session state.

  - **`alc_v2_cancel`** — cancels a session via `ExecutionService::cancel` with
    `CancelCode::User`. Annotated `idempotent_hint = true`; cancelling a session
    that is already at a terminal state is a no-op.

- **`ReqIdRegistry` — adapter-owned request-id ↔ session-id mapping**
  (`algocline-mcp::req_registry`). A new `ReqIdRegistry` struct wraps
  `Arc<RwLock<HashMap<RequestId, SessionId>>>` and provides `insert` / `lookup` /
  `remove_by_session` / `remove_by_request` with a clone-then-release lock pattern
  that never holds the lock across an `.await` point (K-4). The registry is owned
  exclusively by the MCP adapter crate; no wire type (`RequestId`, `ProgressToken`,
  `_meta.*`) crosses the crate boundary into `algocline-core` or `algocline-app`
  (Crux invariant 1). Entry lifetime: inserted on `alc_v2_run` success, removed
  unconditionally by a background `await_terminal` task on session completion, and
  also removed on `alc_v2_cancel` success.

- **`ProgressForwarder` — per-token progress bridge**
  (`algocline-mcp::progress_forwarder`). A free function
  `spawn_progress_forwarder(execution, peer, sid, token)` that calls
  `ExecutionService::observe` to obtain a `broadcast::Receiver<ProgressEvent>` and
  then forwards each event to the MCP caller via `peer.notify_progress`. Slow-reader
  `RecvError::Lagged(n)` is forwarded as a synthetic `{"kind":"lagged","n":n}`
  notification so the caller can detect message loss without silently dropping events.
  `RecvError::Closed` and a `notify_progress` send error both cause the task to exit
  cleanly without panic. Spawned if and only if `_meta.progressToken` is `Some`
  (Crux invariant 2).

- **`ServerHandler::on_cancelled` override — cooperative cancellation from MCP**
  (`algocline-mcp::AlcService`). Overrides the default empty `on_cancelled`
  implementation. When the MCP client sends a `notifications/cancelled` message, the
  handler resolves `request_id → SessionId` via `ReqIdRegistry::lookup` and then
  calls `ExecutionService::cancel` with `CancelCode::User` (Crux invariant 3). A
  lookup miss (mapping already removed or request never registered) is logged at
  `DEBUG` and treated as a no-op. No `JoinHandle::abort()` or direct channel-close
  path is introduced; cancellation is always routed through `ExecutionService`.

- **`impl ExecutionService for AppService`**
  (`algocline-app::service::execution_service_impl`). Wires the six trait verbs to
  the new `SessionRegistryV2`. The legacy `AppService::run` / `EngineApi` path is
  untouched; the new `Arc<SessionRegistryV2>` field added to `AppService` is additive.

- **Value types for execution module** (`algocline-core::execution`).
  Approximately 20 public types exported from `algocline-core`: `SessionId`,
  `SessionSpec`, `ExecutionStateV2`, `ExecutionStateTag`, `PauseInfo`, `ResumePayload`,
  `ResumeOutcome`, `CancelReason`, `CancelCode`, `CancelInfo`, `ExecutionResult`,
  `FailureInfo`, `TerminalOutcome`, `ProgressEvent`, `ObserverHandle`, and seven closed
  error enums (`SpawnError`, `StateError`, `ResumeError`, `CancelError`, `ObserveError`,
  `AwaitError`, `ObserverRecvError`). All types implement `serde::Serialize +
  serde::Deserialize`. Error enums use `thiserror::Error` with `#[from]` converters for
  upstream error types. No `#[non_exhaustive]` (wire boundary adapter is responsible
  for string conversion before crossing the MCP boundary).

### Changed

- **Bump `rmcp` 1.5 → 1.7** across the workspace (`Cargo.toml`,
  `crates/algocline-mcp/Cargo.toml`). rmcp 1.7.0 ships
  [rust-sdk#843](https://github.com/modelcontextprotocol/rust-sdk/pull/843)
  which flattens `PromptMessageContent::Resource` so embedded resource
  serialization matches the MCP spec (`{ "type": "resource", "resource":
  { uri, mimeType, text } }` instead of the previous doubly-nested
  `content.resource.resource.{uri,...}` shape). This unblocks Phase
  1.x-D embedded resource support in `prompts/get` responses (no Phase
  1.x-D wire change in this release; the bump only lifts the upstream
  technical blocker). No source changes required in algocline — the
  current MCP server compiles against 1.7 unchanged.

- **`await_terminal` no longer busy-polls — JoinHandle take-and-await**
  (`algocline-engine::execution`). The previous implementation looped
  `state.lock().await → match → tokio::task::yield_now().await`
  indefinitely until the state transitioned to a terminal variant.
  `yield_now()` consumes no CPU but keeps the task in the scheduler's
  ready queue, so N parallel `await_terminal` callers occupy N worker
  slots that legitimate work cannot use. `SessionRecord.join_handle`
  is now `Mutex<Option<JoinHandle<()>>>` and `await_terminal` takes the
  handle, `.await`-s it directly, then reads the (now-terminal) state
  once via the existing mutex. Single-awaiter discipline is documented;
  a second concurrent caller that observes `None` falls through to a
  direct state read, returning `AwaitError::Joined("...concurrent
  awaiter race...")` only in the rare race where the state has not yet
  been written. Current production usage is single-caller, so the race
  branch is purely defensive. No API change. Regression coverage:
  `await_terminal_returns_done_for_trivial_script`,
  `await_terminal_does_not_panic_on_second_concurrent_caller`.

- **Remove `SessionRecord._sentinel_rx` (sink-free invariant clarified)**
  (`algocline-engine::execution::record`). The sentinel
  `broadcast::Receiver<ProgressEvent>` was held only to ensure
  `bus_tx.send(...)` returned `Ok` when 0 user observers were subscribed,
  but every production call site already uses `let _ = bus_tx.send(...)`
  to absorb the result. The sentinel only added cost — a 256-deep buffer
  per session with `ProgressEvent` clones cycling under `Lagged` eviction
  — without protecting anything that wasn't already protected by the
  `let _ = ...` pattern. The Crux R3 (sink-free fan-out) doc comment in
  `record.rs` is reworded to reflect the actual invariant: "caller is not
  crashed by 0 observers", not "`send` always returns `Ok`". The matching
  unit test is renamed `bus_tx_send_succeeds_with_zero_observers` →
  `bus_tx_does_not_crash_caller_with_zero_observers` with the
  `assert!(result.is_ok())` removed (the new contract is panic-freedom).
  No API change at `ExecutionService::observe`; existing observers
  continue to receive the full event stream with the same late-subscribe
  semantics.

- **`lagged_emits_wrapper_event` test flush is now deterministic**
  (`algocline-mcp::progress_forwarder`). Removed the 50ms wall-clock
  `tokio::time::sleep` that preceded the duplex-transport drop. The
  duplex pipe buffer preserves bytes the server side has already
  written past the server-side drop, so `drop(running) → read_to_end`
  on the client side drains them without a timing-dependent delay.
  Bumps the `read_to_end` timeout 300ms → 500ms so the overall
  wall-clock upper bound is unchanged on slow CI runners. Test-only
  change; no production code touched.

### Fixed

- **MCP `alc_run.ctx` / `alc_advice.opts` / `alc_eval.strategy_opts`
  schema now declares `type: object`** instead of the unconstrained
  schemars default for `serde_json::Value`. Some MCP clients infer an
  unconstrained field as a free-form string and JSON-stringify it
  before sending, which previously caused the Lua-side `ctx` global
  to arrive as `type(ctx) == "string"` and broke pkgs that required
  a table (`swarm_frame.normalize_ctx: ctx must be a table (got
  string)`). With the schema constrained to object, conforming
  clients send the value as a real JSON object and the existing
  `lua.to_value` path materialises a Lua table as documented. Tracked
  in `tests/e2e.rs::test_alc_run_ctx_schema_is_object`. No runtime
  type change — Rust deserialisation still accepts `serde_json::Value`.

- **Defensive server-side normalisation for non-conforming clients.**
  When `alc_run.ctx` / `alc_advice.opts` / `alc_eval.strategy_opts`
  arrive as a `Value::String` whose body parses as a JSON object or
  array, the server reparses the payload before injecting into the
  Lua VM (`AppService::run` / `::advice` / `::eval`). Conforming
  clients (which already send a real object) are unaffected; legacy
  callers that still stringify the field — including the existing
  `agent-profiles` Skills that defensively called
  `alc.json_decode(ctx)` to recover from the bug — keep working
  without code changes. Regression coverage:
  `tests/e2e.rs::test_alc_run_ctx_stringified_json_normalized_to_table`.

- **Prevent `state_before` nesting in `CancelInfo` on `Paused → Cancelled`**
  (`algocline-engine::execution::driver`). When a session was cancelled
  while in the `Paused` state, two `Cancelled` transitions could fire in
  sequence — first from `SessionRegistryV2::cancel` (which transitions
  immediately so a paused driver does not hang waiting on a resume),
  then from `driver_loop` checkpoint D once `cancel_token` fires. Both
  paths called `build_cancel_info` independently and the second call
  read the now-`Cancelled` state, producing
  `Cancelled(state_before=Cancelled(state_before=Paused))` instead of
  `Cancelled(state_before=Paused)`. The fix is two cooperating changes:
  (1) `transition_state` is now idempotent on terminal → terminal
  transitions (first transition wins — state is not overwritten and no
  `StateTransition` event is emitted), and (2) `build_cancel_info` is
  nested-aware (when the current state is already `Cancelled`, the new
  `state_before` inherits the inner `state_before` rather than wrapping
  the outer `Cancelled`). Either change alone breaks the regression;
  both together also future-proof against new cancel paths that may add
  similar double-transition scenarios. Regression coverage:
  `transition_state_terminal_is_idempotent`,
  `build_cancel_info_does_not_nest_on_already_cancelled_state`.
  Verified on the wire via `alc_v2_run` paused → `alc_v2_cancel` →
  `alc_v2_state` — `state_before` is now the original `Paused` snapshot,
  not a nested `Cancelled` wrapper.

## [0.34.0] - 2026-05-10

### Changed

- **BUNDLED_VERSION bump: `algocline-bundled-packages` v0.22.1 → v0.23.0**
  (`src/init.rs` `BUNDLED_SOURCES`). Picks up the bundled
  `card_analysis` package release — the analyzer pkg dispatched by
  default from the new `alc_card_analyze` MCP tool. Bundled v0.23.0
  promotes the POC analyzer (previously available only at
  `~/.algocline/packages/card_analysis/`) into the bundled fleet
  with `M.spec` (input + result shapes locked to the host
  `CardAnalyzeResult` typed contract), `S.instrument(M, "run")`
  wrapper for runtime shape validation, and `tests/test_card_analysis.lua`
  (5 cases / 6 tests covering happy path / input validation / empty
  samples sentinel / 4 failure heuristic paths + no-signal fallback /
  LLM unparseable fallback). `evalframe` source remains pinned at
  v0.3.0. Bundled release notes: `algocline-bundled-packages` v0.23.0
  CHANGELOG / README.

### Added

- **`alc_card_analyze` MCP tool — Card auto-analyzer**.
  Loads a Card body and its samples sidecar host-side and dispatches
  them to a Lua analyzer pkg via `require(pkg).run(ctx)` (default
  `pkg = "card_analysis"`). Sister tool to `alc_advice`: `alc_advice`
  runs a generic strategy over a free-form task; `alc_card_analyze`
  runs an analyzer over a Card. The Card schema (Tier 1 body + Tier 2
  `samples.jsonl` sidecar) is owned by the host so analyzer pkgs only
  deal with prompt construction + `alc.llm` + hint shaping.

  ctx shape passed to the pkg's `M.run(ctx)` — enumerated in the MCP
  tool description so pkg authors do not need to read Rust source:

  ```jsonc
  {
    "card_id": "<id>",
    "card":    <full Card body, same shape as alc_card_get>,
    "samples": [<sidecar rows, same shape as alc_card_samples>]
  }
  ```

  `DEFAULT_CARD_ANALYZE_PKG = "card_analysis"` (constant in
  `algocline-app/src/service/card.rs`) is an **IF promise**, not a
  bundled hard dependency: any installed pkg of that name (bundled,
  project-variant, or user-installed) satisfies the dispatch. The
  analyzer pkg is currently expected to live at
  `~/.algocline/packages/card_analysis/`; bundled-packages migration
  and `alc_shapes` spec are tracked as follow-ups.

  Internally `card_analyze` reuses the existing `AppService::advice`
  execution path, so auto-install bundled fallback /
  `start_and_tick` / response warning splicing all work uniformly
  with strategy dispatch.

  **Typed output contract**: the host deserializes the pkg's
  `ctx.result` through `CardAnalyzeResult` before constructing the
  MCP response. Required fields: `pattern: String`,
  `suggested_change: String`, `confidence: f64`. Optional:
  `failure_count: u64`, `sample_count: u64`. Pkg output that fails
  to deserialize returns a typed error instead of passing freeform
  JSON to the caller. The response shape is flat —
  `{ status, result: { pattern, suggested_change, confidence, ... }, stats }`
  — not double-nested.

  Trait surface: `EngineApi::card_analyze(card_id, pkg)` is added
  with a default-impl returning a clean error
  ("not implemented by this EngineApi impl"), so external trait
  implementors are not broken (breaking for trait implementors only;
  MCP wire shape is purely additive). Issue #1778367957-70837.

## [0.33.1] - 2026-05-09

### Changed

- **BUNDLED_VERSION bump: `algocline-bundled-packages` v0.21.0 → v0.22.1**
  (`src/init.rs` `BUNDLED_SOURCES`). Picks up the bundled-packages
  doc-convention work (V1 SSoT / Projection rule, §4.1 PkgName +
  StyledName 1-line summary form, narrative SSoT decommission of
  `M.docs.narrative`, bundled adoption guide). `evalframe` source
  remains pinned at v0.3.0.

- **`docs/pkg-author-conventions.md` §4.1 — 1-line summary casing
  convention added**. PkgName-only form
  (`{PkgName} — {verb phrase}`) is retained when the pkg directory
  name is the canonical form; new PkgName + StyledName form
  (`{PkgName}({StyledName}) — {verb phrase}`) covers pkgs with a
  paper-cited abbreviation (CoT, UCB, MCTS, etc.). Provides a reading
  aid without breaking directory / SSoT consistency. The §4.5 LuaCATS
  example is updated to `cot(CoT) — ...` to demonstrate the form.

- **`docs/pkg-author-conventions.md` upgraded to V1** —
  single-source-of-truth expansion. New §0 (SSoT / Projection global
  rule) and §1 (Publish patterns and lint scope) introduce the
  Source-vs-Projection discipline and the Bundled / Community /
  Private publish-mode taxonomy, together with the
  `Required` / `Recommended` / `Optional` field status legend. The
  existing top-level pkg shape, docstring narrative, and docstring
  style sections are renumbered (§2-§4); §5 Lint rules table is
  expanded to all 11 active codes plus 2 planned V1 codes
  (`E_META_MISSING_INPUT_SHAPE`, `E_PARAM_MISSING_DESCRIBE`); §6
  Migration is split into §6.1 (legacy `M.docs.narrative` removal,
  retained from V0) and §6.2 (V1 conventions adoption). Parameter
  SSoT location is corrected from the stale `M.meta.input_shape`
  phrasing to the canonical `M.spec.entries.{entry}.input` (matches
  `extract.lua` and the `cot/init.lua` reference). All
  cross-references to `algocline-bundled-packages/docs/docstring-convention.md`
  removed; this convention doc is now the canonical specification
  (the bundled-side doc is scheduled for removal in a separate
  change). Internal issue IDs dropped from public-facing prose. The
  2 planned V1 lint rules are spec-only at this revision; their
  implementation is a follow-up change. No code changes — pure spec
  document update.

### Added

- **Bundled-packages adoption guide for the M.docs SSOT migration**
  (`docs/bundled-packages-adoption-guide.md`). Operator-facing
  playbook for migrating an entire bundled-packages repo (currently
  117 pkgs at tag v0.21.0) onto the `M.docs` spec key shipped in
  v0.33.0. Complements `docs/pkg-author-conventions.md` (the
  per-pkg authoring spec): the convention doc tells you *what* to
  write, this guide tells you *how to migrate an existing fleet*.
  Covers field-by-field reference for `M.docs.{narrative,
  schema_version}`, the B-1 / B-2 / B-3 sub-step strategy
  (mechanical declaration insert / Diátaxis section restructuring
  / docstring discipline), the `alc_pkg_doctor narrative_issues`
  bucket as an adoption progress meter, edge cases (custom
  narrative paths, partial adoption, `alc init --force` overwrite
  caveat, future schema_version expansion), and a 9-row
  cross-reference table back into algocline-core / algocline-app /
  algocline-mcp source paths so bundled maintainers do not need to
  trace the impl themselves. Backs the off-repo follow-up issue
  #1778197753.

## [0.33.0] - 2026-05-08

### Added

- **`alc_pkg_doctor` narrative_issues bucket — M.docs SSOT lint (L-1)**
  (`crates/algocline-app/src/service/pkg/doctor.rs`,
  `tests/e2e.rs`, #1778197805 L-1). New `narrative_issues` top-level
  bucket on the `alc_pkg_doctor` JSON output. Two `kind`/`severity`
  classifications:

  - `declared_missing` (severity `warn`): the pkg declares
    `M.docs.narrative = "<path>"` but the file is absent. Surfaces
    typos and forgot-to-install-narrative cases.
  - `unmigrated` (severity `info`): the pkg has no `M.docs`
    declaration but a convention `narrative.md` exists — the
    bundled adoption signal for #1778197753 (machine-readable
    progress tracking of the SSOT migration).

  Pkgs whose `M.docs.narrative` resolves to an existing file, and
  pkgs with neither declaration nor convention narrative, stay
  silent (clean state). The pass piggy-backs on
  `EngineApi::pkg_resolve_narrative_path` (introduced in #1778112139)
  so the resolution semantics are identical to the
  `alc://packages/{name}/narrative` resource consumer.

  L-2 (Diátaxis section heading lint) and L-3 (docstring 1-line
  summary lint) remain out of this release; they require markdown
  and LuaCATS parser dependencies respectively and will be
  evaluated after bundled adoption (#1778197753) signals demand.

- **`M.docs` spec key — narrative SSOT mechanism for pkg authors**
  (`crates/algocline-app/src/service/lua/gendoc/docs/{entity_schemas,pkg_info,extract}.lua`,
  `crates/algocline-app/src/service/pkg/read.rs`,
  `crates/algocline-mcp/src/resources.rs`,
  `crates/algocline-core/src/engine_api.rs`,
  `docs/pkg-author-conventions.md`,
  #1778112139). Pkg authors can now declare narrative location and
  metadata via a top-level `M.docs` table:

  ```lua
  M.docs = {
      narrative      = "narrative.md",  -- pkg-dir-relative path
      schema_version = 1,
  }
  ```

  - `EngineApi::pkg_resolve_narrative_path(name)` returns the
    declared path (or `None` for convention fallback). `..` and
    leading `/` are rejected as a path-traversal guard.
  - `alc://packages/{name}/narrative` (#1777052474) consults
    `M.docs.narrative` first; falls back to the convention path
    `<pkg>/narrative.md` for pkgs without `M.docs`.
  - gendoc `extract.lua` populates `PkgInfo.docs` from `M.docs`,
    making the SSOT visible to downstream lint / projection paths.
  - `docs/pkg-author-conventions.md` codifies the full author
    surface: `M.meta` / `M.spec` / `M.docs` / `M.run` layering,
    Diátaxis section structure for `narrative.md`, and rustdoc-flavoured
    docstring 1-line summary discipline (arXiv 2510.26130 evidence).

  Backward compatible — `M.docs` is optional, all existing pkgs
  continue to work via convention fallback. bundled-packages
  117-pkg adoption is tracked in a separate issue (#1778197753),
  and lint enforcement in `alc_pkg_doctor` is tracked in
  #1778197805.

- **`alc://packages/{name}/narrative` MCP resource — bundled-pkg
  Stdpkg reference** (`src/init.rs`,
  `crates/algocline-mcp/src/resources.rs`,
  #1777052474). New resource template that exposes per-pkg narrative
  Markdown for bundled packages. `alc init` /
  `pkg_install AUTO_INSTALL_SOURCES` now copies
  `<source>/docs/narrative/{name}.md` to
  `~/.algocline/packages/{name}/narrative.md` (silent skip when the
  source has no narrative for that pkg). `ResourceCatalog` reads
  the file directly as `text/markdown`; missing narrative returns
  -32002 ResourceNotFound.

  **Scope (re-framed from V2 cluster)**: bundled-only Stdpkg
  reference. Personal-package gendoc-output narrative is
  intentionally not surfaced via Resources — schema completion is
  already covered by `alc_pkgs.d.lua` (#1777032565), and external
  narrative indexing belongs to services like Context7. The
  companion `list_changed` notification issue (#1777052497) was
  closed at the same time because the dynamic-pkg mutation surface
  it depended on is now out of Resource scope.

- **`alc_session_new` — optional MCP-connection session pin**
  (`crates/algocline-app/src/service/session.rs` (new),
  `crates/algocline-app/src/service/mod.rs`,
  `crates/algocline-app/src/service/project.rs`,
  `crates/algocline-mcp/src/service.rs`,
  `crates/algocline-core/src/engine_api.rs`,
  #1776627475). New MCP tool that pins a `project_root` (and
  optional `mode` ∈ `"default"` | `"test"`) for the current MCP
  connection. Subsequent tool calls without an explicit
  `project_root` resolve via P > **S** > E > W (per-call > session
  pin > `ALC_PROJECT_ROOT` > cwd ancestor walk). Activation is
  optional — when no session is activated, behaviour matches
  the legacy 3-layer chain (P > E > W) exactly.

  **Why**: AI agents working across multiple worktrees were
  forgetting per-call `project_root` arguments, causing variant-
  scope `pkg_install` calls to leak into the global manifest. The
  pin makes the project context implicit at the connection level
  while preserving per-call override semantics for debug / probe
  flows.

  **Lifetime**: per-MCP-connection. For stdio MCP transport (the
  only transport algocline currently supports), one process serves
  one connection, so the pin lives on `AppService` and drops with
  the process. No explicit `alc_session_end` — closing the MCP
  connection (or activating again) replaces the pin.

  **Modes**: `"default"` matches legacy resolution. `"test"` is a
  hint for downstream tools to apply stricter isolation; the
  current release records the mode but does not yet branch on it
  (consumers like scenario test runners can opt in incrementally).

  Backward compatible — every existing tool continues to accept an
  explicit `project_root` argument, and the env var / cwd walk
  fallbacks are preserved.



## [0.32.0] - 2026-05-07

### Fixed

- **Pool dispatch / client lib tests pass on systems lacking `/bin/true`
  and `/bin/false`** (`crates/algocline-app/src/pool/dispatch.rs`,
  `crates/algocline-app/src/pool/client.rs`, #1778084517). The two
  spawn-tests now invoke `Command::new("true"|"false")` so tokio's PATH
  lookup resolves to `/usr/bin/...` on the macOS sandbox layouts where
  `/bin/{true,false}` is absent. The handshake-timeout test held its
  server-side `Inner` across the sleep so async-move actually captures
  the writer/reader; previously the server socket dropped immediately
  and the client received `ResponseParse(EOF)` rather than the expected
  `Handshake` timeout.

### Added

- **`alc_status` list path now merges pool sessions**
  (`crates/algocline-app/src/service/status.rs`,
  `tests/e2e_pool.rs`, #1778084339). Calling `alc_status` without a
  `session_id` previously enumerated only `SessionRegistry` snapshots;
  `host_mode=true` sessions (which live in `pool_registry`) were
  invisible. The list path now fetches `pool_registry.read().await`
  live entries and merges them into the returned `sessions` array
  with the same shape used by the single-session fallback (marked
  `pool: true`). `SessionRegistry` takes precedence on sid collision
  (defensive — host_mode design avoids collisions). The compact
  `{"active_sessions":0,"sessions":[]}` wire format is preserved when
  both registries are empty (snapshot regression guard).

- **`alc_status include_history=true` fetches pool conversation_history
  via IPC** (`crates/algocline-app/src/pool/protocol.rs`,
  `src/pool_worker.rs`, `crates/algocline-app/src/service/status.rs`,
  #1778084344). The pool wire protocol's `Status` request was extended
  from a unit variant to a struct with `include_history: bool` (with
  `#[serde(default)]` for legacy `{"op":"status"}` wire compatibility);
  the response carries an optional `conversation_history` field
  (`#[serde(default, skip_serializing_if = "Option::is_none")]`). When a
  caller queries `alc_status` for a paused pool session with
  `include_history: true`, the service performs a one-shot UDS
  round-trip to the worker and injects the active session's
  `conversation_history` into the response. IPC failures surface as an
  additive `history_warning` field per the Service-layer
  Error-propagation rule rather than dropping the status reply itself.
  The list path is intentionally not extended (would require N IPC
  round-trips); that is tracked separately if needed.

### Changed

- **`alc.state` per-key file dispatch — `default.json` SSoT relief**
  (`crates/algocline-engine/src/state.rs`, #1776868812). The Rust
  `JsonFileStore` backend now writes keys shaped `{prefix}:{id}` (with
  safe segments — ASCII alphanumerics + `_-.`) to per-key files at
  `{root}/{prefix}/{id}.json` instead of bundling them into the legacy
  single `{ns}.json` (typically `default.json`). The shape matches what
  `flow.state.lua` (algocline-bundled-packages) already produces — keys
  like `flow_orch:abc-123` — so no Lua-side change is required.

  Read-side falls back to the legacy `{ns}.json` for entries written
  before this change, and `incr` / `set_nx` honour both files so
  semantics stay consistent across the dispatch boundary. Existing
  `default.json` is **not migrated** — it remains a legacy reader
  fallback that gradually shadows out as keys are re-set into
  dispatched files (per-task new sessions get the per-file layout
  from the first write).

  Keys without `:` (or with unsafe characters in either segment)
  bypass dispatch and continue using the legacy single-file path.

### Added

- **`alc_hub_dist projections=["luacats"]` — per-pkg IF types projection
  (`types/alc_pkgs.d.lua`)** (#1777032565). The existing luacats branch
  emitted only `alc_shapes` *registered* shapes
  (`types/alc_shapes.d.lua`); each pkg's `M.spec.entries.run.{input, result}`
  was invisible to LuaLS / LuaCATS consumers, so `alc.run("cot", ctx)`
  did not get ctx / return-value completion. The projection now also
  emits `types/alc_pkgs.d.lua` containing one entry per pkg shape:

  - inline shapes (`T.shape({...})`) become `---@class AlcPkgInput_<pkg>`
    / `---@class AlcPkgResult_<pkg>` blocks with one `---@field` per
    schema entry.
  - ref shapes (`T.ref("voted")`) become `---@alias AlcPkgResult_<pkg>
    AlcResultVoted` (resolves into the existing alc_shapes class).

  `gen_docs.lua` writes both `types/alc_shapes.d.lua` and
  `types/alc_pkgs.d.lua` in a single `--luacats` pass. Output is
  deterministic (pkgs sorted by name) and Issue A
  (`#1777032506`, embed + distribute alc_shapes.d.lua) remains the
  prerequisite — no schema or distribution change to alc_shapes.d.lua.

  New helpers in `alc_shapes.luacats`:
  `M.gen_pkgs(pkg_specs)` (top-level driver) and `M.alias_for(class_name,
  ref_schema, class_prefix)` (per-ref alias renderer).

- **`alc.stats.llm_calls()` — Lua-side access to the auto-counted session
  LLM call total** (`crates/algocline-engine/src/bridge/data.rs`,
  `crates/algocline-core/src/metrics.rs`). The `SessionStatus.llm_calls`
  counter has been incremented automatically on every paused-cycle
  complete since 0.29.0 (alc_status v2), but Lua scripts could only
  observe it via an external `alc_status` MCP round-trip. Recipes /
  ingredients can now read it directly:

  ```lua
  local before = alc.stats.llm_calls()
  -- ... do work that may invoke alc.llm() on multiple branches ...
  local count = alc.stats.llm_calls() - before
  ```

  This obviates the `total_llm_calls = total_llm_calls + 1` pattern
  manually maintained across recipe/ingredient packages (52 occurrences
  across 7 files in algocline-bundled-packages at the time of this
  change). New `StatsHandle` (`pub use algocline_core::StatsHandle`)
  exposes a read-only handle on `Arc<Mutex<SessionStatus>>`; mirrors the
  `BudgetHandle` pattern.

  Migration of bundled recipes (`sc`, `recipe_safe_panel`,
  `recipe_deep_panel`, `orch_adaptive`, `orch_gatephase`, `orch_escalate`,
  `particle_infer`) to the new API is tracked in a separate issue and
  ships in algocline-bundled-packages.

## [0.31.2] - 2026-05-07

### Fixed

- **Test isolation: `AppConfig::default()` no longer pollutes `cwd/.algocline/`**
  (`crates/algocline-app/src/service/config.rs`,
  `crates/algocline-app/src/service/transcript.rs`). The `#[cfg(test)]`
  `Default` impl previously returned a relative `app_dir = ".algocline"`,
  which caused `AppService::new()` startup GC to write
  `crates/algocline-app/.algocline/state/pool/registry.json` whenever
  `cargo test -p algocline-app --lib` was run from the crate dir. The
  default now leaks a fresh `tempfile::tempdir()` per call, so every
  `..Default::default()` callsite (22+ in unit/transcript tests) is
  automatically rooted at an isolated tempdir. `transcript::tests::config_with_log_dir`
  also dropped its hand-rolled `AppDir::new(PathBuf::from(".algocline"))`
  in favour of `..AppConfig::default()`.

- **`alc_status` — pool sessions are now visible when queried by `session_id`**
  (`crates/algocline-app/src/service/status.rs`). Previously `alc_status`
  consulted only the in-memory `SessionRegistry`; sessions started with
  `alc_run host_mode=true` live exclusively in `pool_registry` (registry.json)
  and were reported as `session 'sid' not found (may have completed)`. The MCP
  wire returned this plain-text error and `e2e_pool::test_pool_paused_session_visible_in_status`
  panicked on JSON parse. `AppService::status()` now falls back to
  `pool_registry` on `SessionRegistry` miss and returns `{status: "needs_response",
  session_id, pool: true, pid, sock, version, created_at}` for live pool entries.
  `include_history` is ignored on the pool path (worker-side IPC fetch is
  out of scope for this fix).

- **`alc init` — Collection packages now install all sub-files** (`src/init.rs`
  `copy_package`). Previously only `init.lua` was copied for Collection-layout
  packages, leaving sibling files (`mc.lua`, `stats.lua`, `sweep.lua`,
  `eval.lua`, `search.lua`, `stop.lua`, etc.) behind and causing `alc_pkg_doctor`
  to report `incomplete_pkg` for every multi-file bundled package (e.g.
  `abm/optimize`). The fix replaces the single-file copy with a full directory
  tree copy (`copy_dir`) matching the pattern already used by the MCP
  `alc_pkg_install` path.

- **`alc_pkg_install` — `force` parameter now propagates end-to-end**
  (`crates/algocline-mcp/src/service.rs`, `crates/algocline-core/src/engine_api.rs`,
  `crates/algocline-app/src/service/pkg/install.rs`). Previously `PkgInstallParams`
  had no `force` field; the MCP wire boundary silently dropped any caller-supplied
  value. `PkgInstallParams.force: Option<bool>` is now an additive field
  (`#[serde(default)]`), wired through `EngineApi::pkg_install` and
  `AppService::pkg_install_typed` to the install path. Passing `force: true` via
  the MCP tool now overwrites an already-installed Collection package instead of
  skipping it. (breaking for trait implementors only; MCP wire shape is additive)

## [0.31.1] - 2026-05-06

### Changed

- **BUNDLED_VERSION: `algocline-bundled-packages` v0.20.0 → v0.21.0**.
  `src/init.rs` `BUNDLED_SOURCES` tag bumped. New packages picked up by
  `alc init` / `alc update`:
  - `slm_mux` (Selection, Pure Computation) — SLM-MUX confidence-based
    per-model selection + complementarity-driven K-subset selection
    (arXiv:2510.05077, ICLR 2026 Poster).
  - `solve_verify_split` (Orchestration, Pure Computation) — compute
    allocator splitting test-time budget between solver and verifier
    (arXiv:2504.01005, COLM 2025).
  - `particle_infer`, `isp_aggregate` — additional inference/aggregation
    primitives.
  - `alc_shapes.M.slm_muxed` result shape registered.
  evalframe stays at v0.3.0. MCP-side `AUTO_INSTALL_SOURCES`
  (`crates/algocline-app/src/service/resolve.rs`) is unchanged (main-branch
  pinning, separate channel from CLI bundled tag).

## [0.31.0] - 2026-05-06

### Added

- **`alc` CLI: `clap` derive + `-h` / `--help` / `-V` / `--version` support**.
  Replaces the previous hand-rolled `args.get(1) == "..."` dispatch in
  `src/main.rs` with a `#[derive(Parser)]` `Cli` struct and a `Commands` enum
  (`Init`, `Update`). Default behaviour (no subcommand) still falls through to
  MCP stdio server mode, so `alc` invoked by an MCP client (e.g. Claude Code)
  is unchanged. Existing `alc init` / `alc update` flags (`--force`, `--dev`,
  …) are forwarded verbatim via `trailing_var_arg = true` + `allow_hyphen_values
  = true`, preserving full backward compatibility. `alc -h` / `alc --help`
  now print the subcommand list and a one-paragraph note that the default
  mode is MCP server (intended to be launched by an MCP client) and that
  ad-hoc shell access to MCP tools is best done through an `agent-block`
  harness that calls the MCP server directly. `alc -V` / `alc --version`
  print `alc <version>`. Three new e2e tests
  (`test_cli_help_short`, `test_cli_help_long`, `test_cli_version`) exercise
  these dry-run code paths without spawning the stdio MCP server.

- **Pool worker POC: long-lived `alc_run` sessions that survive MCP server
  restarts** (`host_mode=true`). When `alc_run` is called with
  `host_mode: true`, the session runs inside a dedicated worker subprocess
  (`1 session = 1 process`, each with its own mlua VM) rather than inside the
  MCP server process. The worker persists independently of the MCP server
  lifecycle so that a `paused` session survives a Claude Code (MCP client)
  restart and can be resumed via `alc_continue` after reconnection. The
  default (`host_mode` absent or `false`) is fully unchanged — all existing
  `alc_run` / `alc_continue` calls continue to run in-MCP with no behaviour
  difference.

  Key components added:

  - **`PoolError` + pool wire protocol** (`crates/algocline-app/src/pool/`):
    `PoolError` (`thiserror` enum covering `Connect`, `RegistryCorrupted`,
    `Spawn`, `Handshake`, `SessionNotFound`, `VersionMismatch`),
    `PoolRequest` / `PoolResponse` serde enums (JSON line IPC), and a
    version-handshake type.

  - **`PoolClient`** (`crates/algocline-app/src/pool/client.rs`): Unix domain
    socket client. Connects to a worker's UDS path, sends length-prefixed JSON
    lines, and receives responses. Cancel-safe design: on cancellation the
    connection is dropped and re-established to avoid partial-line buffer
    corruption.

  - **`--pool-worker` hidden subcommand** (`src/pool_worker.rs` +
    `src/main.rs`): the binary can be spawned as a worker subprocess with
    `alc --pool-worker --sid <sid> --sock <path>`. The worker starts its own
    `tokio` runtime, accepts exactly one IPC client, initialises one mlua VM
    via `AsyncIsle::spawn`, and dispatches `run` / `continue` / `stop` IPC
    messages in a loop. 1 session = 1 worker process; sessions are fully
    isolated.

  - **`PoolRegistry`** (`crates/algocline-app/src/pool/registry.rs`):
    persistent session registry at `~/.algocline/state/pool/registry.json`
    (path overridable via `ALC_POOL_STATE_DIR`). Entries are written atomically
    via `tempfile::NamedTempFile::persist` (`rename(2)`). On `AppService::new`,
    `scan_and_gc` evicts orphaned entries using `libc::kill(pid, 0)` (ESRCH
    detection). An advisory lock (`fs2::FileExt::lock_exclusive`) on
    `~/.algocline/pool/registry.lock` serialises concurrent writers.

  - **`AppService` host_mode dispatch** (`crates/algocline-app/src/service/`):
    when `host_mode=true`, `AppService::run` spawns a worker subprocess via
    `tokio::process::Command::new(current_exe())` with `pre_exec(setsid)` and
    registers the session in `PoolRegistry`. `alc_continue` / `alc_continue_batch`
    auto-detect the routing (pool vs in-MCP) from `PoolRegistry::lookup`.
    Registry I/O is wrapped in `spawn_blocking` to avoid blocking the async
    executor.

  - **Worker idle timeout + SIGTERM graceful shutdown**: workers respect
    `ALC_POOL_IDLE_TIMEOUT` (default `1800` s; `0` = no timeout). After the
    timeout elapses with no IPC activity the worker removes itself from
    `registry.json` and exits. SIGTERM is caught via
    `tokio::signal::unix::signal(SignalKind::terminate())` and triggers the
    same cleanup path.

  - **MCP tools `alc_pool_ensure` / `alc_pool_status` / `alc_pool_stop`**:
    three new MCP tools for pool lifecycle management.
    - `alc_pool_ensure` (`idempotent_hint=true`): ensures a pool worker is
      running for a session, spawning one if absent. Returns `{sid, sock,
      pid, status}`.
    - `alc_pool_status` (`read_only_hint=false, idempotent_hint=true`): queries
      live registry entries; returns the list of active pool sessions with PID /
      socket path / version. (`read_only_hint` corrected from `true` — the
      implementation runs GC + `registry.json` write on every call; see Fixed.)
    - `alc_pool_stop` (`destructive_hint=true`): sends SIGTERM to a named
      session's worker and removes the registry entry. Guards against
      `pid == 0` to prevent accidental process-group signals.

  Registry corruption is propagated as `PoolError::RegistryCorrupted` (not
  silently defaulted) in accordance with the project's Result propagation
  policy.

### Fixed

- **Pool worker tracing initialisation** (`src/main.rs`): the
  `Commands::PoolWorker` arm now calls `setup_tracing(config.log_dir.as_deref())?`
  before entering `pool_worker::run`. Previously the 14 `tracing::warn!` /
  `info!` / `error!` calls inside `src/pool_worker.rs` were dead because the
  subscriber was never registered in the worker subprocess. Worker stderr now
  emits structured log lines under `RUST_LOG`.

- **`alc_run` `#[tool]` description** (`crates/algocline-mcp/src/service.rs`):
  added a sentence documenting the `host_mode` parameter — "Pass
  `host_mode: true` to route the session through the persistent worker pool
  (`1 session = 1 process`); the worker survives MCP server restarts and can
  be resumed via `alc_continue`." Previously the tool description was silent
  about `host_mode`, violating K-49 (new option must be reflected in `#[tool]`
  doc).

- **`alc_run` cache-reload failure now surfaced to MCP caller**
  (`crates/algocline-app/src/service/run.rs`): when `PoolRegistry::load_or_default`
  fails after a `run_via_pool` call, the error is now propagated as
  `pool_cache_reload_warning` in the wire response JSON (additive field, never
  omitted when a reload error occurs). Previously the `Err(e)` arm only emitted
  `tracing::warn!`, which reaches the MCP server's stderr log but is invisible
  to the MCP caller UI — a silent data loss in violation of the project's Result
  propagation policy.

- **`alc_pool_status` annotation corrected** (`crates/algocline-mcp/src/service.rs`):
  `read_only_hint` changed from `true` to `false`. The implementation calls
  `scan_and_gc` and writes `registry.json` on every invocation, making
  `read_only_hint = true` a K-111 annotation mismatch. `idempotent_hint = true`
  and `open_world_hint = false` are unchanged.

- **Pool state directory permissions tightened to 0700 / 0600**
  (`crates/algocline-app/src/pool/registry.rs`, `crates/algocline-app/src/pool/dispatch.rs`,
  `src/pool_worker.rs`): the pool state directory (`~/.algocline/state/pool/`),
  `registry.json`, `registry.lock`, and worker UDS socket files are now created
  with mode `0700` / `0600` (owner-only) instead of the umask-default `0755` /
  `0644`. This eliminates a multi-user RCE attack surface on shared hosts.
  Permission setting uses `std::os::unix::fs::PermissionsExt::set_mode` under
  `#[cfg(unix)]`; Windows builds are unaffected.

- **Worker process zombie reaping** (`crates/algocline-app/src/pool/dispatch.rs`):
  `spawn_worker` is migrated from `std::process::Command` to
  `tokio::process::Command`. The returned `tokio::process::Child` handle is
  immediately handed to `tokio::spawn(async move { let _ = child.wait().await; })`
  so that the kernel zombie entry is reaped after the worker exits. Previously
  the `Child` handle was dropped without calling `wait`, leading to zombie
  accumulation across idle-timeout / SIGTERM cycles.

- **`PoolClient::connect` handshake recv bounded to 10 s**
  (`crates/algocline-app/src/pool/client.rs`): the handshake `recv_line` call
  is now wrapped in `tokio::time::timeout(Duration::from_secs(10), …)`. A
  timeout is returned as `PoolError::Handshake("handshake recv timeout (10s)")`.
  Previously a worker that never sent its handshake response caused
  `RunningService::cancel` to hang indefinitely. The four `tests/e2e_pool.rs`
  integration tests that were `#[ignore]`d for this reason are now enabled and
  run by default.

### Changed

- **`EngineApi::run` gains `host_mode: Option<bool>` parameter** (breaking for
  trait implementors only; MCP wire shape is additive). External crates
  implementing `EngineApi` must add this parameter. All in-repo implementations
  (`AppService`, `engine_default_err!` macro, `FakeEngine`, `NotFoundEngine`)
  are updated in the same commit.

- **`RunParams` gains `host_mode: Option<bool>` field** (MCP wire shape is
  additive; existing callers that omit the field receive `host_mode=false`
  default behaviour, which is the unchanged in-MCP path).

## [0.30.0] - 2026-04-26

### Added

- **MCP Resource `alc://hub/index`**: new fixed resource that exposes the
  aggregated hub package catalog as a single `application/json` read. Merges
  cached `hub_index.json` data across all registered hub sources; individual
  source failures are surfaced in a `"warnings"` array field in the response
  JSON rather than failing the whole read (best-effort aggregate). Stale
  cache entries (>1h) and registry-load failures also surface as warnings,
  preserving partial diagnostic information across the MCP wire boundary.
  On a clean install with no cached sources the response is
  `{"schema_version":"hub_index/v0","packages":[]}`. Backed by the new
  `EngineApi::hub_index_aggregate` trait method and
  `AppService::aggregate_index` service method.

- **MCP `completion/complete` for resource template arguments**: `ServerHandler::complete`
  is now implemented in `algocline-mcp`, enabling IDE-style tab-completion of resource
  template variables (e.g. `@alc:alc://packages/<TAB>` auto-completes with installed
  package names in Claude Code). Supported argument slots:
  - `alc://packages/{name}/…` → installed package names (from `pkg_list`)
  - `alc://cards/{card_id}` → eval card IDs (from `card_list`)
  - `alc://scenarios/{name}` → scenario names (from `scenario_list`)
  - `alc://eval/{result_id}` → eval result IDs (from `eval_history`)
  - `alc://logs/{…}` → empty (no session-list API)
  All results are prefix-filtered by the typed value, capped at 100 entries
  (`has_more: true` + `total` when truncated). `ref/prompt` references return an
  empty result without error. `ServerCapabilities` now declares `completions`.
  New public API surface: `extract_template_vars` (RFC 6570 Level-1 variable
  parser) and `complete_resource_arg` on `ResourceCatalog` (for external crates
  that build on the MCP resource layer).

### Changed

- **`EngineApi::hub_index_aggregate` added** (breaking for trait implementors only;
  MCP wire shape is additive). External crates implementing `EngineApi` must add this
  method. The default return type is `Result<String, String>` (JSON string), consistent
  with all other hub trait methods.
- **Bump `rmcp` 0.16.0 → 1.5.0**. rmcp 1.x marks most model structs
  `#[non_exhaustive]`, so direct struct expressions (`ReadResourceResult`,
  `CompleteResult`, `ServerInfo`, `ReadResourceRequestParams`,
  `CallToolRequestParams`, `CompleteRequestParams`) were migrated to
  constructor / `Default` + field-mutation patterns. MCP wire shape unchanged;
  breaking only for direct rmcp consumers reading internal types in this
  workspace.

### Fixed

- **MCP Resources: error code for resource-not-found cases now returns `-32002`** (was
  `-32602 invalid_params` at 8 sites in `resources.rs`). Affected paths: segment
  mismatch in `read_types`, file-not-found in `read_types`, `pkg not found` in
  `read_packages/meta`, and wildcard arms in `read_packages`, `read_cards`,
  `read_scenarios`, `read_eval`, and `read_logs`. URI parse errors / unknown service /
  malformed query still correctly return `-32602 invalid_params`.
- **README**: corrected `alc://packages/{name}` template URI rows — the actual
  templates served are `alc://packages/{name}/init.lua` and
  `alc://packages/{name}/meta` (the bare `alc://packages/{name}` form returns
  `resource not found`).
- **`alc://hub/index` registry-load failure**: previously the registry-load
  step's `?` early-return discarded already-accumulated `warnings` entries
  (e.g. `config.toml hub.collection_url:` parse warnings). Now degrades to
  `Ok` with the failure surfaced as a warning, symmetric with per-source
  cache-corrupt handling.
- **`alc://hub/index` stale cache no longer silently empty**: `load_cached`
  now distinguishes `NotPresent` / `Stale(HubIndex)` / `Fresh(HubIndex)` /
  `Corrupt(String)`. Stale entries (>1h) still merge their data and emit a
  `hub cache stale (>3600s)` warning instead of being indistinguishable from
  a fresh-install empty result.
- **`completion/complete` for `eval_id`**: prefix filter now applied to the
  full eval history rather than after the source-side limit. Matches outside
  the newest 100 entries are now visible and `has_more` accurately reflects
  truncation.

### Improved

- **MCP Resources: `title` field populated for all fixed resources and resource templates**
  via extended `make_resource` / `make_template` helpers. Fixed resources
  (`alc://types/alc.d.lua`, `alc://types/alc_shapes.d.lua`) now carry human-readable
  titles visible in MCP client UIs. All 7 resource templates similarly include titles.

### Removed

- **Dead `chrono` direct dep + `option_env!("VERGEN_BUILD_TIMESTAMP")` path**
  in `algocline-mcp`. The `annotations.lastModified` Some-branch was
  unreachable in standard builds (no vergen wired), and an earlier note about
  populating `lastModified` from a build-time timestamp did not actually
  apply on shipped binaries. The shape always emitted `annotations: None`,
  which is preserved.

## [0.29.1] - 2026-04-26

### Fixed

- **Log directory now honors `ALC_HOME`**: `AppConfig::resolve_log_dir`
  branch 2 (`{app_dir}/logs`) is derived from the resolved `AppDir`
  instead of hard-coding `dirs::home_dir().join(".algocline").join("logs")`.
  Setting `ALC_HOME=/custom` now resolves logs under `/custom/logs`,
  matching every other Service-layer path (packages / cards / state /
  evals). Previous behavior left logs at `~/.algocline/logs` even when
  `ALC_HOME` was set — a latent inconsistency. `LogDirSource::Home`
  Display label is unchanged (`~/.algocline/logs`) since it documents
  the unset-default semantics, not the resolved path.
- This also eliminates the last `dirs::home_dir()` direct read in the
  Service layer; the only remaining HOME / `ALC_HOME` reads under
  `crates/algocline-app/src/service/` are inside `AppConfig::resolve_app_dir`
  (the documented single resolution point).

## [0.29.0] - 2026-04-26

### Added

- **`alc_status` v2 — long-running session observability**: session detail
  JSON now includes 5 additive fields for diagnosing in-flight execution
  without external instrumentation:
  - `phase` (string enum: `starting` / `running` / `llm_pending` /
    `continuing` / `completed` / `failed` / `aborted`)
  - `started_at` / `last_activity_at` / `elapsed_ms` (unix ms;
    `last_activity_at` is the primary stuck-detection signal)
  - `tokens` (cumulative `prompt_total` / `response_total` / `total`
    plus optional `current_query` snapshot during paused LLM calls)
  - `recent_logs` (per-session ring buffer, cap=20; captures both
    `alc.lua.print` and `alc.log` channels)
  - `conversation_history` (LLM query records, cap=10; opt-in via
    `StatusParams.include_history` to preserve high-frequency polling
    cost). Existing `alc_status` callers keep working — all new fields
    are additive Option types.
- New `algocline-core::recent_log` module (`LogEntry` / `LogSink`,
  `Arc<Mutex<VecDeque>>` ring buffer) and `ExecutionObserver::on_log`
  trait method. `MetricsObserver` routes log entries to the per-session
  `LogSink`; `Session` exposes the sink to the Lua bridge via
  `BridgeConfig`. (breaking for crate-internal `ExecutionMetrics::snapshot`
  callers — `include_history: bool` parameter added; MCP wire shape is
  additive.)

## [0.28.0] - 2026-04-26

### Changed

- **Service-layer error propagation**: typed `ServiceError` / `ProjectFilesError` /
  `LockError` / `TranscriptError` / `PkgListError` / `HubRegistriesError` /
  `InstalledManifestStoreError` hierarchy is now used end-to-end inside
  `crates/algocline-app/src/service/`. `String` as `E` is confined to the MCP
  wire boundary (`engine_api_impl.rs`), where typed errors are flattened via
  `.map_err(|e| e.to_string())`. `with_exclusive_lock` is now generic over
  `E: From<LockError>`, eliminating the prior `String` round-trip that fused
  Lock-acquisition failures with `alc.toml` corruption into a single variant.
  (breaking for direct `service::*` consumers; MCP wire shape is unchanged.)
- **Silent-drop sites surfaced**: best-effort enrichment / projection writes
  that previously used `let _ = ...` or `tracing::warn!`-and-drop now surface
  warnings to the caller through additive `warnings` / `transcript_warning`
  fields in MCP response JSON. Affected: `hub_info` card-store reads,
  transcript meta projection writes, `run` session-strategy mutex poison.
  CLI / library context lacks a server-framework exception catcher, so
  `tracing::warn!` alone is insufficient — both the log and the user-facing
  response now carry the diagnostic.

### Fixed

- **`pkg_read_init_lua` honours explicit `project_root`**: previously called
  `resolve_project_root(None)` unconditionally, ignoring the AppService
  context. Tests worked around this by setting `ALC_PROJECT_ROOT` env, which
  raced across parallel tests because env names are process-global. Signature
  now takes `Option<&Path>` (matching `pkg_list` / `resolve_extra_lib_paths`),
  and tests pass `tmp.path()` directly. (breaking for direct callers;
  MCP wire boundary passes `None` for backwards-compatible behaviour.)
- **Card-store test isolation**: `crates/algocline-engine/src/card.rs` test
  module previously shared a process-wide `OnceLock<FileCardStore>` keyed
  by nanosecond-timestamp `unique_pkg()`. Nanosecond collisions caused
  flaky failures in `find_where_*`, `get_by_alias_*`, etc. All 29 tests now
  use per-test `FileCardStore::new(tempdir)`.
- **Crate-wide env-var test serialisation**: `with_env_var(key, val, f)`
  helper consolidated in `crates/algocline-app/src/service/test_support.rs`
  with a single static `Mutex`. `config.rs::app_dir_env_overrides_home`
  (which races against any test calling `AppConfig::default()`) now routes
  through this shared lock instead of bare `std::env::set_var`.

## [0.27.1] - 2026-04-25

### Fixed

- `JsonFileStore`: per-namespace lock to prevent lost updates under concurrent
  `alc.state.*` calls within the same process. The previous implementation
  noted "Safe within one process" but assumed single-threaded execution and was
  vulnerable to lost updates across tokio tasks (non-locking read-modify-write).
  A `std::sync::Mutex` per namespace now serialises the load → mutate →
  atomic-rename cycle. Multi-process safety still requires a backend with native
  `INCR` (Redis) or transactions (SQLite).
- Lua standard `print` is now redirected to `tracing::info!(target = "alc.lua.print", ...)`.
  Previously, raw `print(...)` from user strategies wrote directly to process
  stdout and corrupted the rmcp JSON-RPC transport, manifesting as
  `serde error expected value at line 1 column 1` in agent-block ↔ algocline
  sessions with ~50% reproducibility.

### Changed

- Bundled-packages tag bumped from `v0.19.0` to `v0.20.0`. v0.20.0 adds new packages (`particle_infer`, `isp_aggregate`) and includes the missing `abm/{mc,stats,sweep}.lua` + `abm/frame/` + `optimize/{eval,search,stop}.lua` files that `alc_pkg_doctor` previously flagged as `incomplete_pkg`.

## [0.27.0] - 2026-04-25

### Added

- `alc_pkg_doctor`: detect `incomplete_pkg` defect when a package's `init.lua`
  requires sibling submodules (`pkg.sub`) but the corresponding `sub.lua` /
  `sub/init.lua` is missing. Static string-literal `require` only (parenthesised
  form: `require("pkg.sub")` / `require('pkg.sub')`); dynamic / non-quoted forms
  are out of scope for MVP.

- **MCP Resources capability** — `algocline-mcp` now advertises `resources`
  alongside `tools`. Service-layer read-only paths are projected as MCP
  resources under the `alc://` scheme:
  - Fixed (`resources/list`): `alc://types/alc.d.lua`, `alc://types/alc_shapes.d.lua`
  - Templates (`resources/templates/list`): `alc://packages/{name}/init.lua`,
    `alc://packages/{name}/meta`, `alc://cards/{card_id}`,
    `alc://cards/{card_id}/samples`, `alc://scenarios/{name}`,
    `alc://eval/{result_id}`, `alc://logs/{session_id}`
  - Pagination via query string for samples/logs (`?offset=N&limit=M`).
  - MIME: `.lua` → `text/x-lua`, JSON → `application/json`.
  - See `docs/mcp-resources.md` for the full catalog and `@alc:<uri>` mention examples.

  Out of scope for this release (candidates for follow-up):
  - `alc://hub/index` — pending canonical AppDir path for `hub_reindex` default output.
  - `alc://packages/{name}/narrative` — `hub_gendoc` currently emits to an external `out_dir`.
  - `list_changed` notifications and `resources/subscribe` — static capability only in V1.

### Fixed

- **MCP Resources — pagination caps**: `?limit=` and `?offset=` on
  `alc://cards/{id}/samples` and `alc://logs/{session_id}` are now capped
  (10,000 / 10,000,000 / 10,000 / 1,000,000 respectively). Values above the
  cap return `invalid_params`. Prevents MCP-DoS via unbounded allocation.
- **MCP Resources — ID reserved-char rejection**: card_id, session_id, eval
  result_id, package name, and scenario name are now validated at the MCP
  boundary. URI-reserved characters (`& = ? / % SPACE`) return `invalid_params`.
- **MCP Resources — eval ID strict validation**: `alc://eval/{result_id}` now
  validates `^[A-Za-z0-9-]+_\d+$` instead of a bare `contains('_')` check.
- **MCP Resources — `read_types` explicit match**: only `alc.d.lua` and
  `alc_shapes.d.lua` are accepted as file names; any multi-segment or
  unrecognized path returns `invalid_params`.
- **MCP Resources — `parse_query` `=` in value**: removed erroneous rejection
  of query values containing `=` (e.g. `?key=a=b` is now accepted; `split_once`
  already splits on the first `=` only).
- **`pkg_read_init_lua` — malformed `alc.local.toml` propagation**: a corrupt
  local TOML file now returns `Err` (surfaced to MCP caller) instead of being
  silently swallowed via `tracing::warn!`. (CLAUDE.md §Service 層の Error 伝播規律)
- **MCP Resources — `pkg_meta` trait method**: extracted `EngineApi::pkg_meta`
  to avoid a JSON round-trip in the MCP layer; `read_packages` meta arm now
  delegates to the new method. (breaking for `EngineApi` implementors only)
- **MCP Resources — tempdir leak in test helper**: `make_fake_catalog` now
  returns `(ResourceCatalog, TempDir)` instead of leaking via `mem::forget`.
- **MCP Resources — 4 missing E2E smoke tests**: added
  `test_mcp_resource_read_card`, `test_mcp_resource_read_card_samples_pagination`,
  `test_mcp_resource_read_eval_detail`, and `test_mcp_resource_read_logs_pagination`.

### Changed

- `EngineApi` trait: added `pkg_read_init_lua(name)` (breaking for `EngineApi`
  implementors only; MCP wire shape is additive).
- `AlcService::new` now takes `Arc<AppDir>` as a second argument (additive
  constructor change for library embedders; MCP wire shape unchanged).
  `AppConfig::app_dir()` accessor can be used to obtain the handle before
  casting `Arc<AppService>` to `Arc<dyn EngineApi>`.
- `EngineApi` trait: added `pkg_meta(name)` method so the MCP `read_packages`
  meta arm avoids a JSON round-trip via `pkg_list` (breaking for `EngineApi`
  implementors only; MCP wire shape is additive).

### Internal

- Extracted `engine_default_err!` macro in `algocline-mcp` test module to
  deduplicate `EngineApi` test stub boilerplate (45-method trait — eliminates
  per-stub edits when adding new methods). Applied to `NoopEngine`;
  `FakeEngine` builder and `NotFoundEngine` overrides remain handwritten.

## [0.26.3] - 2026-04-24

### Changed

- `alc init` now also distributes `alc_shapes.d.lua` to `~/.algocline/types/` alongside `alc.d.lua`, enabling zero-config LuaCats completion for `alc_shapes` combinators (T.string, T.number, ...) and registered shapes. `pkg_install` response adds a new `alc_shapes_types_path` field (additive; existing `types_path` unchanged).

## [0.26.2] - 2026-04-24

### Fixed

- **`alc_pkg_install` no longer aborts on pre-existing `pkg_link` dev
  symlinks.** In collection install mode, any entry whose destination
  at `~/.algocline/packages/<name>` already points outside the packages
  base (the canonical `pkg_link` case) previously produced a hard
  `Path '<name>' escapes base directory` error from `ContainedPath::child`
  and aborted the entire batch. The symlink is now detected via
  `symlink_metadata()` before the containment check, routed to a new
  `skipped_symlinks: [<name>, ...]` field in the response JSON, emitted
  as a `tracing::warn!` line, and the installer continues with the
  remaining pkgs. Operators run `alc_pkg_unlink <name>` if they want
  the git-clone copy to replace the dev link.

## [0.26.1] - 2026-04-24

### Changed

- **BUNDLED_VERSION bump**: `BUNDLED_SOURCES` for
  `algocline-bundled-packages` moved from `v0.18.0` → `v0.19.0`.
  `v0.19.0` is the first bundled release that tracks the v0.26 core
  migration — Lua `context7_config.lua` / `devin_wiki_config.lua`
  retired, project-specific content migrated to `alc.toml`
  `[hub] / [hub.context7] / [hub.devin]` sections, and the `just dist`
  pipeline streamlined to a single `alc_hub_dist` MCP call without
  `config_path`. `alc init` now pulls the v0.26-ready bundled repo.

## [0.26.0] - 2026-04-24

### Added

- **`luacats` projection for `alc_hub_dist` / `alc_hub_gendoc`**: emits
  `source_dir/types/alc_shapes.d.lua` from the embedded alc_shapes SSoT
  via `S.LuaCats.gen`. This unifies bundled-side `just gen-shapes`
  behavior into core so any third-party package author can regenerate
  lua-ls type definitions in a single `alc_hub_dist` call. Opt-in:
  caller must include `"luacats"` in `projections`.
- **pkg compat contract (`M.meta.alc_shapes_compat`)**: packages can declare
  a semver range of alc_shapes versions they are known to work with
  (e.g. `alc_shapes_compat = ">=0.25.0, <0.26"`). On load the
  `alc_hub_dist` / `alc_hub_gendoc` path reads the declared range and
  rejects out-of-range loads with a typed `ShapesCompatViolation
  { pkg_name, declared_range, actual_version, hint }` error surfaced via
  the MCP wire response. Malformed ranges return `ShapesCompatMalformed`
  with the `semver` parse error. Undeclared packages continue loading
  with a warning (backward compat; the bundled pkg set will be migrated
  in a separate commit in the same v0.26 release window).
  Drift is resolved **declaratively**: core vendors alc_shapes (v0.25.1
  refactor `f9d6260`), and pkg authors declare their supported range.
  No runtime `allow_mirror` / source_dir-mirror preload path is
  introduced — a pkg author who needs a patched alc_shapes is expected
  to fork core or upstream the patch rather than run a parallel mirror.
- **`alc_pkg_scaffold` MCP tool**: generates a minimal package skeleton
  at `<target_dir>/<name>/init.lua` with an `M.meta` / `M.spec.entries.run`
  / `M.run` template and a pre-filled `alc_shapes_compat` range derived
  from the embedded alc_shapes version (e.g. current core 0.25.1 →
  `">=0.25.0, <0.26"`). Optional `category` / `description` params flow
  into `M.meta`. Typed errors on name validation failure
  (`PkgScaffoldError::NameInvalid`) or pre-existing init.lua
  (`AlreadyExists`) are surfaced via the MCP wire response. Companions
  the v0.26 pkg compat contract (commit `bce529a`): scaffolded packages
  are ecosystem-ready the moment they're created.
- **`narrative` and `llms` projections for `alc_hub_dist` / `alc_hub_gendoc`**:
  these projections were previously rejected with `unknown projection` even
  though the embedded `gen_docs.lua` already emits `docs/narrative/{pkg}.md`,
  `docs/llms.txt`, and `docs/llms-full.txt` unconditionally when
  `lint_only=false`. The Rust-side allowlist now accepts `"narrative"` and
  `"llms"` as valid `projections` values. No `gen_docs.lua` argv change is
  needed (approach A): the files are produced whenever any non-lint-only
  projection set is requested, regardless of whether `narrative`/`llms` appear
  in the list. The MCP doc string for `alc_hub_gendoc` / `alc_hub_dist` is
  also updated to list all accepted projection values, including the previously
  undocumented `luacats`.

### Changed — **BREAKING**: `alc_hub_gendoc` `config_path` retires `.lua`; new `alc.toml [hub.*]` sections

- `config_path` no longer accepts `.lua` files.  Passing a `.lua` path returns
  `gendoc: config_path extension '.lua' is no longer supported; use .toml`.
- When `projections` includes `"context7"` or `"devin"` and `config_path` is
  omitted, the project root's `alc.toml` is auto-explored.  New subsections
  `[hub]`, `[hub.context7]`, `[hub.devin]` are recognized.  See
  `docs/hub-gendoc-config.md` for the full schema.
- C7 / Devin rules and repo notes now ship as core defaults embedded in
  `algocline`.  User config may append (`extra_rules` / `extra_repo_notes`),
  override wholesale (`rules_override` / `repo_notes_override`), or read from
  an external file (`rules_file` / `repo_notes_file`).  `rules_file` and
  `rules_override` are mutually exclusive (typed error on both set).
- Migration: move `context7_config.lua` / `devin_wiki_config.lua` contents
  into the repo's `alc.toml` under the new subsections.  Callers using an
  explicit flat `config_path=*.toml` with `[context7]` / `[devin]` sections
  continue to work unchanged.

### Fixed

- **`alc_hub_gendoc` / `alc_hub_dist` context7 projection: `projectTitle` was
  always `null`.**  `HubProjectionConfig::to_context7_toml` now populates
  `projectTitle` from the resolved name chain
  (`[hub.<proj>].name > [hub].name > hub_index repo basename >
  DEFAULT_NAME_FALLBACK`) and `description` from the resolved description
  chain (`[hub.context7].description > [hub].description >
  DEFAULT_C7_DESCRIPTION`).  The precedence chains were already computed;
  the output keys were simply never written.  Devin identity fields
  (`project_name`, `description`) are not emitted: the DeepWiki schema
  (<https://docs.devin.ai/work-with-devin/deepwiki>) only documents
  `repo_notes` and `pages` as recognised top-level keys.

### Changed

## [0.25.1] - 2026-04-23

### Changed

- **Embedded `hub_gendoc` / `alc_hub_dist`: `alc_shapes` Lua library now fully
  vendored.** `algocline-app` now embeds the full `alc_shapes` module set
  (`init.lua` / `t.lua` / `reflect.lua` / `check.lua` / `instrument.lua` /
  `luacats.lua` / `spec_resolver.lua`) via `include_str!`, eliminating the
  disk fallback path in `register_preloads`. The `source_dir`'s on-disk
  `alc_shapes/` directory is no longer consulted at runtime. Non-breaking:
  MCP wire shape unchanged; `source_dir` is still accepted. Any third-party
  package author can now invoke `alc_hub_dist` against their own source tree
  without vendoring `alc_shapes` themselves.
- **`alc_shapes` version pinning**: the embedded `alc_shapes` now declares
  `M.VERSION` and the `hub_gendoc` / `alc_hub_dist` resolver reads it from
  any on-disk mirror at `source_dir/alc_shapes/init.lua`. When the mirror's
  `M.VERSION` differs from the embedded constant, dist fails with a typed
  `ShapesVersionMismatch { embedded, mirror, hint }` error surfaced via the
  MCP wire response — catches drift between core's vendored copy and a
  downstream repository's mirror at the earliest possible point.

### Removed

- `gendoc_bundled_smoke.rs` — the `#[ignore]`d bundled-packages parity smoke
  test is retired in favour of a plain rmcp-based E2E
  (`test_alc_hub_dist_fixture` in `tests/e2e.rs`) that runs against an in-repo
  fixture (`tests/fixtures/hub_dist_sample/`) and is CI-default.

### Fixed

- **Embedded `hub_gendoc` parity with standalone Lua**: `register_preloads`
  now accepts `source_dir` and loads `alc_shapes` / `alc_shapes.t` from disk
  when available (`{source_dir}/alc_shapes/init.lua`), falling back to
  in-binary stubs for fixture-only runs. Also adds a disk-backed preload test
  (`T.ref("voted")` via `tools.docs.projections`) and hardens
  `T.discriminated` by copying `variants` before schema construction.
- **Embedded `hub_gendoc` / `alc_hub_dist`**: extend the in-process
  `alc_shapes` / `alc_shapes.t` stubs so bundled package `init.lua`
  modules load under `gen_docs` (real trees use `local T = S.T`,
  `S.instrument`, `:describe`, and `T.boolean` / `T.table`). Fixes
  `dist` / `gendoc` failures against full `algocline-bundled-packages`
  that were unrelated to hub TOML config. Also fixes Lua closure
  ordering so `describe` captures `make_schema` as a local.

## [0.25.0] - 2026-04-23

### Added

- `alc_hub_gendoc` MCP tool — generate human-readable docs (narrative /
  hub / llms / context7 / devin projections) from a `hub_index.json`.
  Embeds `gen_docs.lua` + docs modules from algocline-bundled-packages so
  downstream hub repos no longer need to vendor the generator. Bundled-
  specific `context7_config.lua` / `devin_wiki_config.lua` are injected
  via the `config_path` argument.
- `alc_hub_dist` MCP tool — facade that runs `alc_hub_reindex` followed
  by `alc_hub_gendoc` in sequence, returning a composed
  `{ reindex, gendoc, preset_catalog_version, preset? }` response. Optional `preset` expands into
  primitive `alc_hub_gendoc` arguments (builtin `Current` recipes +
  optional `alc.toml` overrides under `[hub.dist.presets.<name>]`).
  Successful responses always include `preset_catalog_version`; when
  `preset` is used, an additional `preset` object includes the resolved
  primitive args for observability. `alc_info` also reports
  `preset_catalog_version`.
  Fails fast on reindex error; on gendoc failure the error text embeds
  the (already-succeeded) reindex JSON so the caller sees both outcomes.

### Changed

- `alc init` bundled-packages tag bumped `v0.17.0` → `v0.18.0`
  - `evalframe` stays at `v0.3.0`
  - v0.18.0 ships `hub_index.json` reindexed against the new typed
    `PackageSource` wire shape (see next entry). Older v0.17.0 still loads
    via the read-compat shim.

### Changed — `source` field wire format

The `source` field in `alc_hub_info` / `alc_hub_search` responses and in the
on-disk `~/.algocline/hub_index.json` / `~/.algocline/installed.json` files
changes from a plain string to a tagged object. `alc_info` / `alc_pkg_list`
outputs follow suit.

**Before (0.24.x)**

```json
"source": ""
"source": "installed"
"source": "bundled"
"source": "/Users/me/pkgs/foo"
"source": "https://github.com/owner/repo"
```

**After (0.25.0)**

```json
"source": {"type": "unknown"}
"source": {"type": "installed"}
"source": {"type": "bundled", "collection": null}
"source": {"type": "path", "path": "/Users/me/pkgs/foo"}
"source": {"type": "git",  "url":  "https://github.com/owner/repo", "rev": null}
```

Variants: `unknown` / `installed` / `bundled {collection}` / `path {path}` /
`git {url, rev}`.

### Migration

**Existing users: mostly no-op.**
`~/.algocline/installed.json` and `~/.algocline/hub_index.json` stored in the
legacy string form still load in 0.25.0 (a read-compat shim absorbs them).
Each entry is rewritten to the tagged form the next time `alc_pkg_install` /
`alc_pkg_remove` / `alc_hub_reindex` touches it. Legacy `""` entries now
surface in `alc_pkg_repair` as `unrepairable` with
`reason: "source unknown (legacy entry; run alc_hub_reindex)"`; run
`alc_hub_reindex` once to clear them.

**MCP consumers that read `source` from `alc_hub_info` / `alc_hub_search`:
update required.** Replace the string-typed branch with an object dispatch:

```lua
local src = response.pkg.source  -- table in 0.25.0+
if     src.type == "git"      then return src.url
elseif src.type == "path"     then return src.path
elseif src.type == "bundled"  then return src.collection   -- nil or string
else                                return nil             -- installed / unknown
end
```

For dual-compat with 0.24.x, append `elseif type(src) == "string" then ...`.

**Bundled packages repo:** `algocline-bundled-packages/hub_index.json` will
ship already reindexed in v0.18.0. Running 0.25.0 against v0.17.0 still
works through the read shim.

## [0.24.1] - 2026-04-19

### Changed

- `alc init` bundled-packages tag bumped `v0.16.0` → `v0.17.0`
  - `evalframe` stays at `v0.3.0`

## [0.24.0] - 2026-04-19

### Changed

- `alc init` bundled-packages tag bumped `v0.15.0` → `v0.16.0`
  - `evalframe` stays at `v0.3.0`

### Added

- **`alc_pkg_remove` `scope` parameter**: reintroduced with manifest-only
  semantics. Accepts `"project"` (default, existing behavior — removes
  from `alc.toml` + `alc.lock`), `"global"` (removes the entry from
  `~/.algocline/installed.json`), or `"all"` (both, lenient — succeeds
  if either scope had the entry).
  - Physical files in `~/.algocline/packages/{name}/` are **never**
    deleted by any scope. This is different from the historical
    0.14.0 `scope="global"` (removed in 0.15.0), which deleted the
    cache directory and was retired for safety. The name is reused
    because the new semantics are safe (manifest edit only) and
    mirror `PkgLinkScope`'s existing `scope` pattern.
  - Closes the "orphan `installed.json` entry has no tool cleanup
    path" gap that forced manual JSON edits (e.g. after e2e test
    source tempdirs vanish, leaving dead manifest entries).
- **`PkgRemoveScope` enum** in `algocline-mcp`: snake_case-serialized
  `project` / `global` / `all`. Unknown values are rejected by the
  schema rather than silently defaulted.
- **`EngineApi::pkg_remove`**: gains a `scope: Option<String>`
  parameter (breaking for trait implementors; the in-tree `AppService`
  impl is updated accordingly).

## [0.23.1] - 2026-04-19

### Changed

- `alc init` bundled-packages tag bumped `v0.14.0` → `v0.15.0`
  - 106 packages available (adds SoT docs pipeline + `alc_shapes` DSL extensions on bundled side; algocline core is unchanged — `hub_index.json` wire format `hub_index/v0` and flat `M.meta` structure are compatible)

## [0.23.0] - 2026-04-19

### Added

- `alc_status`: new `pending_filter` parameter for field-level projection of paused `LlmQuery`
  - String preset: `"meta"` (no prompt), `"preview"` (truncated prompt, default `ALC_PROMPT_PREVIEW_CHARS=200`), `"full"` (full prompt)
  - Custom object: `{ include_prompt: bool, prompt_max_chars: usize, include_batch_items: bool, include_opts: bool, include_payload: bool }`
  - UTF-8 safe truncation via `chars().take(N)` (never splits multi-byte sequences)
  - Unknown preset → typed error (no silent fallback)
  - Omit / `null` → legacy shape (full query fidelity)
- `ALC_PROMPT_PREVIEW_CHARS` env var: configure the preview char cap (default 200). Setting `0` yields empty previews — for no prompt at all, use the `"meta"` preset instead
- `algocline-engine`: public `PendingFilter`, `PromptProjection`, and `DEFAULT_PROMPT_PREVIEW_CHARS` for downstream host integration
- E2E coverage (`tests/e2e.rs`): 6 new rmcp-based tests — preset meta / preview+full / unknown preset error / bad-shape error / custom object filter / paused session projection
- `alc_pkg_list` / `alc_hub_search`: unified list-tool knobs (`limit` / `sort` / `filter` / `fields` / `verbose`)
  - `verbose = "summary"` (default) | `"full"`; `fields` > `verbose` when both set
  - `sort`: `"-key,key2"` DSL; default `pkg_list="-active,-installed_at"`, `hub_search="-installed,name"`
  - `filter`: key-value exact match; when `filter` and legacy `category` / `installed_only` conflict on the same key, **`filter` wins** (explicit parameter priority). `hub_search`'s `category` / `installed_only` fold into the `filter` map only when the key is not already set
  - `limit`: `null` → tool default (50); `0` → **no limit / return all** (empty-means-all idiom); negative → clamped to 0 (also "no limit")
- `hub_search`: `SearchResult.docstring_matched: Option<bool>` — hit source flag (true only when the query hit docstring alone)
- Size regression guard: default summary output capped < 15KB (50 entries × ~224 chars/entry ≈ 11 KB realistic ceiling, ~34% headroom)

### Changed

- `EngineApi::status` trait gains `pending_filter: Option<serde_json::Value>` (breaking for trait implementors only; MCP wire shape is additive)
- `Session::snapshot` / `SessionRegistry::list_snapshots`: signatures gain `Option<&PendingFilter>` for server-side projection
- `alc_pkg_list` summary preset fields (verbatim): `name`, `scope`, `version`, `active`, `resolved_source_path`, `resolved_source_kind`
- `alc_pkg_list` full preset fields (verbatim): summary + `install_source`, `installed_at`, `updated_at`, `override_paths`, `overrides`, `linked`, `link_target`, `broken`, `path`, `source`, `source_type`, `meta`, `error`
- `alc_hub_search` summary preset fields (verbatim): `name`, `version`, `description`, `category`, `installed`, `docstring_matched`
- `alc_hub_search` full preset fields (verbatim): summary + `source`, `card_count`, `best_card`, `docstring`
- `hub_search`: `SearchResult.docstring` is now `skip_serializing` (internal signal for matching; projected only when the field set includes `"docstring"` — via `fields=["docstring"]` or `verbose="full"`)

### Preset drift policy

- Add field to preset = **minor** version bump
- Remove/rename field = **major** version bump
- Both must list the full preset verbatim in the CHANGELOG `Changed` section

### Fixed

- `alc_pkg_list` / `alc_hub_search`: large 1-line JSON output (63K–68K chars) no longer exceeds Claude Code context limits at default `verbose="summary"`

## [0.22.0] - 2026-04-18

### Added — Card sink fan-out (`ALC_CARD_SINKS`) + backfill

Card mutations (`create` / `append` / `write_samples` / alias writes /
`import_from_dir`) now fan out to every subscriber URI listed in the
`ALC_CARD_SINKS` env var (pipe-separated). The primary store remains
authoritative; subscriber failures are isolated — primary writes always
succeed, and per-subscriber health is exposed via `alc_stats` under the
new `card_sinks` field (`ok` / `err` counters keyed by event kind +
`last_error`).

Registered sinks can be backfilled with the new tool:

```lua
local r = alc.card.sink_backfill({
  sink    = "file:///tmp/mirror",
  dry_run = false,
})
-- r = { sink, pushed = {...}, skipped = {...}, failed = {...}, pushed_samples = {...} }
```

`alc_card_sink_backfill` pushes every Card from the primary store to one
registered subscriber URI. Drift-safe: cards already present on the sink
are skipped (never overwritten). Bypasses bus fan-out so in-sync peers
see no duplicate Created events. Supports `dry_run` for preview.

Tool annotations: `destructive_hint = false`, `idempotent_hint = true`,
`open_world_hint = false`.

### Added — `SubscriberStats` exposed via `alc_stats`

`alc_stats` now includes a `card_sinks` array with one row per
registered subscriber:

```json
"card_sinks": [
  {
    "sink": "file:///tmp/mirror",
    "ok":  { "created": 12, "appended": 3, "samples": 5, "aliases": 1 },
    "err": { "created": 0,  "appended": 0, "samples": 0, "aliases": 0 },
    "last_error": null
  }
]
```

All four event-kind keys are always present (may be 0). `last_error`
populates the latest failing publish with `{ kind, msg, ts_ms }`.

## [0.21.0] - 2026-04-17

### Changed

- `alc_pkg_repair`: LocalPath sources whose source directory is missing, or exists but has no `init.lua` at root, are now classified as `unrepairable` (`kind: "installed_missing"`) with an actionable `reason` / `suggestion`, instead of landing in `failed` with the misleading "'name' parameter is only supported for single-package dirs" message. Matches the policy already used for Bundled / Path sources: states detectable before attempting install belong in `unrepairable`; `failed` is reserved for runtime errors during an actual install attempt.
- `alc_pkg_install` (local path): now rejects a missing source directory up front with a clear `"Source directory does not exist: {path}"` error instead of falling through to the collection-mode branch and producing the misleading `'name' parameter` error.
- `alc init`: bundled `algocline-bundled-packages` tag bumped `v0.12.0` → `v0.14.0`. `evalframe` stays at `v0.3.0`.

## [0.20.1] - 2026-04-16

### Added

- `alc_pkg_list`: entries now include `resolved_source_path` (canonical absolute dir), `resolved_source_kind` (installed/linked/local_path/bundled), and `override_paths` (shadowed same-name pkg paths) for LLM agent source access.

### Fixed

- `alc_pkg_list`: project `installed` / `git` / `bundled` entries no longer list their own backing directory (`packages_dir()/{name}`) as a `override_paths` self-shadow. Only genuinely distinct same-name packages (e.g. a project `path` vendor dir overriding a global install) now appear in `override_paths`.

### Changed

- `alc_pkg_list` (internal): meta merge ordering tightened so every host-authoritative field (`error`, `linked`, `link_target`, `broken`, …) is uniformly protected from Lua `meta.*` clobbering. Output JSON shape is unchanged for conforming packages; packages whose `meta` illicitly shadowed these names now correctly return the host value.
- `alc_pkg_list` (internal): `resolved_source_kind` is now a typed enum internally (`Installed`/`Linked`/`LocalPath`/`Bundled`); wire format is identical (snake_case strings).

## [0.20.0] - 2026-04-13

### Added — `alc_card_samples` / `alc.card.read_samples` gains `where`

The per-case sidecar reader now accepts the same nested-object `where`
DSL as `alc_card_find`, evaluated against each JSONL row. `offset` is
applied after filtering (Prisma/SQL convention), so paging the matched
subset is predictable.

```lua
local failures = alc.card.read_samples(card_id, {
  where  = { passed = false, score = { lt = 0.5 } },
  offset = 0,
  limit  = 20,
})
```

Pure addition — calls without `where` keep previous semantics.

### Added — `alc_card_lineage` / `alc.card.lineage`

New lineage walker that traverses Card ancestry/descendants via the
`metadata.prior_card_id` convention. Directions: `"up"` (ancestors,
default), `"down"` (descendants), `"both"`. Optional `depth` cap,
`include_stats`, and `relation_filter` for following only edges with
specific `prior_relation` values. Returns `{ root, nodes, edges,
truncated }` where `nodes[*].depth` is signed (0 root, negative
ancestor, positive descendant).

Also documents `[strategy_params]` and `metadata.prior_card_id` /
`metadata.prior_relation` as recognized Card schema conventions.

### Changed — **BREAKING**: `alc_card_find` / `alc.card.find` DSL

Two breaking changes, plus one additive field. 0.x allows breaks —
migrate callers before upgrading.

**1. Filter fields → `where` object (Prisma-style)**

All ad-hoc filter fields (`scenario`, `model`, `min_pass_rate`) are
removed. Use a nested `where` object that walks Card sections, with
implicit equality on scalars and reserved operators on leaf objects.

```lua
-- Before
alc.card.find({ pkg = "foo", scenario = "bar", min_pass_rate = 0.5 })

-- After
alc.card.find({
    pkg = "foo",
    where = {
        scenario = { name = "bar" },
        stats = { pass_rate = { gte = 0.5 } },
    },
})
```

Operators: `eq ne lt lte gt gte in nin exists contains starts_with`,
plus logical `_and` / `_or` / `_not` at any level.

**2. `sort` → `order_by` (dotted path, descending prefix, multi-key)**

`sort = "pass_rate"` is replaced with `order_by` — a dotted-path string
(`"stats.pass_rate"`), a `-` prefix for descending, or an array for
multi-key sort with tiebreakers.

```lua
-- Before
sort = "pass_rate"

-- After
order_by = "-stats.pass_rate"
-- or
order_by = { "-stats.pass_rate", "created_at" }
```

**Added**: `offset` for pagination (pure addition, non-breaking).

Missing-field semantics: `eq`/`lt`/etc. evaluate false on missing
fields; `ne`/`nin` evaluate true; `exists: false` matches only when
the field is absent.

See `docs/lua-stdlib.md#alccardfind` for the full reference.

## [0.19.0] - 2026-04-13

### Added

- **`alc_hub_info`**: Show detailed information for a single package — metadata, all Cards, aliases, and aggregated stats (card count, eval count, best pass rate). Looks up remote indices first, falls back to local `init.lua` parse.
- **`collection_url` support**: New `[hub].collection_url` in `~/.algocline/config.toml` adds a Tier 0 aggregated index URL, fetched before per-source registries.

### Fixed

- **Path traversal guard** in `hub_info`: reject package names containing `..`, `/`, or `\`.
- **Duplicate `card::list` call** in `hub_info`: reuse a single call for both JSON output and stats.
- **`count_evals_for_pkg` ordering**: two-pass collection eliminates `read_dir` iteration-order dependency.

### Changed

- Enriched module-level RustDoc for `card.rs` (Card schema, design principles, storage layout) and `hub.rs` (staged design, index schema, 4-tier discovery, caching).

## [0.18.0] - 2026-04-12

### Added — Hub: Package Discovery & Search

Registry-based remote index discovery with per-source caching.

- **`alc_hub_search`**: Search packages across remote Hub indices + local install state. Index URLs are auto-discovered from hub registries (populated by `pkg_install` / `card_install`), the installed-packages manifest, and bundled seeds. Results include `installed: true/false`, descriptions, categories, and source URLs.
- **`alc_hub_reindex`**: Generate a hub index from a packages directory. New `source_dir` parameter enables pure metadata extraction from a repo checkout (no manifest or card data mixed in) for CI publishing.
- **Hub registries** (`~/.algocline/hub_registries.json`): Persistent registry of source URLs, auto-populated on `pkg_install` and `card_install`. Atomic writes via tempfile + rename.
- **Per-source cache** (`~/.algocline/hub_cache/{hash}.json`): Each remote index cached independently with 1-hour TTL using FNV-1a URL hashing.

### Changed

- Bump `algocline-bundled-packages` to v0.11.2 (adds `hub_index.json`)

## [0.17.1] - 2026-04-12

### Changed

- Bump `algocline-bundled-packages` from v0.11.0 to v0.11.1
  (Optimizer Card support)

## [0.17.0] - 2026-04-12

### Added — `alc.eval()` Lua function

Evalframe facade exposed as a first-class Lua function in prelude.
Accepts string scenario names or inline tables, wires the algocline
provider automatically, and optionally emits a Card on completion.

### Changed — `alc_eval` MCP tool delegates to `alc.eval()`

The MCP `alc_eval` tool now delegates to the prelude `alc.eval()`
function instead of hand-building evalframe Lua code. Card emission
is handled Lua-side, removing Rust-side `maybe_save_card`.
`eval_compare` shares the `STD_SHIM` constant with `eval`.

### Added — Card schema v0 (frozen)

Immutable run-result snapshots stored as TOML under
`~/.algocline/cards/{pkg}/{card_id}.toml`. The full v0 surface is now
considered frozen — future additions land behind a `v1` schema bump.

**v0 schema**:
- REQUIRED: `schema_version`, `card_id`, `created_at`, `pkg.name`
- Everything else is OPTIONAL and auto-injected when derivable
- `card_id` format: `{pkg}_{model_short}_{YYYYMMDDTHHMMSS}_{hash6}`
- Low-hex `hash6` (DJB2 last 6 chars) to avoid top-bit collisions
- `param_fingerprint` auto-computed from `[params]` when present

**Lua API (`alc.card.*`)**:
- `create(table)` — write a new Card (immutable)
- `get(card_id)` / `get_by_alias(name)` — fetch full Card
- `list(filter?)` / `find(query?)` — summaries with sort / filter
- `append(card_id, fields)` — additive-only annotation
- `alias_set(name, card_id, opts?)` / `alias_list(filter?)` — mutable aliases
- `write_samples(card_id, samples)` / `read_samples(card_id, opts?)` —
  write-once per-case JSONL sidecar

**MCP tools (host-side read surface)**:
- `alc_card_list` / `alc_card_get` / `alc_card_find`
- `alc_card_alias_list` / `alc_card_alias_set` / `alc_card_get_by_alias`
- `alc_card_append`
- `alc_card_samples` (per-case sidecar read with `offset` / `limit` paging)

**`alc_eval` integration**: Opt-in `auto_card=true` emits a Card from
the eval result on completion, and when per-case rows are present
dumps them to a `{card_id}.samples.jsonl` sidecar.

**Examples**: `examples/cards/prompt_ab_demo.lua` — a self-contained
6-trial prompt sweep exercising create / find / alias_set / append
end-to-end with no LLM calls.

## [0.15.1] - 2026-04-09

### Added

- **mlua-mathlib v0.3.0**: Upgraded from v0.2. Adds 22 new `alc.math` functions:
  - Hypothesis testing: `welch_t_test`, `mann_whitney_u`, `chi_squared_test`, `ks_test`
  - Ranking & IR metrics: `rank`, `spearman_correlation`, `kendall_tau`, `ndcg`, `mrr`
  - Information theory: `entropy`, `kl_divergence`, `js_divergence`, `cross_entropy`
  - Special functions: `logsumexp`, `logit`, `expit`
  - Time series: `moving_average`, `ewma`, `autocorrelation`
  - Combinatorics: `permutations`
  - RNG: `shuffle`, `sample_with_replacement`

## [0.15.0] - 2026-04-09

### Added

- **`alc_init` MCP tool**: Initialize project — creates `alc.toml` in the project root if absent. Equivalent to `alc init` for project-scoped setup via MCP
- **`alc_update` MCP tool**: Update installed packages declared in `alc.toml` — re-installs each entry from its recorded source URL and updates `alc.lock`
- **`alc_migrate` MCP tool**: Migrate legacy `alc.lock` (v1 `local_dir` entries) to the new `alc.toml` + `alc.lock` schema. Generates `alc.toml` from existing lock entries and rewrites `alc.lock` to the new format
- **`alc_pkg_unlink` MCP tool**: Remove a symlink created by `alc_pkg_link`. Rejects real directories (only symlinks are removed) to prevent accidental deletion of installed packages
- **`alc.toml`**: New project-level package declaration file. Declares packages with `name`, `source`, and optional `version`. Used as the source of truth for project-local package management
- **`alc.toml`-based project discovery**: Project root is now detected by walking up the directory tree to find `alc.toml` (previously `alc.lock`). `alc.lock` remains the resolved lockfile written by install/link operations
- **Lock mismatch warning**: Detects drift between `alc.toml` declarations and `alc.lock` resolved entries. Warns when packages declared in `alc.toml` are absent from `alc.lock` or vice versa
- **`PackageSource::Installed` / `PackageSource::Path`**: Renamed variants replacing `LocalCopy` and `LocalDir` respectively. `Installed` = package installed to cache from a URL; `Path` = symlinked local directory
- **`alc.toml` auto-append on install**: `alc_pkg_install` automatically appends the installed package to `alc.toml` when a project root is detected
- **Symlink-based `alc_pkg_link`**: Rewrites `pkg_link` to create a symlink inside `~/.algocline/packages/` pointing to the local directory. Removes the containment check entirely. `pkg_list` reports `linked`, `link_target`, and `broken` fields for symlink entries
- **Source provenance in `alc_pkg_list`**: Each entry now shows a `from` field indicating the install source (URL, path, or bundled)

### Changed

- **`alc_pkg_remove`**: Unified to remove from `alc.toml` + `alc.lock` only — cache directory is never deleted. The `scope` parameter is removed; removal always targets the project-local declaration
- **`alc_pkg_list`**: Project scope now reads from `alc.toml` (declarations) merged with `alc.lock` (resolved version/source), instead of reading `local_dir` entries directly from `alc.lock`
- **`PkgRemoveParams`**: `scope` field replaced by `version` (optional, for disambiguation)
- **`PkgLinkParams`**: `project_root` field removed; project root is auto-detected via `alc.toml` walk
- **`EngineApi` trait**: Removed `scope` from `pkg_remove`; added `alc_init`, `alc_update`, `alc_migrate`, `pkg_unlink` methods
- **`lockfile.rs`**: `LockPackage` loses `linked_at` field; gains `version: Option<String>`. `resolve_local_dir_paths` renamed to `resolve_path_entries` with containment check removed
- **`project.rs`**: `walk_up_for_lockfile` renamed to `walk_up_for_alc_toml`
- **`detect_legacy_format`**: Migrated from string-contains to TOML structural parsing to prevent false positives on package names containing `linked_at` or `local_dir`
- **Test helper consolidation**: Extracted duplicated `make_app_service` / `with_fake_home` into shared `test_support` module

### Fixed

- **`pkg_link` / `pkg_unlink` tests**: Replaced `Handle::block_on()` inside `#[tokio::test]` (runtime nesting panic) with `FakeHome` RAII guard pattern that allows direct `.await`. All 10 previously broken tests now pass
- **`eval_auto_installs_evalframe_on_missing` test**: Added `rt.enter()` guard for `AppService::new()` which calls `spawn_gc_task` requiring a runtime context; added `HOME_MUTEX` serialization to prevent env var races with `FakeHome` tests
- **Dead code cleanup**: Removed unused `resolve_installed_paths`, `resolve_abs`, and `#[allow(dead_code)]` annotations

## [0.14.0] - 2026-04-09

### Added

- **`alc_pkg_link`**: Link a local directory as a project-local package without copying. Records the path in `alc.lock`. Supports single package and collection layouts. Idempotent — re-linking updates the existing entry
- **`alc.lock`**: Project-local lockfile schema (version=1) for managing project-scoped package references. Stores `local_dir` entries pointing to on-disk paths
- **Project-local package resolution**: `alc.lock` `local_dir` entries are resolved as high-priority `FsResolver`s, taking precedence over `ALC_PACKAGES_PATH` and global `~/.algocline/packages/`. Enables per-project package overrides without modifying global state
- **`project_root` parameter**: `alc_run`, `alc_advice`, `alc_pkg_list`, `alc_pkg_remove` accept optional `project_root` to activate project-local package resolution. Auto-detected via `ALC_PROJECT_ROOT` env or `alc.lock` ancestor walk when omitted
- **`scope` parameter**: `alc_pkg_list` and `alc_pkg_remove` accept `scope` (`"project"` / `"global"`) for explicit scope targeting
- **`PackageSource` enum**: Type-safe representation of package origins (Git / LocalCopy / LocalDir / Bundled) with legacy string inference for backward compatibility

### Changed

- **`BUNDLED_VERSION`**: Updated bundled-packages from `v0.9.0` to `v0.11.0`
- **`EngineApi` trait**: `run` and `advice` gain `project_root: Option<String>` parameter; `pkg_list` gains `project_root`; `pkg_remove` gains `project_root` and `scope` (breaking for trait implementors)
- **`pkg.rs` → `pkg/` module**: Split monolithic `pkg.rs` into `pkg/install.rs`, `pkg/list.rs`, `pkg/remove.rs`, `pkg/tests.rs` submodules

### Fixed

- **Lua injection prevention**: Package names are whitelist-validated before interpolation into Lua source in `pkg_list` meta evaluation
- **Path containment**: `pkg_link` canonicalizes and containment-checks `LocalDir` paths so `alc.lock` cannot reference paths outside `project_root`
- **Atomic lockfile writes**: `save_lockfile` uses `NamedTempFile` + `persist` to prevent readers from observing half-written `alc.lock`
- **`eval_simple` require cache**: Clears `package.loaded[name]` before meta evaluation to avoid stale cached modules across calls

## [0.13.0] - 2026-04-04

### Added

- **`alc.llm_json(prompt, opts?)`**: LLM call with automatic JSON parsing and 1-retry repair. Uses `alc.json_extract` for 3-stage fallback parsing; on failure, retries with previous output included so the model can fix rather than regenerate
- **`alc.math`**: Numeric computing namespace (44 functions) via mlua-mathlib v0.2.0 — RNG, distribution sampling (Normal, Beta, Gamma, Poisson, Binomial, etc.), descriptive statistics, CDF/PPF, special functions (erf, gamma, beta, digamma, factorial), transforms (softmax, histogram, Wilson CI)
- **`docs/lua-stdlib.md`**: `alc.math` section with full API reference
- **`types/alc.d.lua`**: LuaCats type definitions for all `alc.math.*` functions

### Changed

- **`BUNDLED_VERSION`**: Updated bundled-packages from `v0.7.0` to `v0.9.0`, evalframe from `v0.1.0` to `v0.3.0`
- **Dependencies**: mlua-mathlib `0.1` → `0.2`

## [0.12.1] - 2026-04-02

### Fixed

- **`alc.match_bool`**: Add word boundary check to prevent false positives (e.g. `"ok"` in `"token"`, `"pass"` in `"bypass"`, `"no"` in `"innovation"`)
- **`alc.match_enum`**: Fuzzy fallback now splits text into words and compares per-word instead of whole-text, enabling typo detection in long LLM responses

### Added

- **`docs/lua-stdlib.md`**: Type Support section — LuaCats setup and `lua-language-server --check` CI integration guide

## [0.12.0] - 2026-04-02

### Added

- **`alc.match_enum(text, candidates, opts?)`**: Fuzzy enum matcher for LLM output. Case-insensitive substring match with Jaro-Winkler fuzzy fallback (Layer 0, powered by `fuzzy-parser` crate)
- **`alc.match_bool(text)`**: Yes/no normalizer for LLM responses. Returns `true`, `false`, or `nil` based on last-occurring affirmative/negative keyword (Layer 0)
- **`alc.parse_number(text, pattern?)`**: Extract numbers from LLM output with optional Lua pattern (Layer 1 Prelude)
- **Host token tracking**: `alc_continue` accepts optional `usage` field with `prompt_tokens` / `completion_tokens`. Tracked as `TokenSource::Host` in `ExecutionMetrics`, providing accurate token counts instead of character-based estimates
- **`max_tokens` budget**: Host can set `max_tokens` in `alc_run` context (`ctx._max_tokens`). When budget is exhausted, subsequent `alc.llm()` calls fail with a budget error
- **`alc init` / `alc update`**: Distributes `alc.d.lua` LuaCats type stub to `~/.algocline/types/alc.d.lua` on every run. Enables editor completion (Lua Language Server, `lua_ls`) for all `alc.*` StdLib functions. If `.luarc.json` is absent from the current directory, a setup tip is printed to stderr
- **MCP server startup**: Automatically distributes `alc.d.lua` on each server start, so the type stub is always up-to-date after `cargo install`
- **`alc_pkg_install` response**: Added `types_path` field — absolute path to the installed `alc.d.lua` stub — so MCP clients can surface the location without an extra tool call

### Changed

- **`alc_advice` `task` parameter**: Now optional (`Option<String>`). Packages that don't use `ctx.task` (e.g. `factscore`, `optimize`, `lineage`) can be called with `opts` alone
- **`EngineApi::advice` trait**: `task` parameter changed from `String` to `Option<String>` (breaking for trait implementors)
- **`EngineApi::continue_single` trait**: Added `usage: Option<TokenUsage>` parameter (breaking for trait implementors)

## [0.11.1] - 2026-04-01

### Changed

- **`alc_log_view`**: Added `max_chars` parameter for detail mode (default: 100KB). Truncates transcript from oldest rounds when response exceeds limit. Set `max_chars=0` for unlimited

## [0.11.0] - 2026-03-30

### Changed

- **`BUNDLED_VERSION`**: Updated from `v0.6.0` to `v0.7.0`

### Fixed

- **Clippy warnings**: Removed redundant closure in `spec.rs`, replaced `assert_eq!(…, true)` with `assert!()` in unit tests

## [0.10.0] - 2026-03-25

### Added

- **`alc.fork(strategies, ctx, opts?)`**: Parallel multi-VM strategy execution (Layer 0). Spawns N independent Lua VMs, each running one strategy with the same context. LLM requests from all children are batched through the parent's channel for true LLM parallelism. Strategy names validated (alphanumeric + underscore only)
- **`alc.cache(prompt, opts?)`**: Session-scoped memoized LLM call (Layer 1). Returns cached response for repeated identical prompts. FIFO eviction at 256 entries. Supports `cache_key` override and `cache_skip` bypass. `alc.cache_info()` / `alc.cache_clear()` for introspection
- **`alc.parallel(items, prompt_fn, opts?)`**: Batch-parallel LLM calls over an array (Layer 1). Transforms each item into a prompt via `prompt_fn`, sends all as a single `alc.llm_batch()` call. Optional `post_fn` for response post-processing
- **`QueryId::fork(vm_index, seq)`**: Fork-specific query ID format (`f-{vm}-{seq}`) for child VM LLM request tracking
- **`query_id` auto-resolve**: `alc_continue` without explicit `query_id` now auto-resolves when exactly one query is pending. Returns error for zero or multiple pending queries
- **`query_id` in response**: Single-query `needs_response` now includes `query_id` field for explicit identification

### Changed

- **`EngineApi` trait**: Extracted transport-independent API trait from `AppService` into `algocline-core`. MCP handler now operates through `Arc<dyn EngineApi>`, enabling future remote (socket/HTTP) implementations without depending on the concrete `AppService`
- **`FeedResult`, `ExecutionResult`, `TerminalState`**: Added `Serialize` derive for future transport serialization (HTTP/gRPC)
- **`BridgeConfig`**: Added `lib_paths` field for package search paths (needed by `alc.fork` to setup child VMs)
- **`bridge` module split**: Extracted `ForkEvent`, `ForkQuery`, `register_fork` into `bridge/fork.rs` submodule (bridge.rs 1249 → mod.rs 934 + fork.rs 345)

## [0.9.0] - 2026-03-24

### Added

- **Budget control**: `ctx.budget` with `max_llm_calls` and `max_elapsed_ms` limits. `alc.budget_remaining()` (Layer 0) returns remaining capacity, `alc.budget_check()` (Layer 1) provides boolean guard for optional LLM calls. Budget is enforced at `alc.llm()` / `alc.llm_batch()` call time
- **Token estimation**: `TokenCount` and `TokenSource` types for prompt/response token tracking in `ExecutionMetrics`
- **Progress reporting**: `alc.progress(step, total, msg?)` for structured step tracking, readable via `alc_status`
- **`alc_status`**: MCP tool to query active session status — state, metrics snapshot, progress, and strategy name. Omit `session_id` to list all active sessions
- **`alc.pipe(strategies, ctx, opts?)`**: Sequential pipeline combinator. Chains multiple strategies, passing each stage's result as the next stage's `ctx.task`. Supports both `require()`-based strategies and inline functions. Records `pipe_history` for debugging

### Changed

- **`BridgeConfig` struct**: Replaced growing parameter list in `bridge::register()` with a single config struct holding `llm_tx`, `ns`, `custom_metrics`, `budget`, and `progress` handles
- **Handle-based metrics access**: `CustomMetrics`, `Budget`, `Progress` now accessed via cloneable Handle types instead of `Arc<Mutex<T>>` directly

## [0.8.0] - 2026-03-24

### Changed

- **`BUNDLED_VERSION`**: Updated from `v0.4.0` to `v0.5.0` (9 new packages: 5 orchestration — orch_fixpipe, orch_gatephase, orch_adaptive, orch_nver, orch_escalate; 3 routing — router_daao, router_semantic, router_capability; 1 optimization — optimize)

## [0.7.1] - 2026-03-22

### Fixed

- **Per-session VM isolation**: Each `alc_run` / `alc_advice` call now spawns a dedicated Lua VM. Previously, all sessions shared a single VM, causing global namespace pollution (`alc`, `ctx`, `package.loaded`) between concurrent sessions. This eliminates coroutine cross-contamination when running multiple strategies in parallel

### Changed

- **`package.loaded` clearing removed**: No longer needed since each session starts with a fresh VM

## [0.7.0] - 2026-03-22

### Added

- **`alc_stats`**: Aggregate usage stats across all logged sessions. Per-strategy counts, averages (elapsed_ms, llm_calls, rounds), and totals. Optional `strategy` filter and `days` time window
- **`alc_info`**: Diagnostic tool showing server configuration — resolved log directory (with source), tracing mode, packages directory, and version. Similar to `mise doctor`
- **Strategy tracking**: Session logs (`.json` and `.meta.json`) now record `strategy` name for all advice/eval sessions, enabling per-strategy analytics

### Changed

- **`AppConfig`**: Replaced `TranscriptConfig` with centralized `AppConfig` resolved from environment variables. Single resolution point for all configuration
- **Log directory fallback chain**: `ALC_LOG_DIR` env → `~/.algocline/logs` → `$XDG_STATE_HOME/algocline/logs` → `<cwd>/algocline-logs` → None (stderr-only). Sandbox/container environments now preserve file logging via cwd fallback
- **Tracing**: Unified `setup_tracing` into single function accepting `Option<&Path>`. File + stderr when log dir available, stderr-only otherwise
- **Crate dependencies**: Removed `algocline-engine` dependency from `algocline-mcp` — accepts `AppService` directly

### Refactored

- **`algocline-app::service`**: Split 3099-line monolithic `service.rs` into domain-based module directory (`service/config.rs`, `path.rs`, `resolve.rs`, `transcript.rs`, `eval_store.rs`, `run.rs`, `eval.rs`, `pkg.rs`, `logging.rs`, `scenario.rs`, `tests/`). No API changes

## [0.6.0] - 2026-03-20

### Added

- **`alc.json_extract(raw)`**: Extract JSON object/array from LLM output. Handles raw JSON, markdown fences (` ```json ``` `), and embedded JSON within surrounding text via balanced brace/bracket iteration
- **`alc.state.update(key, fn, default?)`**: Single-operation read-modify-write for state. Reads current value, applies transform function, writes back
- **`alc.llm_safe(prompt, opts, default)`**: Non-throwing LLM wrapper. Returns default on failure instead of raising, logs warning. For optional enrichment where failure should not abort the pipeline
- **`alc.fingerprint(str)`**: Text normalization + DJB2 hash (8-char hex). For deduplication, not cryptography
- **`alc.tuning(defaults, ctx, opts?)`**: Config merge with deep-merge support for dict-like nested tables, shallow-replace for arrays/scalars. Supports `opts.prefix` for namespaced overrides, strips `_schema` key (reserved for Layer 2 parameter metadata)

### Changed

- **`BUNDLED_VERSION`**: Updated from `v0.3.0` to `v0.4.0` (6 new strategy packages: s2a, plan_solve, rstar, faithful, moa, bot)

### Fixed

- **`alc.json_extract`**: Iterate all balanced brace/bracket pairs via `gmatch` instead of first-match-only. Fixes false-negative when non-JSON balanced text precedes valid JSON
- **`alc.fingerprint`**: DJB2 modulo corrected from `0xFFFFFFFF` (2^32-1) to `0x100000000` (2^32) per standard specification
- **`alc.tuning`**: Warn and fall back to defaults when `opts.prefix` value exists but is not a table, preventing silent unintended overrides from top-level ctx keys

## [0.5.0] - 2026-03-18

### Added

- **Scenario management**: `alc_scenario_list`, `alc_scenario_show`, `alc_scenario_install` tools for managing reusable eval scenarios in `~/.algocline/scenarios/`
- **`scenario_name` parameter**: `alc_eval` now accepts `scenario_name` to load installed scenarios by name (e.g. `"math_basic"`), in addition to existing `scenario` (inline) and `scenario_file` (path)
- **Bundled scenarios**: `alc init` / `alc_pkg_install` automatically installs scenarios from `scenarios/` subdirectory in package collections
- **Resilience pattern**: `DirEntryFailures` type alias for batch I/O operations that collect per-entry failures instead of aborting. JSON responses include `"failures"` field for diagnostics

### Changed

- **`BUNDLED_VERSION`**: Updated from `0.2.0` to `v0.3.0` (includes 9 new strategy packages, robust_qa, and 3 bundled eval scenarios)

## [0.4.0] - 2026-03-17

### Added

- **`alc_eval`**: Evaluate a strategy against a scenario with test cases and graders. Accepts inline Lua (`scenario`) or file path (`scenario_file`) with a strategy name. Strategy is auto-wired as provider via `ef.providers.algocline`
- **`alc_eval_history`**: List past eval results with optional strategy filter, sorted newest-first
- **`alc_eval_detail`**: View a specific eval result by ID in full detail
- **`alc_eval_compare`**: Compare two eval results with Welch's t-test for statistical significance via evalframe's `stats.welch_t`
- **Eval persistence**: Results automatically saved to `~/.algocline/evals/` with full JSON result + lightweight meta files for fast listing
- **`alc.time()`**: Wall-clock primitive for evalframe latency tracking
- **evalframe**: Bundled as a system dependency, auto-installed on first `alc_eval` / `alc_eval_compare` use
- **Multi-source bundled installation**: `alc init` now supports multiple source repositories (Collection and Single kinds) instead of a single URL. `--dev` mode searches local sibling directories

### Changed

- **`alc_pkg_list`**: System packages (evalframe) excluded from listing to avoid require errors and declutter output
- **Lua string escaping**: Fixed escaping for newlines/carriage returns in bridge layer

## [0.3.0] - 2026-03-15

### Added

- **`underspecified` flag**: New domain primitive on `LlmQuery` for marking prompts whose preconditions depend on intent/goal definitions outside the current context. Same serde pattern as `grounded` flag
- **`alc.specify()`**: Layer 1 prelude convenience wrapper that sets `underspecified = true`, pairing with `alc.ground()` / `grounded` pattern
- **Bundled packages v0.2.0**: 15 new packages including intent understanding (ambig, prism, intent_discovery, intent_belief), reasoning strategies (ucb, panel, cot, sc, reflect, calibrate, contrastive, meta_prompt, factscore, cove), and combinators (deliberate, pre_mortem)

### Changed

- **`BUNDLED_VERSION`**: Updated from `0.1.0` to `0.2.0`

## [0.2.1] - 2026-03-15

### Changed

- **`alc init` versioning**: Decoupled bundled packages version from algocline's own `CARGO_PKG_VERSION`. Introduced `BUNDLED_VERSION` constant (`0.1.0`) so the two can evolve independently
- **`alc init` transport**: Replaced GitHub Releases tarball download with `git clone --branch v{BUNDLED_VERSION}`, eliminating the need for release asset management

### Removed

- **`review` package**: Removed from bundled package list (poor output quality)

## [0.2.0] - 2026-03-15

### Added

- **Transcript logging**: Full prompt/response transcript saved to `~/.algocline/logs/{session_id}.json` with lightweight `.meta.json` summaries for fast listing
- **Session notes**: `alc_note` tool to annotate completed sessions with feedback/observations; notes persisted in log files with `notes_count` tracked in meta
- **Log viewer**: `alc_log_view` tool to list sessions (from meta files) or view full transcript detail
- **Auto stats**: `rounds`, `total_prompt_chars`, `total_response_chars` tracked automatically via `MetricsObserver`
- **Transcript in stats**: `transcript_to_json()` on `ExecutionMetrics` for structured prompt/response history (excluded from `to_json()` stats output)
- **Local package install**: `alc_pkg_install` accepts absolute local paths, copying directly without git clone; supports both single packages and collections with overwrite semantics for dev workflow
- **Collection install**: Package repositories with `*/init.lua` subdirs are detected as collections and each subdir installed as a separate package
- **Test suite**: 151 tests across all crates — unit tests, property-based tests (proptest), path traversal rejection, chunk function invariants, state machine transitions

### Changed

- **Package architecture**: Standard packages extracted to separate `algocline-bundled-packages` repository; `alc_advice` auto-installs from GitHub if requested package is missing
- **MSRV**: Updated from 1.77 to 1.88

## [0.1.0] - 2026-03-01

### Added

- Initial release
- MCP server with `alc_run`, `alc_continue`, `alc_advice` tools
- Three-layer Lua StdLib: Layer 0 (Rust primitives), Layer 1 (Lua prelude), Layer 2 (packages via `require()`)
- `alc.llm()` / `alc.llm_batch()` — coroutine-based async LLM calls
- `alc.json_encode` / `alc.json_decode` — serde_json bridge
- `alc.log()` — tracing bridge
- `alc.state` — persistent key-value store (`~/.algocline/state/`)
- `alc.chunk()` — text segmentation (lines/chars with overlap)
- `alc.stats` — custom metrics recording
- Prelude combinators: `alc.map`, `alc.reduce`, `alc.vote`, `alc.filter`
- Package management: `alc_pkg_list`, `alc_pkg_install`, `alc_pkg_remove`
- `alc init` — bundled package installer (GitHub Releases + local fallback)
- Domain model: `ExecutionState` state machine with `PendingQueries` join barrier
- `ExecutionObserver` trait for cross-cutting concerns
- `SessionRegistry` for concurrent session management
- `ContainedPath` for path traversal prevention
- Coroutine-based execution via `mlua-isle` (non-blocking `alc.llm()`)
