apcore-cli 0.11.0

Command-line interface for apcore modules
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
# Changelog

All notable changes to this project will be documented in this file.
Format: [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).


## [0.11.0] - 2026-09-02

Bumps the required `apcore` floor to `0.28` and `apcore-toolkit` to `0.10.2` to track the aligned apcore 0.28.0 release (2026-08-31). Carries one display fix and a crate-root lint attribute (both under Fixed). `make check` is green end to end: `cargo fmt --check`, `cargo clippy --all-targets --all-features -- -D warnings` (0 warnings), and 800 tests across 31 binaries including the conformance suite, all against apcore 0.28.0.

**Why a minor rather than a patch.** This SDK needed no correctness fix of its own, but the three CLI SDKs ship as one version line, and the `apcli health` summary output changes here too. Version-locking them is what keeps the compatibility table and the cross-SDK conformance fixtures meaningful; see apcore-cli-python 0.11.0 for the changes that set the bump. Note for downstream crates: a `apcore-cli = "0.10"` pin does not resolve 0.11 — widen it to `">=0.11"` or `"0.11"`.

### Added

- **`tests/acl_argument_scoped_approval.rs` (4 cases)** pins the cross-SDK contract that an ACL-sourced approval requirement reaches `CliApprovalHandler`, replacing what had been a code-path argument with a measurement. The fourth case is the discriminating one — with auto-approve off and no TTY under `cargo test` the handler refuses, so the ungated call must still succeed while the `force`-carrying call must fail with `ErrorCode::ApprovalDenied`. Without that pair a gate that never fired would pass the suite.

### Changed

- **`apcore = ">=0.28"`, `apcore-toolkit = ">=0.10.2"`.** apcore-toolkit 0.10.2 is a dependency-tracking release with no source change.

### Fixed

- **`DEPENDENCY_NOT_FOUND` and `DEPENDENCY_VERSION_MISMATCH` exited 1 instead of 44.** Both are real `apcore::errors::ErrorCode` variants, and both reached `map_apcore_error_to_exit_code`'s catch-all arm, which returns `EXIT_MODULE_EXECUTE_ERROR`. apcore-cli-python and apcore-cli-typescript map both to 44, so the same dependency failure ended a script with a different code depending on which CLI ran it — and 1 is the code that reads as "the module ran and failed" rather than "the module could not be resolved".

  Found by a mechanical three-way diff of the exit-code maps, not by inspection: extracting all three and comparing key-for-key reported **2 divergent of 22 codes**, and re-running it after the fix reports 0. `EXIT_DEPENDENCY_NOT_FOUND` / `EXIT_DEPENDENCY_VERSION_MISMATCH` are named as their own constants to mirror the other two SDKs rather than reusing `EXIT_MODULE_NOT_FOUND` at the call site. Pinned in all three SDKs so the maps cannot drift again.

- **The `apcli health` summary line reported "no data" for a project whose modules it had just listed.** apcore classifies module health in **four** tiers — `healthy` / `degraded` / `error` / `unknown` — and the tally iterated only the first three. `unknown` means "no calls recorded yet", which is the state every module in a fresh project is in, so the common case rendered a populated table above a total that denied it:

  ```
    probe.echo                   unknown      0.0%         --
  Summary: no data
  ```

  **Pre-existing, and not introduced by this upgrade** — all three SDKs have emitted `unknown` since the tier set existed. apcore 0.28.0 is what brought it into focus: `sys-health-summary.schema.json` had declared the enum as `["healthy", "degraded", "unhealthy"]`, a value **no SDK emits**, and the release corrects it to the four tiers actually produced, splitting the summary's `unhealthy` count field into `error` and `unknown`. With the canonical shape finally naming four tiers, rendering three is a plain omission. Fixed in all three SDKs together, with the tally now covering `unknown`; a genuinely empty tally still reads "no data".

- **`cargo clippy -- -D warnings` failed on `clippy::result_large_err`, which would have taken CI red.** Two `Sandbox` methods returning `Result<Value, ModuleExecutionError>` were flagged because the enum's passthrough variant carries `apcore::errors::ModuleError` by value and that type is ≥184 bytes. **Not a regression from this upgrade** — reproduced identically with the previous `apcore = ">=0.27"` pin, so the trigger was the clippy version, not apcore; but CI runs `dtolnay/rust-toolchain@stable` with `-D warnings`, so it was going to fail there regardless of when it started.

  Resolved with a crate-root `#![allow(clippy::result_large_err)]`, mirroring apcore-rust, which suppresses the same lint at its own crate root with the reason that applies verbatim here: *"ModuleError is intentionally large (rich structured error for an SDK); boxing it everywhere would change the public API."* The variant holds the error by value on purpose — `cli::map_module_error_to_exit_code` reads its `ErrorCode` to keep the exit-code taxonomy identical across the `--sandbox` and direct paths — so boxing would both break a public enum in a patch release and diverge from the decision made by the crate that owns the type.

- **The discriminating approval tests no longer depend on stdin not being a terminal.** They originally used `CliApprovalHandler` with auto-approve off and relied on its non-TTY refusal, which is not a property of the test — it is a property of how the suite happens to be launched. `cargo test` does not redirect stdin, so run from an interactive shell the Rust case printed its prompt, blocked for the full 60-second timeout and then failed on `ApprovalTimeout` instead of `ApprovalDenied`; `pytest -s` and a main-thread vitest configuration reach the same trap. All three now register a small recording stub that always refuses, which removes the ambient dependency and lets each test assert the stronger property directly: that the gate consulted a handler **at all**, and for exactly which call. The real `CliApprovalHandler` is still exercised, on the auto-approve path, where its answer is deterministic.

### Notes

- **Three of 0.28.0's BREAKING Rust changes land on types this crate names, and all three are source-compatible here.** `ACLRule` gains an `approval` field and `AuditEntry` becomes `#[non_exhaustive]` — neither is constructed by this crate, which never builds or loads an `ACL`. `CallbackApprovalHandler::new` now takes an async fallible callback — `CliApprovalHandler` implements `apcore::ApprovalHandler` directly and never uses the convenience constructor. `ACL::evaluate_conditions` returning `ConditionOutcome` and `ACL::check` failing closed on allow-with-approval-required are both unreachable: the crate calls neither.

- **The one 0.28.0 change that reaches this SDK works correctly and needed no code.** Spec v1.28.0 §6.9 makes the approval gate fire on the union of three sources, so an ACL rule carrying `approval: required` (§6.1.6) now routes calls to modules annotated `requires_approval: false` through `CliApprovalHandler`. The trait adapter rebuilds its `module_def` shape from `request.annotations.requires_approval`, and `builtin_steps.rs:816` sets that to `true` before constructing the request for **any** source of the requirement — so `get_requires_approval` passes and the prompt runs. `cli_to_apcore_result` already returns a typed `apcore::ApprovalResult`; apcore-cli-python returned a bare mapping on the same path and had to be fixed in its 0.11.0.

- **`system.usage.*` behaviour changes are upstream-side and pass through unread.** 0.28.0 makes both modules honour `period` (statistics were previously computed over the full retained history) and changes `hourly_distribution[].hour` to `YYYY-MM-DDTHH`. `dispatch_usage` forwards `--period` verbatim, and the two TTY formatters read only `modules`, `period`, `module_id`, `call_count`, `error_count`, `avg_latency_ms`, `trend` and `p99_latency_ms`. `hourly_distribution` appears nowhere in this crate — nor in the Python or TypeScript CLIs — so no formatter or assertion depends on the retired key shape.

- **What the delta does *not* touch.** `Registry.list` / `get_definition`, `Executor::call` / `validate`, the approval handler protocol and the toolkit `format_*` surfaces are all unchanged across 0.27.0 → 0.28.0.


## [0.10.5] - 2026-08-17

Patch release. Bumps the required `apcore` floor to `0.27` to track the aligned apcore 0.27.0 release (2026-08-14). **No source changes** — the full test suite (fmt + clippy + all tests) passes unchanged against apcore 0.27.0, including the 511-case conformance suite.

The apcore 0.26.0 → 0.27.0 delta is BREAKING at the spec level, but touches no surface the CLI consumes — verified against the release notes and the actual call sites:

- **Middleware semantics** — `before_step` failure is now terminal/non-recoverable, `after_step` fires after a recovered step body. The CLI never constructs or configures middleware or pipelines; it only constructs `Executor::new(Arc<Registry>, Config)` and calls `Executor::call(module_id, input, None, None)` / `describe_pipeline` (read-only `StrategyInfo`). No exposure.
- **ACL-failed `validate()` introspection** — a failed `acl` check now withholds `module_preflight` / `module_preview` checks and `predicted_changes`. The CLI's validate path builds its own preflight checks locally from the registry descriptor (`validate.rs`), never consuming `Executor::validate()`'s return shape. No exposure.
- **`Registry.register_versioned` metadata `dependencies` persistence** — the CLI never calls `register`; module registration is via `Registry::discover(&FsDiscoverer)`, which emits `dependencies: vec![]` / empty `metadata`. No exposure.
- **Schema conversion (A23)** — object detection, nullable `anyOf` wrapping, sorted `required` are SDK-conversion rules. The CLI runs its **own** schema→clap converter (`schema_parser.rs` / `ref_resolver.rs`) on the descriptor's `input_schema`; `required` is order-insensitive and `type`-less nullable branches already fall to the string default. No exposure.
- **`pipeline.configure` 4-field set / `requires`/`provides` non-configurable** — the CLI never configures pipelines; a host config carrying other keys now fails at load (spec-mandated strictness, upstream concern).
- **No type coercion at the module boundary** — the CLI's own clap-string→JSON coercion (`cli.rs`) applies before `call()`, which receives already-typed JSON values. No exposure.
- **Removed/renamed API surface** — `ErrorCode::ConfigurationError` → `PipelineConfigurationError`, OtelTracing* removal + `opentelemetry` feature, `TracingMiddlewareConfig` field removal, `SchemaValidator::new()` no-coerce default — none used by the CLI (which uses only the `ModuleExecuteError` variant and no tracing middleware).

## [0.10.4] - 2026-07-14

Patch release. Bumps the required `apcore` floor to `0.26` to align the ecosystem on the 0.26.0 governance layer (additive, no breaking changes). No code or API changes.

## [0.10.3] - 2026-07-07
update package dependency version for apcore-toolkit (0.10.0) and increment project patch version

## [0.10.2] - 2026-06-24

### Changed

- **Required runtime bumped to apcore 0.25.0 and apcore-toolkit 0.9.1.** `Cargo.toml`
  dependencies raised from `apcore = "0.24"` / `apcore-toolkit = "=0.8.1"` to
  `apcore = "0.25"` / `apcore-toolkit = "=0.9.1"`, tracking the aligned apcore 0.25.0
  and apcore-toolkit 0.9.1 releases (both resolve from crates.io). **No source
  changes** — the full test suite passes unchanged.

  Neither delta touches a surface the CLI consumes:
  - **apcore 0.24.0 → 0.25.0** adds config-driven ACL discovery (`acl.root`
    activation + `ACL.discover`), auto-wired only by the `APCore` bootstrap and
    skipped when the caller supplies its own `Executor`. The CLI builds its own
    `Executor::new(Arc<Registry>, config)` and never constructs `APCore`, so
    discovery does not engage. The companion change — Rust's `acl.root` now
    defaulting to `./acl` instead of being hard-required — only relaxes config
    validation and is backward-compatible.
  - **apcore-toolkit 0.8.1 → 0.9.1** is a bug-fix release; its only API-surface
    change relaxes `RegistryWriter::write` / `HttpProxyWriter::write` from
    `&mut Registry` to `&Registry` (source-compatible, and unused by the CLI). The
    toolkit surface the CLI uses (`format_*`, `DisplayResolver`, `ScannedModule`,
    `ModuleStyle`, `FormatOutput`) is unchanged.

## [0.10.1] - 2026-06-15

### Changed

- **Required runtime bumped to apcore 0.24.0 and apcore-toolkit 0.8.1.** `Cargo.toml`
  dependencies raised from `apcore = "0.22"` / `apcore-toolkit = "=0.8.0"` to
  `apcore = "0.24"` / `apcore-toolkit = "=0.8.1"`, tracking the aligned apcore 0.24.0
  and apcore-toolkit 0.8.1 releases (both resolve from crates.io). **No source
  changes** — the full test suite passes unchanged.

  The apcore 0.22.0 → 0.24.0 delta does not touch any surface the CLI consumes:
  - **Schema type coercion now default-on; `SchemaValidator` returns the coerced
    value** — the CLI does not use apcore's `SchemaValidator`. It implements its own
    JSON-Schema → clap translator (`schema_parser.rs`) and validates via the
    `jsonschema` crate directly (`cli.rs`), so the coercion change has no effect.
  - **Per-instance `ToggleState` isolation (#71)** — the CLI never constructs
    `ToggleState`/`APCore` nor calls `is_module_disabled()`; toggling is delegated to
    `system.*` modules via `Executor::call()`.
  - **Error `details` snake_case alignment (A-D-019)** — the CLI reads only
    `err.code` (for exit-code mapping); it never serializes apcore error `details`.
  - **`Registry::list()/get_definition()`**, **`Executor::call()/set_approval_handler()`**,
    `Config::default()`, the `ApprovalHandler` trait, and `ModuleAnnotations` are
    unchanged across the delta.
  - Out of scope and unused by the CLI: registry-event delivery/DLQ (A-D-013),
    middleware `on_error` (A-D-010/012/015), `APCore.on()/events()` bus (D1-011),
    array redaction (A-D-003), `Config` env coercion (A-D-007/009),
    `CircuitBreakerMiddleware`, `Context::create()`.

## [0.10.0] - 2026-05-18

### Changed — BREAKING (feature surface)

- **Removed `toolkit` Cargo feature flag — apcore-toolkit is now unconditionally required (resolves 6.2; lands ADR-07).** `apcore-toolkit = "=0.7.0"` was already declared as a hard runtime dependency in `Cargo.toml`, but the code base wrapped every toolkit-delegating path in `#[cfg(feature = "toolkit")]` and provided silent-downgrade fallbacks under `#[cfg(not(feature = "toolkit"))]`. This created the same "fake optional" self-contradiction the PY / TS 0.10.0 release fixed: required at the manifest level, soft-degraded at the code level. The sweep landed in this release:
  - Deleted 10 `#[cfg(feature = "toolkit")]` gates across `src/output.rs` (descriptor adapter, markdown/skill arms in `format_module_list` / `format_module_detail`, and 5 test helpers / tests) and `src/main.rs` (toolkit-integration block).
  - Deleted 3 `#[cfg(not(feature = "toolkit"))]` fallback branches that silently degraded `--format markdown` / `--format skill` to JSON with a `tracing::warn!`.
  - Removed the now-dead `TOOLKIT_MISSING_HINT` const.
  - Removed `toolkit = []` and `default = ["toolkit"]` from `Cargo.toml` `[features]`. Only `test-support` remains.
- **Migration for downstream crates** that depended on `apcore-cli` with `default-features = false`: explicitly opting out of the `toolkit` feature was previously a way to compile without the toolkit code paths (at the cost of silent format downgrade); that option is gone. apcore-toolkit will always be linked. If you genuinely cannot tolerate the toolkit dependency, pin to `apcore-cli = "0.9"` and stay there until you can adopt the unified surface.

## [0.9.0] - 2026-05-13

### Added

- **`tests/conformance_snake_case_kwargs.rs`** — runs the cross-language Algorithm C-SNAKE fixture (`apcore-cli/conformance/fixtures/snake-case-kwargs/cases.json`) against `schema_to_clap_args` + `reconcile_bool_pairs`, mirroring `extract_cli_kwargs`'s extraction path. Five cases verify that schema property names with underscores (`has_solution`, `sort_by`, `sort_order`) survive the round trip from clap parse to the kwargs dict. No source change required — clap's `Arg::new(prop_name)` keeps the snake_case id as the access key; the Rust SDK is a parity reference for the parallel TypeScript fix. Surfaced as part of the cross-SDK regression coverage gap audit.
### Fixed (2026-05-13 — cross-SDK audit D10/D11/D1)

- **Sandbox output-cap raises wrong error class** (D11-007) — byte-cap overflow now returns `ModuleExecutionError::OutputSizeExceeded { module_id, limit_bytes, overflow_stream }` instead of `OutputParseFailed`. Display message uses MiB units and names the overflowing stream (`stdout`/`stderr`/`stdout+stderr`), matching Python and TypeScript. `src/security/sandbox.rs:374`.
- **`exec --dry-run` emits Rust-only "Pipeline preview" stderr block** (D11-011) — the preview was not declared in the spec and had no Python/TS equivalent. Removed for cross-SDK parity; `--trace` now uniformly routes through `executor.call_with_trace` across all three SDKs.
- **CLI brand string inconsistency in error messages** (D11-006) — `src/security/config_encryptor.rs:56` `DecryptFailed` error text changed from `apcore-cli config set` to canonical `apcli config set`, matching `src/security/auth.rs` which already used `apcli`.
- **Unused `schemars` dev-dependency** (D6 re-audit) — `schemars = "0.8"` in `[dev-dependencies]` had zero usage (`use schemars`, `#[derive(JsonSchema)]`). Removed.
- **Stale `CLAUDE.md` `Sandbox::execute` arity claim** (D10 re-audit) — the "Current Conventions" bullet claimed Rust used a 2-parameter signature with executor bound at construction time; actual source has been 3-parameter since v0.7. Updated to reflect the real 3-parameter form.

### Added

- **`set_all_options_help` cross-SDK parity note** (D1-W1) — `src/cli.rs:104` doc-comment now documents that Rust intentionally ships without the deprecated `set_verbose_help` alias (post-rename, no pre-v0.9 callers). Python/TS keep the alias for one MINOR deprecation cycle.
- **`ConfigResolver::resolve` language-idiom note** (D10-W1) — `src/config.rs:113` doc-comment documents that Rust narrows the return to `Option<String>` (serde_yaml_ng string-coercion) while Python returns `Any` and TypeScript returns `unknown`. Embedders needing typed YAML access are pointed at a v0.10 typed-resolver follow-up.
- **`AuthProvider` encryptor-fallback language-idiom note** (D11-005) — `src/security/auth.rs` now documents that Rust's two-tier encryptor chain (explicit arg → fresh instance) differs from Python/TS's three-tier chain (explicit arg → `config.encryptor` peer attribute → fresh instance). The peer-attribute tier requires a `ConfigResolver` field addition tracked as a v0.10 follow-up.

### Fixed

- **CSV `--format csv` heterogeneous-keys data loss** — `format_exec_result` previously derived headers from `arr[0].as_object().keys()` only, silently dropping fields that first appeared in later rows. Now delegates to `apcore_toolkit::format_csv()` which uses the **union of keys across all rows** in insertion-order. `src/output.rs:537-566`.
- **CSV line terminator** — now `\r\n` per RFC 4180 (was `\n`). Existing test expectations updated; old `\n`-based assertions replaced with CRLF assertions in `tests/test_output.rs`.

### Changed

- **User-visible help/man/completion text no longer leaks the `apcore` framework name** to end users of downstream CLIs built on apcore-cli. Affected strings: `--extensions-dir` option help (`Path to apcore extensions directory.` → `Path to extensions directory.`, `src/main.rs:367`), `exec` subcommand description (`Execute an apcore module` → `Execute a module`, `src/shell.rs:62`, `src/cli.rs:344`, plus the `shell.rs:1102` test fixture and the `tests/test_shell.rs:13-14` integration-test fixture), and man-page `ENVIRONMENT` text in `src/shell.rs:640, 653, 658` (`Path to the apcore extensions directory` → `Path to the extensions directory`, `Global apcore logging verbosity` → `Global logging verbosity`, `API key for authenticating with the apcore registry` → `API key for authenticating with the registry`). README's `--verbose` row updated to match. The `test_generate_man_page_name_uses_description` assertion updated to the new "about" text. Logger fields, source comments, doc comments, and environment-variable identifiers (`APCORE_*`) are unchanged — only descriptive copy that appears in `--help`, shell completion, and `man` output. Cross-SDK parity with Python 0.8.1 and TypeScript 0.8.2.

### Changed (breaking CLI surface)

- **Global `--verbose` flag renamed to `--all-options`** — The help-display flag is now `--all-options`; use `apcore-cli module --help --all-options` to reveal hidden built-in options. `verbose` is removed from the reserved schema property names set — module schemas may now freely define `verbose: boolean` for runtime output control. Public API: `set_verbose_help` / `is_verbose_help` renamed to `set_all_options_help` / `is_all_options_help`; statics renamed accordingly. Tracked in [apcore-cli#21](https://github.com/aiperceivable/apcore-cli/issues/21).

### Changed (breaking feature-flag semantics)

- **`apcore-toolkit` promoted from optional Cargo feature to REQUIRED runtime dependency** (`>=0.7.0`). The `toolkit` feature flag is retained in `default` features for backward compatibility — existing `#[cfg(feature = "toolkit")]` gates continue to work — but consumers using `default-features = false` must explicitly enable `features = ["toolkit"]` to compile. Reqired because csv/yaml/jsonl now route through the toolkit's reference implementation.
- **`serde_json::Map` iteration order** — transitively switched to insertion-order via the toolkit's `preserve_order` feature. Test assertions that relied on alphabetical iteration (`tests/test_output.rs::test_csv_plain_value_passthrough`) updated to expect insertion-order.

### Removed

- `csv_scalar_string` and `csv_field` private helpers — replaced by `apcore_toolkit::format_csv()` and the toolkit's RFC 4180 internals.

### Why

See ADR-09 in `apcore-cli/docs/tech-design.md` for the byte-equivalent toolkit-delegated tier rationale.

## [0.8.0] - 2026-05-08

### Security

- **D10-001 (critical) — `AuthProvider::authenticate_request` rejects trailing CR/LF in the API key** (`src/security/auth.rs`). Previous behaviour stripped trailing `\r`/`\n` before the malformed-key check, allowing a key ending in `"\n"` to silently produce `Authorization: Bearer <stripped>` and exposing the SDK to header-injection vectors. Python and TypeScript both reject any `\r` or `\n` at any position; Rust now matches. The regression test `test_authenticate_request_strips_trailing_crlf` was asserting the wrong behaviour and has been renamed to `test_authenticate_request_rejects_trailing_crlf` with the assertion inverted.
- **D10-truncated #1 — `ConfigEncryptor::retrieve` surfaces a user-actionable decryption error** (`src/security/config_encryptor.rs`). All `retrieve()`-time decryption failures (b64 decode, v1 AES, v2 AES) now route through a new `ConfigEncryptorError::DecryptFailed { key }` variant with the spec'd message `"Failed to decrypt configuration value '{key}'. Re-configure with 'apcore-cli config set {key}'."` Previously, the most common decryption failure modes leaked the internal `AuthTagMismatch` message and dropped both the originating config key and the remediation guidance. `AuthTagMismatch` is preserved as the internal-helper variant returned from `_aes_decrypt_v1`/`_aes_decrypt_v2`. Cross-SDK parity with Python (`config_encryptor.py:62-64,70-72`) and TypeScript (`config-encryptor.ts:136-149`).
- **D11-001 — Built-in-group rename surface** (`src/builtin_group.rs`, `src/cli.rs`, `src/main.rs`, `src/lib.rs`). Restores FE-13 P0 parity with Python `ApcliGroup.name` and TypeScript `ApcliGroup#name`. New `pub fn ApcliGroup::name(&self) -> &str` accessor backed by a `name: String` field that defaults to `"apcli"` and is validated against `NAME_REGEX = ^[a-z][a-z0-9_-]*$`. New factory variants `from_cli_config_with_name` / `from_yaml_with_name` / `try_from_yaml_with_name` accept `name: Option<String>`; the original 2-arg factories delegate with `None` for backward compatibility. New `validate_builtin_group_name` helper and `ApcliGroupError::InvalidName` variant. New module-level `effective_reserved_group_names`, `is_reserved_group_name`, and `pub fn set_reserved_group_names(...)` (mirrors TypeScript `setReservedGroupNames`); `cli.rs::build_module_command_with_limit` consults the live set so a renamed built-in group is honoured at collision-check time. Binary entry-point seeds the live set from `apcli_cfg.name()` and threads the resolved name through the `clap::Command::new(...)` builder.
- **D11-W3 — Sandbox canonicalises inherited `APCORE_EXTENSIONS_ROOT`** (`src/security/sandbox.rs:248`). The child env now carries an absolute, symlink-resolved path so sandboxed processes cannot escape via a relative or symlink-bait extensions root.
- **D11-002 — `sorted_json` recurses into nested objects and arrays** for hash canonicalisation (`src/security/audit.rs:20`). Previously only top-level keys were sorted, so audit-log input hashes diverged for inputs with nested structures. Aligns Rust with the Python and TypeScript canonicalisation contract.

### Added

- **D11-001 — `pub fn set_reserved_group_names(names: &[String])`** module-level setter on `builtin_group` (mirrors TypeScript `setReservedGroupNames`) plus the `ApcliGroup::name()` accessor and `from_cli_config_with_name` / `from_yaml_with_name` / `try_from_yaml_with_name` factory variants. See Security entry above for the full surface.
- **D1-004 — `Sandbox::with_extensions_root(...)` and `Sandbox::with_max_output_bytes(...)` builder methods** (`src/security/sandbox.rs`). Cross-SDK parity with Python `Sandbox.with_extensions_root` / `with_max_output_bytes` (`apcore-cli-python/src/apcore_cli/security/sandbox.py:85,95`). `extensions_root` overrides any inherited `APCORE_EXTENSIONS_ROOT` env var with a canonicalised absolute path; `max_output_bytes` replaces the `SANDBOX_OUTPUT_SIZE_LIMIT_BYTES` constant as the per-instance output cap. Both fields are wired through to `_sandboxed_execute`. 5 new unit tests + 4 new integration tests cover field defaults, single-setter behaviour, fluent chaining, and the disabled-path passthrough invariant.
- **`CliError::SchemaParserFailure { module_id, source }` variant** in `src/cli.rs` — wraps `SchemaParserError::ReservedPropertyName` and `::FlagCollision` so both route to `EXIT_SCHEMA_CIRCULAR_REF` (48) via `CliError::exit_code()`. Previously these errors were re-wrapped as `CliError::InvalidModuleId` and exited with code 2, breaking cross-SDK exit-code parity with Python `sys.exit(48)` and TypeScript `process.exit(EXIT_CODES.SCHEMA_CIRCULAR_REF)`. Audit D11-NEW-005 (see Fixed).
- **`--format markdown` and `--format skill`** for `apcli list` and `apcli describe`
  (issue [aiperceivable/apcore-cli#20](https://github.com/aiperceivable/apcore-cli/issues/20)),
  gated behind the `toolkit` Cargo feature. Both delegate to
  `apcore_toolkit::format_module(s)` (≥0.6) so the output is byte-identical
  to the same toolkit call in the Python and TypeScript SDKs. `--format skill`
  emits vendor-neutral SKILL.md content directly loadable by Claude Code
  (`.claude/skills/<id>/SKILL.md`) and Gemini CLI
  (`.gemini/skills/<id>/SKILL.md`):

  ```bash
  apcore-cli apcli describe users.create --format skill > .claude/skills/users.create/SKILL.md
  ```

  A new internal `descriptor_to_scanned()` helper adapts the registry's JSON
  module-descriptor shape to the toolkit's `ScannedModule` type. When the
  `toolkit` feature is disabled, requesting `markdown` or `skill` logs a
  warning and falls back to `json`.
- **Issue #17 — `system_usage` aggregator + `list --sort calls|errors|latency`**:
  new module `src/system_usage.rs` reads `~/.apcore-cli/audit.jsonl`, filters
  by period (default 24h), and returns per-module aggregates (`calls`,
  `errors`, `avg latency_ms`). `list --sort {calls,errors,latency}` now
  consults the aggregator instead of falling back to id-sort with a buried
  `tracing::warn!`. When the audit log has no entries in the period window
  the discovery layer prints a user-visible note to stderr
  (`note: no usage data available for --sort <field>; sorted by id. ...`)
  and falls back to id-sort. Module-protocol registration of
  `system.usage.summary` / `system.usage.module` as registry-callable
  built-ins is tracked as a follow-up — today the readers are invoked
  directly by the discovery layer.
- New file: `src/system_usage.rs`.
- **Issue #18 + #19 — Rust parity**: new `pub fn create_cli_with(extensions_dir,
  prog_name, host_version, host_description) -> clap::Command` lives in the
  binary entry point (`src/main.rs`) — embedding API is BIN-only in v0.8
  pending the post-D9 redesign. `host_version=Some(v)` overrides
  `-V/--version`; `host_description=Some(d)` overrides the top-level
  `--help` "About" line. **Issue #18 opt-in semantics:** when
  `host_version` is `None`, `--version` is NOT registered — embedded callers
  no longer leak the SDK's own `CARGO_PKG_VERSION`. The standalone
  `apcore-cli` binary explicitly passes
  `Some(env!("CARGO_PKG_VERSION").to_string())` so its `--version` flag
  remains wired. When `host_description` is omitted, the surface defaults to
  `f"{prog_name} CLI"`. Rationale: the embedding API was removed in v0.7.0
  (D9-001/D9-002), but parameterizing the builder now means downstream Rust
  hosts experimenting with `apcore-cli` as a library do not have to fork
  the crate, and the re-introduced embedding API can route through this
  seam without further signature churn.
- **Issue #19 — debrand standalone help strings**: the top-level `--help`
  description, the `apcli` subgroup description, the `--verbose` option
  text, the root `after_help` footer, and the per-module verbose-hint footer
  in `cli.rs` no longer hard-code "apcore" in their phrasing. The
  description defaults to `f"{prog_name} CLI"` (matches TS / Python), and
  the four `(including built-in apcore options)` strings drop the trailing
  `apcore`. Standalone bin still uses the SDK package name for `prog_name`
  by default, so the public `apcore-cli --help` output is unchanged in
  spirit; downstream hosts now get a neutral surface out of the box.
- **D5-002 — Dedicated unit tests for `builtin_group` and `display_helpers`** (`tests/test_builtin_group.rs`, `tests/test_display_helpers.rs`). 10 tests cover `APCLI_SUBCOMMAND_NAMES` / `RESERVED_GROUP_NAMES` constants, all four `from_cli_config` modes, both auto-detect branches, and both `try_from_yaml` validation errors. 6 tests cover display-block extraction, alias precedence chain, and tag fallback chain.
- **D11-NEW-001 / D11-NEW-003 — `ref_resolver` preserves parent sibling `required` in `anyOf`/`oneOf`** (`src/ref_resolver.rs`). `resolve_node` now extracts `sibling_required` from the parent before the branch loop and merges it sibling-first deduplicated with the branch intersection at the end; `merged.properties` is also seeded from the parent (parity with the existing `allOf` branch handling). 3 new regression tests cover `anyOf`, `oneOf`, and dedup overlap. Matches Python `ref_resolver.py:100-118`.
- **`Documented parity gap for the built-in-group rename feature`** in `src/lib.rs` (now superseded by D11-001 above — kept here for the comment block listing the implementation requirements that have since landed).
- **D1-006 — Documented `allowed_prefixes` parity gap** in `src/lib.rs`. TypeScript `createCli` gained `allowedPrefixes` (commit `0f2e08a`); Rust cannot mirror it until the high-level embedding factory (removed in v0.7.0 D9-001/002) returns. The lib-level cross-SDK parity note now records that TypeScript is no longer missing it and Rust is the sole gap.

### Changed

- **D6-W1 — `serde_yaml` replaced with `serde_yaml_ng = "0.10"`** (`Cargo.toml:29`). Upstream `serde_yaml` was deprecated; `serde_yaml_ng` is the maintained drop-in replacement. No API surface change.
- **D6-003 — `apcore` pin policy relaxed** from `=0.21.0` to `0.21` (minor floor), aligning with `apcore-cli-python` (`>=0.21.0`) and `apcore-cli-typescript` (`>=0.21.0`).
- **Dependency bumps** — `nix 0.29 → 0.30.1`, `thiserror 1 → 2.0.18`, `comfy-table 6 → 7.2.2` (transitive: `crossterm 0.26 → 0.29`, `unicode-width 0.1 → 0.2`).
- **`Makefile` `coverage` target** now passes `--fail-under-lines 85` to `cargo llvm-cov`, matching the Python `pyproject.toml` `[tool.coverage.report] fail_under = 85` and the new TypeScript `vitest.config.ts` `thresholds.lines: 85`. Cross-SDK CI parity (audit D5-004).
- **`apcli list` and `apcli describe` `--format` value-parsers** expanded to
  `[table, json, csv, yaml, jsonl, markdown, skill]`. `describe` previously
  accepted only `[table, json]`. Unknown values exit with code 2 (clap
  rejection) as before. Issue
  [aiperceivable/apcore-cli#20](https://github.com/aiperceivable/apcore-cli/issues/20).
- **Dependency bump**: `apcore = "0.21"` (was `=0.19.0`) and the optional
  `apcore-toolkit = "=0.6.0"` (was `=0.5.0`). Aligns with upstream
  `apcore 0.21.0` (`Module::preview` / `PreflightResult::predicted_changes`)
  and `apcore-toolkit 0.6.0` (surface-aware formatters). No CLI-visible
  behavioural breaks.
- **D8-W1 — `Cargo.lock` is now tracked** in git. Per Cargo guidance, the lockfile must be committed for crates that ship a `[[bin]]` target to guarantee reproducible binary builds. The lockfile was previously gitignored.
- **D9-W5 — `register_completion_command` no longer takes `prog_name`** (`src/shell.rs:79`). The parameter was unused; signature now matches the TypeScript `registerCompletionCommand` contract.

### Fixed

- **D11-NEW-005 — `schema_to_clap_args` `Err(SchemaParserError::*)` was mapped to exit code 2**, not 48. The call site in `src/cli.rs:425` previously wrapped both `ReservedPropertyName` and `FlagCollision` as `CliError::InvalidModuleId`, which exits 2. Both are spec-defined exit-48 schema-validity errors per `apcore-cli/docs/features/schema-parser.md` Contract: `schema_to_click_options` Errors (cross-SDK parity with Python `sys.exit(48)` and TS `process.exit(EXIT_CODES.SCHEMA_CIRCULAR_REF)`). Fix routes through the new `CliError::SchemaParserFailure` variant.
- **D9-NEW-002 — `merge_allof` did not deduplicate `required` across branches**. The function concatenated each branch's `required` array via `.extend()`, producing duplicates when two branches independently required the same field name. Spec mandates first-seen-wins dedup (matching TypeScript `[...new Set(...)]` and Python's new explicit seen-set). Fix: replace `.extend()` with a `for item in req { if !merged_required.contains(item) { merged_required.push(...) } }` loop. Outer `obj.required` parent-vs-branches dedup at line 244-251 was already correct.
- **D10-002 — `resolve_refs` exit-code split** (`src/cli.rs:69`). `RefResolverError::Unresolvable` now exits `45` (`EXIT_SCHEMA_REF_UNRESOLVABLE`) while `RefResolverError::Circular` and `RefResolverError::MaxDepthExceeded` exit `48` (`EXIT_SCHEMA_CIRCULAR_REF`). Previously all three collapsed onto a single exit code, breaking cross-SDK parity with Python `sys.exit(45)` / `sys.exit(48)` and the TypeScript `EXIT_CODES.SCHEMA_REF_UNRESOLVABLE` / `SCHEMA_CIRCULAR_REF` split.
- **D10-W1 + D11-W5 — `schema_parser` flag-collision check probes `seen_flags` before inserting the synthetic `--no-X`** (`src/schema_parser.rs:280`), and the collision message now references the original boolean property name instead of the negated form. Cross-SDK message parity.
- **D10-truncated #3 — Clarified `CliApprovalHandler::check_approval` shadow** (`src/approval.rs`). Added a doc-comment disambiguation table covering both `check_approval` overloads (the inherent method that takes `&Value` and is an alias for `request_approval`, and the `apcore::ApprovalHandler` trait impl that takes `&str` and implements the spec's Phase B polling protocol returning `"rejected — CLI does not support async polling"`). The previous comment claimed the inherent method "matches the Python/TypeScript `check_approval` method name", which was misleading. Doc-only change.
- **D11-W1 — `ConfigEncryptor` username fallback chain extended to `USER → LOGNAME → USERNAME`** (`src/security/config_encryptor.rs:233`) for Windows parity with the Python and TypeScript SDKs.
- **D9-W3 — `register_discovery_commands` deleted; `cmd_list` demoted to `pub(crate)`** (`src/discovery.rs:313`). The wrapper had no remaining callers and exposed an internal helper that was never part of the spec'd surface.
- **D10-info-1 — `APCORE_CLI_APCLI` env value is now trimmed before lowercase normalisation** (`src/builtin_group.rs:633`). Spec invariant 2 (`apcore-cli/docs/features/builtin-group.md`) requires the env-var parser to be both case-insensitive and trim-on-read; values like `"  show  "` or `"\thide\n"` now resolve to `"all"`/`"none"` instead of hitting the warn-and-fallthrough branch. Pure-whitespace strings collapse to `"unset"` (parity with the empty-string short-circuit) rather than warning.
- **D11-010 — `AuditLogger` write-failure warnings are deduplicated**. Repeated IO failures against the same `AuditLogger` instance now emit `"Could not write audit log"` at most once; subsequent failures fall through to `trace` level. The dedup flag lives in `Arc<AtomicBool>` so clones share state, matching TypeScript `_writeFailureWarned` and Python `_write_failure_warned` (`src/security/audit.rs:227`).
- **D11-011 — `ExposureFilter` accepts `mode = "none"` silently** (`src/exposure.rs:59`). Python and TypeScript treat `"none"` as a legitimate user-supplied value (hides every module); Rust was warning `"Unknown ExposureFilter mode 'none'"` and clamping back to `"none"`. The end-state was identical, but the spurious warning broke log-noise parity. `"none"` is now in the `VALID_MODES` whitelist; truly unknown modes still warn-and-clamp (fail-closed).

### Removed

- **D9-003 — FE-13 §11.2 root-level deprecation shims**. The 13 hidden
  root-level shim subcommands (`list`, `describe`, `exec`, `validate`,
  `init`, `health`, `usage`, `enable`, `disable`, `reload`, `config`,
  `completion`, `describe-pipeline`) that forwarded to `apcli <name>` with a
  deprecation warning were removed per spec §11.3 ("Removed in v0.8").
  Callers must now use `apcli <name>`. The `DEPRECATED_ROOT_COMMANDS` const,
  `print_deprecation_warning`, `build_apcli_group_for_dispatch`,
  `forward_shim_args`, and `parse_shim_for` helpers in `src/main.rs` were
  deleted along with the registration loop and 13 dispatch arms.
- **D6-002 — `tokio-test = "0.4"` dev-dependency removed**. The crate had zero references across `src/`, `tests/`, and `examples/`; `#[tokio::test]` macros come from tokio's own `macros` feature.
- **D9-W3 — `register_discovery_commands` wrapper removed** from `src/discovery.rs`. See Fixed entry above.

---

## [0.7.0] - 2026-04-25

### Removed

- Removed `run_with_config` and `CliConfig` from the public surface — both
  were stubs and unwired (D9-001, D9-002). `run_with_config` returned 1 with
  a "not yet implemented" message in every branch; `CliConfig` declared
  `commands_dir`, `binding_path`, `group_depth`, `expose`, and `apcli`
  fields that no code path read. The embedding API will be reintroduced
  when actually implemented. `CliConfigError` was removed alongside.
- Removed `EXIT_CONFIG_NAMESPACE_DUPLICATE` constant alias (D9-003) — use
  `EXIT_CONFIG_NAMESPACE_RESERVED` for exit code 78.

### Added

- **Cross-language conformance test** (`tests/conformance_apcli_visibility.rs`) consuming the shared apcli-visibility fixtures from the `aiperceivable/apcore-cli` spec repo (`conformance/fixtures/apcli-visibility/`). One `#[test]` per canonical scenario (`standalone-default`, `embedded-default`, `cli-override`, `env-override`, `yaml-include`). Asserts apcli group visibility and subcommand registration against each fixture's `create_cli.json` / `env.json` / `input.yaml` inputs. A process-global `Mutex` guards scenarios that touch `APCORE_CLI_APCLI` / `cwd`. Byte-matching against `expected_help.txt` is gated behind `#[ignore]` until the canonical clap v4 / GNU-style help formatter is ported — tracked for parity with `apcore-cli-typescript/src/canonical-help.ts`.
- **`APCORE_CLI_SPEC_REPO` env var** — overrides the spec-repo lookup path for conformance fixtures. Defaults to a sibling checkout (`../apcore-cli/`). The test is a no-op (prints a skip notice and returns) when the spec repo is absent.
- New `[[test]]` entry in `Cargo.toml` registering the conformance test binary.
- **FE-12: Module Exposure Filtering** — Declarative control over which discovered modules are exposed as CLI commands.
  - `ExposureFilter` struct in `exposure.rs` with `is_exposed(&self, module_id)` and `filter_modules(&self, ids)` methods.
  - Three modes: `All` (default), `Include` (whitelist), `Exclude` (blacklist) with glob-pattern matching.
  - `ExposureFilter::from_config(value)` constructor for loading from `apcore.yaml` `expose` section.
  - `list --exposure {exposed,hidden,all}` filter flag in discovery commands.
  - `GroupedModuleGroup` integration: applies exposure filter during command registration.
  - `ConfigResolver` gains `expose.*` config keys.
  - 3-tier config precedence: `--expose-mode` CLI flag > env var > `apcore.yaml`.
    (The fourth `CliConfig.expose` tier was removed alongside `CliConfig` —
    see the Removed section above.)
  - Hidden modules remain invocable via `exec <module_id>`.
- New file: `exposure.rs`.

### Fixed

- Correctly propagate executor errors by moving `map_err` inside the `block_in_place` scope.

### Changed

- **CI — spec-repo checkout**: `.github/workflows/ci.yml` now checks out `aiperceivable/apcore-cli` into `.apcore-cli-spec/` and exposes it to `cargo test` via `APCORE_CLI_SPEC_REPO`. Mirrors the pattern in `apcore-cli-python` / `apcore-cli-typescript`.
- **Dependency bump**: requires `apcore = 0.18.0` (was `0.17.1`).
- `MAX_MODULE_ID_LENGTH` updated to 192 (was 128) — `cli.rs` constant `MODULE_ID_MAX_LEN` and
  `validate_module_id` already tracked the upstream spec change.
- `describe-pipeline` rendering updated to build a `StrategyInfo` value (new `apcore 0.18.0`
  type) from preset step data and use its `name` / `step_count` / `step_names` fields for
  display. Header format: `Pipeline: <name> (<n> steps)`.
- `FsDiscoverer::discover` signature updated to `discover(&self, _roots: &[String])` to match the
  new `apcore::registry::Discoverer` trait contract (`discover(roots: &[String])`).
- `Registry::discover(&discoverer)` now returns `usize` (module count) instead of
  `Vec<String>` — updated `main.rs` and `fs_discoverer.rs` tests accordingly.
- `Registry::get_definition` now returns `Option<ModuleDescriptor>` (owned) instead of
  `Option<&ModuleDescriptor>` — removed unnecessary `.cloned()` call in `discovery.rs`.
- Centralized CLI dispatch flags and builtin command definitions to improve maintainability.

---

## [0.6.0] - 2026-04-06

### Changed

- **Dependency bump**: requires `apcore = 0.17.1` (was `0.15.1`). Adds Execution Pipeline Strategy, Config Bus enhancements, Pipeline v2 declarative step metadata, `minimal` strategy preset.
- `CliConfig::group_depth` default changed from 0 to 1 (custom `Default` impl).
- Error tuple in executor path changed to `(i32, String, Option<Value>)` to carry structured error data for FE-11 enhanced error output.

### Added

- **FE-11: Usability Enhancements** — 11 new capabilities:
  - `--dry-run` preflight mode. Standalone `validate` command in `validate.rs` with `format_preflight_result()` and `first_failed_exit_code()`.
  - System management commands: `health`, `usage`, `enable`, `disable`, `reload`, `config get`/`config set` in `system_cmd.rs`. Graceful no-op when system modules unavailable.
  - Enhanced error output: `emit_error_json()` / `emit_error_tty()` with structured guidance fields from `Option<&Value>`.
  - `--trace` pipeline visualization with timing data.
  - `CliApprovalHandler` struct in `approval.rs`. `--approval-timeout`, `--approval-token` flags.
  - `--stream` JSONL output.
  - Enhanced `list` command: `--search`, `--status`, `--annotation`, `--sort`, `--reverse`, `--deprecated`, `--deps`, `--flat`. `ListOptions` struct.
  - `--strategy` selection: `standard`, `internal`, `testing`, `performance`, `minimal`. `describe-pipeline` command in `strategy.rs` with Pure/Removable/Timeout columns.
  - Output format extensions: `--format csv|yaml|jsonl`, `--fields` dot-path field selection. `format_module_list_with_deps()`.
  - Multi-level grouping: `CliConfig::group_depth`.
  - Custom command extension: `CliConfig::extra_commands: Vec<clap::Command>`.
- New error code constant: `EXIT_CONFIG_ENV_MAP_CONFLICT`.
- New files: `system_cmd.rs`, `strategy.rs`, `validate.rs`.
- `BUILTIN_COMMANDS` expanded to 14 entries. `KNOWN_BUILTINS` in `shell.rs` updated to match.
- `RESERVED_FLAG_NAMES` expanded with all FE-11 flag names.

---

## [0.5.1] - 2026-04-03

### Added
- **Pre-populated registry support** — `CliConfig` struct with optional `registry` (pre-populated `RegistryProvider`) and `executor` (pre-built `ModuleExecutor`) fields. When provided, downstream binaries can skip filesystem discovery entirely. This enables frameworks that register modules at runtime (e.g. apflow's bridge) to generate CLI commands from their existing registry.
- `CliConfig` exported from crate root with `Default` impl.

---

## [0.4.0] - 2026-03-29

### Added
- **Verbose help mode** — Built-in apcore options (`--input`, `--yes`, `--large-input`, `--format`, `--sandbox`) are now hidden from `--help` output by default. Pass `--help --verbose` to display the full option list including built-in options.
- **Universal man page generation** — `build_program_man_page()` generates a complete roff man page covering all registered commands. `--help --man` outputs the man page, enabling downstream projects to get man pages for free.
- **Documentation URL support** — `set_docs_url()` sets a base URL for online docs. Per-command help shows `Docs: {url}/commands/{name}`, man page SEE ALSO includes `Full documentation at {url}`. No default — disabled when not set.

### Changed
- `build_module_command_with_limit()` and `add_dispatch_flags()` respect the global verbose help flag to control built-in option visibility.
- `--sandbox` is now always hidden from help (not yet implemented). Only four built-in options (`--input`, `--yes`, `--large-input`, `--format`) toggle with `--verbose`.
- Improved built-in option descriptions for clarity.

## [0.3.0] - 2026-03-27

### Added
- **Grouped CLI commands (FE-09)** — `GroupedModuleGroup` organizes modules into nested subcommand groups by namespace prefix, enabling `apcore-cli <group> <command>` invocation.
- **Display overlay helpers** — `get_display()` and `get_cli_display_fields()` resolve alias, description, and tags from `metadata["display"]`.
- **Init command (FE-10)** — `apcore-cli init module <id>` scaffolds new modules with `--style` (decorator/convention/binding), `--dir`, and `--description` options.
- **Grouped shell completions** — Bash, Zsh, and Fish completions now support two-level group/command completion via `_APCORE_GRP`.
- **Optional apcore-toolkit integration** — `DisplayResolver` and `RegistryWriter` via `toolkit` feature flag with graceful fallback.
- **Path traversal validation** — `--dir` rejects paths containing `..` components.

### Changed
- `BUILTIN_COMMANDS` updated to include `init` (6 items, sorted).
- `APCORE_AUTH_API_KEY` added to man page ENVIRONMENT section.
- Dependency bump: `apcore >= 0.14`.

## [0.2.2] - 2026-03-22

### Changed
- Rebrand: aipartnerup → aiperceivable

## [0.2.1] - 2026-03-19

### Changed
- Help text truncation limit increased from 200 to 1000 characters (`HELP_TEXT_MAX_LEN` constant)
- `cli.help_text_max_length` config key added to `ConfigResolver::DEFAULTS` (default: 1000)
- `logging.level` default changed from `"INFO"` to `"WARNING"` in `ConfigResolver::DEFAULTS` — aligns with Python/TypeScript SDKs and updated spec

### Added
- `extract_help_with_limit` — configurable-limit variant of `extract_help` (`schema_parser.rs`)
- `schema_to_clap_args_with_limit` — configurable-limit variant of `schema_to_clap_args` (`schema_parser.rs`)
- `build_module_command_with_limit` — accepts `help_text_max_length` parameter (`cli.rs`)
- `HELP_TEXT_MAX_LEN` constant exported from crate root (`lib.rs`)
- Test: `test_extract_help_truncates_at_1000`
- Test: `test_extract_help_no_truncation_within_limit`
- Test: `test_extract_help_custom_max_length`
- Test: `test_help_truncated_at_1000_chars` (integration)
- Test: `test_help_within_limit_not_truncated` (integration)
- 459 tests (up from 458)

## [0.2.0] - 2026-03-18

### Added

**Core Features (ported from apcore-cli-python 0.2.0)**

- **ConfigResolver** — 4-tier configuration precedence (CLI flag > env var > YAML file > defaults)
- **Core Dispatcher** — `validate_module_id`, `collect_input` (STDIN + CLI merge, 10MiB limit), `LazyModuleGroup` (lazy command cache), `build_module_command` (schema-to-clap), `dispatch_module` (full execution pipeline with SIGINT handling)
- **Schema Parser** — `schema_to_clap_args` converting JSON Schema to clap `Arg` instances, boolean flag pairs (`--flag`/`--no-flag`), enum choices with `PossibleValuesParser`, `reconvert_enum_values` for type coercion, `extract_help` with 200-char truncation
- **Ref Resolver** — `resolve_refs` with `$ref` inlining, `allOf` merge, `anyOf`/`oneOf` intersection, depth limit (32), circular detection
- **Output Formatter** — TTY-adaptive rendering (`comfy-table` for terminals, JSON for pipes), `format_module_list`, `format_module_detail`, `format_exec_result`, `resolve_format`, `truncate`
- **Discovery** — `list` command with AND tag filtering, `describe` command with exit-44 on not found, `RegistryProvider` trait, `ApCoreRegistryProvider` adapter
- **Approval Gate** — TTY-aware HITL prompts, `--yes` and `APCORE_CLI_AUTO_APPROVE=1` bypass, 60s `tokio::select!` timeout, `NonInteractive` error for non-TTY, all variants exit 46
- **Shell Integration** — `completion` command (bash/zsh/fish/elvish/powershell via `clap_complete`), `man` command (roff format with EXIT CODES and ENVIRONMENT sections)
- **Security** — `AuthProvider` (env/config/keyring with Bearer header), `ConfigEncryptor` (AES-256-GCM + PBKDF2, keyring fallback), `AuditLogger` (JSONL append, salted SHA-256 input hash), `Sandbox` (tokio subprocess, env whitelist, 300s timeout)

**Dispatch & Execution**

- `exec` subcommand — first-class clap subcommand for module execution
- External subcommand routing — `apcore-cli math.add --a 5` routes through `dispatch_module`
- Schema-derived flags — external subcommands look up module descriptor to build `--a`, `--b` etc. from `input_schema`
- `FsDiscoverer` — recursively scans extensions directory for `module.json` descriptors
- Script-based execution — modules with `run.sh` next to `module.json` execute as subprocesses (JSON stdin/stdout protocol)
- Path-traversal validation — executable paths canonicalized and verified to stay within extensions root

**Examples**

- 8 example modules: `math.add`, `math.multiply`, `text.upper`, `text.reverse`, `text.wordcount`, `sysutil.info`, `sysutil.env`, `sysutil.disk`
- Each module has `module.json` (descriptor) + `run.sh` (execution script)
- `examples/run_examples.sh` — runs all 15 demo scenarios
- `examples/README.md` — module authoring guide

**Developer Experience**

- `Makefile` with `setup`, `build`, `check` (fmt + clippy + tests), `clean` targets
- `.bin/` local binary directory to avoid PATH conflict with Python `apcore-cli`
- Pre-commit hook (fmt, clippy, check-chars)
- 458 tests across 17 test files, 0 failures
- `cargo clippy --all-targets --all-features -- -D warnings` clean

**Infrastructure**

- 10 exit codes matching the apcore protocol (0, 1, 2, 44, 45, 46, 47, 48, 77, 130)
- `add_dispatch_flags()` shared helper for exec and external subcommand flags
- `test-support` cargo feature for gating test utilities (`MockRegistry`, `mock_module`)
- Unified `RegistryProvider` trait (consolidated from separate `ModuleRegistry` + `RegistryProvider`)

### Dependencies

- `apcore` 0.13.0
- `clap` 4 (derive + env + string)
- `tokio` 1 (rt-multi-thread, macros, time, process, io-util, io-std, signal)
- `serde` + `serde_json` + `serde_yaml` 0.9
- `comfy-table` 6
- `aes-gcm` 0.10 + `sha2` 0.10 + `pbkdf2` 0.12
- `keyring` 2
- `clap_complete` 4
- `thiserror` 1 + `anyhow` 1
- `tracing` 0.1 + `tracing-subscriber` 0.3
- `reqwest` 0.12
- `async-trait` 0.1
- `base64` 0.22, `gethostname` 0.4, `chrono` 0.4, `dirs` 5, `tempfile` 3