apcore-cli 0.6.0__tar.gz → 0.8.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. apcore_cli-0.8.0/.github/workflows/ci.yml +62 -0
  2. {apcore_cli-0.6.0 → apcore_cli-0.8.0}/.gitignore +1 -0
  3. apcore_cli-0.8.0/CHANGELOG.md +455 -0
  4. apcore_cli-0.8.0/CLAUDE.md +60 -0
  5. apcore_cli-0.8.0/LICENSE +193 -0
  6. apcore_cli-0.8.0/Makefile +30 -0
  7. {apcore_cli-0.6.0 → apcore_cli-0.8.0}/PKG-INFO +219 -67
  8. apcore_cli-0.8.0/README.md +593 -0
  9. apcore_cli-0.8.0/examples/README.md +121 -0
  10. {apcore_cli-0.6.0 → apcore_cli-0.8.0}/examples/run_examples.sh +33 -33
  11. {apcore_cli-0.6.0 → apcore_cli-0.8.0}/planning/approval-gate.md +3 -1
  12. apcore_cli-0.8.0/planning/exposure-filtering.md +236 -0
  13. {apcore_cli-0.6.0 → apcore_cli-0.8.0}/planning/state.json +18 -1
  14. {apcore_cli-0.6.0 → apcore_cli-0.8.0}/pyproject.toml +12 -2
  15. apcore_cli-0.8.0/src/apcore_cli/__init__.py +196 -0
  16. apcore_cli-0.8.0/src/apcore_cli/__main__.py +77 -0
  17. {apcore_cli-0.6.0 → apcore_cli-0.8.0}/src/apcore_cli/approval.py +46 -14
  18. apcore_cli-0.8.0/src/apcore_cli/builtin_group.py +467 -0
  19. {apcore_cli-0.6.0 → apcore_cli-0.8.0}/src/apcore_cli/cli.py +201 -151
  20. {apcore_cli-0.6.0 → apcore_cli-0.8.0}/src/apcore_cli/config.py +67 -8
  21. apcore_cli-0.8.0/src/apcore_cli/discovery.py +471 -0
  22. apcore_cli-0.8.0/src/apcore_cli/exit_codes.py +76 -0
  23. apcore_cli-0.8.0/src/apcore_cli/exposure.py +145 -0
  24. apcore_cli-0.8.0/src/apcore_cli/factory.py +708 -0
  25. {apcore_cli-0.6.0 → apcore_cli-0.8.0}/src/apcore_cli/init_cmd.py +50 -19
  26. {apcore_cli-0.6.0 → apcore_cli-0.8.0}/src/apcore_cli/output.py +111 -15
  27. apcore_cli-0.8.0/src/apcore_cli/ref_resolver.py +146 -0
  28. {apcore_cli-0.6.0 → apcore_cli-0.8.0}/src/apcore_cli/schema_parser.py +50 -0
  29. {apcore_cli-0.6.0 → apcore_cli-0.8.0}/src/apcore_cli/security/audit.py +15 -2
  30. apcore_cli-0.8.0/src/apcore_cli/security/auth.py +74 -0
  31. apcore_cli-0.8.0/src/apcore_cli/security/config_encryptor.py +166 -0
  32. apcore_cli-0.8.0/src/apcore_cli/security/sandbox.py +271 -0
  33. {apcore_cli-0.6.0 → apcore_cli-0.8.0}/src/apcore_cli/shell.py +16 -3
  34. apcore_cli-0.8.0/src/apcore_cli/strategy.py +215 -0
  35. {apcore_cli-0.6.0 → apcore_cli-0.8.0}/src/apcore_cli/system_cmd.py +239 -61
  36. apcore_cli-0.8.0/src/apcore_cli/system_usage.py +167 -0
  37. apcore_cli-0.8.0/src/apcore_cli/validate.py +100 -0
  38. apcore_cli-0.8.0/tests/conformance/test_apcli_visibility.py +300 -0
  39. apcore_cli-0.8.0/tests/test_apcli_integration.py +548 -0
  40. apcore_cli-0.8.0/tests/test_approval.py +333 -0
  41. {apcore_cli-0.6.0 → apcore_cli-0.8.0}/tests/test_bugfixes.py +7 -6
  42. apcore_cli-0.8.0/tests/test_builtin_group.py +381 -0
  43. {apcore_cli-0.6.0 → apcore_cli-0.8.0}/tests/test_cli.py +446 -28
  44. {apcore_cli-0.6.0 → apcore_cli-0.8.0}/tests/test_config.py +73 -9
  45. {apcore_cli-0.6.0 → apcore_cli-0.8.0}/tests/test_discovery.py +144 -0
  46. apcore_cli-0.8.0/tests/test_discovery_fe13.py +279 -0
  47. apcore_cli-0.8.0/tests/test_display_helpers.py +21 -0
  48. apcore_cli-0.8.0/tests/test_exit_codes.py +58 -0
  49. apcore_cli-0.8.0/tests/test_exposure.py +180 -0
  50. apcore_cli-0.8.0/tests/test_factory_fe13.py +243 -0
  51. apcore_cli-0.8.0/tests/test_init_cmd.py +159 -0
  52. {apcore_cli-0.6.0 → apcore_cli-0.8.0}/tests/test_integration.py +10 -6
  53. apcore_cli-0.8.0/tests/test_list_command_filters.py +166 -0
  54. {apcore_cli-0.6.0 → apcore_cli-0.8.0}/tests/test_output.py +51 -0
  55. apcore_cli-0.8.0/tests/test_output_format_exec.py +99 -0
  56. apcore_cli-0.8.0/tests/test_output_format_markdown_skill.py +126 -0
  57. apcore_cli-0.8.0/tests/test_public_api.py +92 -0
  58. apcore_cli-0.8.0/tests/test_ref_resolver.py +345 -0
  59. apcore_cli-0.8.0/tests/test_sandbox_runner.py +97 -0
  60. {apcore_cli-0.6.0 → apcore_cli-0.8.0}/tests/test_schema_parser.py +100 -5
  61. apcore_cli-0.8.0/tests/test_security/__init__.py +0 -0
  62. apcore_cli-0.8.0/tests/test_security/test_audit.py +195 -0
  63. {apcore_cli-0.6.0 → apcore_cli-0.8.0}/tests/test_security/test_auth.py +13 -0
  64. apcore_cli-0.8.0/tests/test_security/test_config_encryptor.py +196 -0
  65. apcore_cli-0.8.0/tests/test_security/test_sandbox.py +332 -0
  66. apcore_cli-0.8.0/tests/test_strategy.py +252 -0
  67. apcore_cli-0.8.0/tests/test_system_cmd.py +462 -0
  68. apcore_cli-0.8.0/tests/test_system_usage.py +257 -0
  69. apcore_cli-0.8.0/tests/test_toolkit_integration.py +337 -0
  70. apcore_cli-0.8.0/tests/test_validate.py +227 -0
  71. apcore_cli-0.6.0/.github/workflows/ci.yml +0 -46
  72. apcore_cli-0.6.0/CHANGELOG.md +0 -225
  73. apcore_cli-0.6.0/CLAUDE.md +0 -30
  74. apcore_cli-0.6.0/README.md +0 -446
  75. apcore_cli-0.6.0/commands/ops.py +0 -4
  76. apcore_cli-0.6.0/src/apcore_cli/__init__.py +0 -35
  77. apcore_cli-0.6.0/src/apcore_cli/__main__.py +0 -375
  78. apcore_cli-0.6.0/src/apcore_cli/discovery.py +0 -221
  79. apcore_cli-0.6.0/src/apcore_cli/ref_resolver.py +0 -113
  80. apcore_cli-0.6.0/src/apcore_cli/security/auth.py +0 -37
  81. apcore_cli-0.6.0/src/apcore_cli/security/config_encryptor.py +0 -94
  82. apcore_cli-0.6.0/src/apcore_cli/security/sandbox.py +0 -60
  83. apcore_cli-0.6.0/src/apcore_cli/strategy.py +0 -135
  84. apcore_cli-0.6.0/tests/test_approval.py +0 -158
  85. apcore_cli-0.6.0/tests/test_init_cmd.py +0 -82
  86. apcore_cli-0.6.0/tests/test_ref_resolver.py +0 -191
  87. apcore_cli-0.6.0/tests/test_security/test_audit.py +0 -97
  88. apcore_cli-0.6.0/tests/test_security/test_config_encryptor.py +0 -56
  89. apcore_cli-0.6.0/tests/test_security/test_sandbox.py +0 -50
  90. {apcore_cli-0.6.0 → apcore_cli-0.8.0}/.github/CODEOWNERS +0 -0
  91. {apcore_cli-0.6.0 → apcore_cli-0.8.0}/.github/copilot-ignore +0 -0
  92. {apcore_cli-0.6.0 → apcore_cli-0.8.0}/.gitmessage +0 -0
  93. {apcore_cli-0.6.0 → apcore_cli-0.8.0}/.pre-commit-config.yaml +0 -0
  94. {apcore_cli-0.6.0 → apcore_cli-0.8.0}/examples/extensions/math/add.py +0 -0
  95. {apcore_cli-0.6.0 → apcore_cli-0.8.0}/examples/extensions/math/multiply.py +0 -0
  96. {apcore_cli-0.6.0 → apcore_cli-0.8.0}/examples/extensions/sysutil/disk.py +0 -0
  97. {apcore_cli-0.6.0 → apcore_cli-0.8.0}/examples/extensions/sysutil/env.py +0 -0
  98. {apcore_cli-0.6.0 → apcore_cli-0.8.0}/examples/extensions/sysutil/info.py +0 -0
  99. {apcore_cli-0.6.0 → apcore_cli-0.8.0}/examples/extensions/text/reverse.py +0 -0
  100. {apcore_cli-0.6.0 → apcore_cli-0.8.0}/examples/extensions/text/upper.py +0 -0
  101. {apcore_cli-0.6.0 → apcore_cli-0.8.0}/examples/extensions/text/wordcount.py +0 -0
  102. {apcore_cli-0.6.0 → apcore_cli-0.8.0}/planning/config-resolver.md +0 -0
  103. {apcore_cli-0.6.0 → apcore_cli-0.8.0}/planning/core-dispatcher.md +0 -0
  104. {apcore_cli-0.6.0 → apcore_cli-0.8.0}/planning/discovery.md +0 -0
  105. {apcore_cli-0.6.0 → apcore_cli-0.8.0}/planning/grouped-commands.md +0 -0
  106. {apcore_cli-0.6.0 → apcore_cli-0.8.0}/planning/output-formatter.md +0 -0
  107. {apcore_cli-0.6.0 → apcore_cli-0.8.0}/planning/overview.md +0 -0
  108. {apcore_cli-0.6.0 → apcore_cli-0.8.0}/planning/schema-parser.md +0 -0
  109. {apcore_cli-0.6.0 → apcore_cli-0.8.0}/planning/security-manager.md +0 -0
  110. {apcore_cli-0.6.0 → apcore_cli-0.8.0}/planning/shell-integration.md +0 -0
  111. {apcore_cli-0.6.0 → apcore_cli-0.8.0}/src/apcore_cli/_sandbox_runner.py +0 -0
  112. {apcore_cli-0.6.0 → apcore_cli-0.8.0}/src/apcore_cli/display_helpers.py +0 -0
  113. {apcore_cli-0.6.0 → apcore_cli-0.8.0}/src/apcore_cli/security/__init__.py +0 -0
  114. {apcore_cli-0.6.0 → apcore_cli-0.8.0}/tests/__init__.py +0 -0
  115. {apcore_cli-0.6.0/tests/test_security → apcore_cli-0.8.0/tests/conformance}/__init__.py +0 -0
  116. {apcore_cli-0.6.0 → apcore_cli-0.8.0}/tests/conftest.py +0 -0
  117. {apcore_cli-0.6.0 → apcore_cli-0.8.0}/tests/test_e2e.py +0 -0
  118. {apcore_cli-0.6.0 → apcore_cli-0.8.0}/tests/test_shell.py +0 -0
@@ -0,0 +1,62 @@
1
+ name: CI
2
+
3
+ permissions:
4
+ contents: read
5
+
6
+ on:
7
+ push:
8
+ branches: [ "main" ]
9
+ pull_request:
10
+ branches: [ "main" ]
11
+
12
+ jobs:
13
+ test:
14
+ name: Test on Python ${{ matrix.python-version }}
15
+ runs-on: ubuntu-latest
16
+ strategy:
17
+ fail-fast: false
18
+ matrix:
19
+ # Pin CI to a single, consistent Python version to reduce version-related
20
+ # test flakiness. Use 3.12 for CI stability; expand matrix later if desired.
21
+ python-version: ["3.12"]
22
+
23
+ steps:
24
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
25
+
26
+ - name: Checkout apcore-cli spec repo (conformance fixtures)
27
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
28
+ with:
29
+ repository: aiperceivable/apcore-cli
30
+ path: .apcore-cli-spec
31
+
32
+ - name: Set up Python
33
+ uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
34
+ with:
35
+ python-version: ${{ matrix.python-version }}
36
+
37
+ - name: Install uv
38
+ uses: astral-sh/setup-uv@v5
39
+
40
+ # `--extra dev` pulls pytest, ruff, pre-commit, mypy, apdev; `--extra toolkit`
41
+ # brings in apcore-toolkit >= 0.5.0. apcore >= 0.19.0 comes from the main
42
+ # `dependencies` section and is installed unconditionally. Toolkit must be
43
+ # importable for tests/test_toolkit_integration.py (which uses
44
+ # `patch("apcore_toolkit.BindingLoader", ...)` — patch resolves the target
45
+ # at import time and fails if the module is absent).
46
+ - name: Install dependencies (apcore, apcore-toolkit, dev extras)
47
+ run: |
48
+ uv sync --extra dev --extra toolkit
49
+
50
+ # Single pre-commit invocation covers ruff lint + ruff-format + apdev
51
+ # (check-chars, check-imports). Matches the TypeScript CI pattern at
52
+ # ../apcore-cli-typescript/.github/workflows/ci.yml which likewise runs
53
+ # `pre-commit run --all-files` as the lint gate.
54
+ - name: Run pre-commit (ruff + apdev hooks)
55
+ run: |
56
+ uv run pre-commit run --all-files
57
+
58
+ - name: Run tests
59
+ env:
60
+ APCORE_CLI_SPEC_REPO: ${{ github.workspace }}/.apcore-cli-spec
61
+ run: |
62
+ uv run pytest
@@ -16,3 +16,4 @@ venv/
16
16
  env/
17
17
  .env
18
18
  .forge
19
+ .mypy_cache/
@@ -0,0 +1,455 @@
1
+ # Changelog
2
+
3
+ All notable changes to apcore-cli (Python SDK) will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+
9
+ ## [0.8.0] - 2026-05-08
10
+
11
+ ### Removed
12
+
13
+ - **D9-001 — FE-13 §11.2 deprecation shims removed**. The 13 hidden root-level
14
+ shims (`list`, `describe`, `exec`, `init`, `validate`, `health`, `usage`,
15
+ `enable`, `disable`, `reload`, `config`, `completion`, `describe-pipeline`)
16
+ installed by `_register_deprecation_shims` and the `__is_deprecation_shim__`
17
+ collision-handling path in `extra_commands` wiring have been deleted along
18
+ with the `_DEPRECATED_ROOT_COMMANDS` table. Use the canonical
19
+ `apcli <command>` paths instead. Calls like `apcore-cli list` now exit
20
+ non-zero with Click's "No such command" message — the warning window
21
+ documented as "removed in v0.8" is closed.
22
+
23
+ ### Deprecated
24
+
25
+ - **`CliModuleNotFoundError` alias** — the symbol still resolves to
26
+ `ModuleNotFoundError` (see D1-002 in Changed) but is scheduled for
27
+ removal in v0.10.0. Update imports to
28
+ `from apcore_cli import ModuleNotFoundError`.
29
+
30
+ ### Security
31
+
32
+ - **D10-001 — `Sandbox` per-stream output cap** (`sandbox.py:155`). The previous
33
+ implementation summed `stdout + stderr` against a single `max_output_bytes`
34
+ budget — a runaway child writing only to stderr could starve the stdout
35
+ budget and vice versa, and the diagnostic on overflow did not name the
36
+ offending stream. Each stream now has an independent byte budget matching
37
+ Rust and TypeScript; the overflow error names the stream that tripped the
38
+ cap.
39
+ - **D11-W2 — `Sandbox` switched from `subprocess.run` to `subprocess.Popen`
40
+ with threaded chunked reads** (`sandbox.py:155`). `capture_output=True`
41
+ buffered the entire child stdio into parent memory before the cap was
42
+ checked, so a child producing GBs of output could OOM the parent before
43
+ the limit was enforced. The new implementation streams stdout/stderr
44
+ through reader threads with bounded buffers and kills the child as soon
45
+ as either stream exceeds its cap. Memory consumption is now bounded by
46
+ `2 × max_output_bytes` regardless of child output volume.
47
+ - **D11-003 — `ConfigEncryptor` v1 decryption honours
48
+ `APCORE_CLI_CONFIG_PASSPHRASE`** (`config_encryptor.py:128`). `_aes_decrypt_v1`
49
+ hard-coded the host:user material, so v1 ciphertext encrypted by the Rust
50
+ or TypeScript SDKs under a passphrase failed to decrypt on Python.
51
+ Decryption now tries the passphrase-derived key first when the env var is
52
+ set, falling back to host:user material — matching TypeScript
53
+ `aesDecryptV1`. Cross-SDK config bundles are now portable.
54
+ - **D11-008 — `AuditLogger._get_user` fallback chain now includes `LOGNAME`**
55
+ (`audit.py:66`). The canonical chain per `security.md` (D11-W1) is
56
+ `getlogin → pwd.getpwuid → USER → LOGNAME → USERNAME → unknown`. Python
57
+ previously skipped `LOGNAME`, so audit-log `user` fields diverged from
58
+ Rust/TS on hosts where only `LOGNAME` is set (some container runtimes,
59
+ cron jobs).
60
+
61
+ ### Added
62
+
63
+ - **`builtin_group_name="apcli"` kwarg on `create_cli`** — downstream branded CLIs that embed apcore-cli can now expose the built-in commands under a custom namespace (e.g. `mycorp-cli admin health` instead of `mycorp-cli apcli health`). `ApcliGroup` gains a `name` parameter (with property accessor) threaded through `from_cli_config` / `from_yaml` / `_build`. Default `"apcli"` is unchanged. Validated against `/^[a-z][a-z0-9_-]*$/`; invalid values exit 2. `RESERVED_GROUP_NAMES` collision check now consults `GroupedModuleGroup._reserved_group_names` (instance attribute, defaults to the static frozenset; factory replaces with the resolved name). Env var `APCORE_CLI_APCLI` and config keys `apcli.*` deliberately do NOT rename — they are apcore-cli-internal toggles, not user-facing. Cross-SDK parity with TypeScript `createCli({ builtinGroupName })`. New `DEFAULT_BUILTIN_GROUP_NAME` constant exported from `apcore_cli.builtin_group`.
64
+ - **`_exit_on_system_error(e)` helper in `system_cmd.py`** — centralizes the canonical error→exit-code mapping for system-management subcommands, replacing 7 sites that previously used bare `sys.exit(1)` (audit D11-B-002, see Fixed).
65
+ - **5 new tests in `tests/test_builtin_group.py`** — `TestBuiltinGroupRename` class covers default name, custom name via both factories, validation of valid/invalid name shapes (5 valid + 6 invalid forms each).
66
+ - **D1-001 — 13 `register_*_command` factories + `configure_man_help`
67
+ re-exported from `apcore_cli` package root**. Embedders that compose
68
+ their own root command tree no longer need to reach into private
69
+ submodules (`apcore_cli.commands.list_cmd`, etc.). All TS/Rust
70
+ `register_*` counterparts now have a Python public-API equivalent.
71
+ - **D1-003 — `apcore_cli.exit_codes` module** with 24 `EXIT_*` integer
72
+ constants, an `EXIT_CODES` mapping dict, and an `exit_code_for_error()`
73
+ helper. Mirrors TS `errors.ts` `EXIT_CODES` + `exitCodeForError` and
74
+ Rust `src/lib.rs` `EXIT_*` constants. Embedders can now map exceptions
75
+ to documented exit codes without re-implementing the table.
76
+ - **D1-007 — `format_module_list`, `format_module_detail`,
77
+ `resolve_format` re-exported from package root**. The
78
+ output-formatter feature spec declares these as Contracts; previously
79
+ only `format_exec_result` was public.
80
+ - **D1-W1 — `APCLI_SUBCOMMAND_NAMES` re-exported from `apcore_cli`**.
81
+ Matches Rust `lib.rs` and is now in `__all__` for static-analysis
82
+ tooling.
83
+ - **D1-W2 — `ApcliConfig` TypedDict** added to the public surface,
84
+ mirroring the TypeScript type alias and Rust struct so embedders have
85
+ a static contract for the `apcli.*` config block.
86
+ - **D1-W3 — `register_config_namespace()` helper + module-level
87
+ `DEFAULTS` constant** in `config.py`. The package still registers the
88
+ namespace at import time, but embedders can now invoke the helper
89
+ explicitly (parity with `apcore-cli-typescript`).
90
+ - **D1-W5 — Core dispatcher embedder API re-exported from package
91
+ root**: `build_module_command`, `collect_input`, `validate_module_id`,
92
+ `set_audit_logger`, `set_verbose_help`, `set_docs_url`. Embedders no
93
+ longer have to import from `apcore_cli.cli` directly. Matches Rust
94
+ `lib.rs:186-190` and TS `index.ts:18`. New `tests/test_public_api.py`
95
+ pins the surface against future drift.
96
+ - **D1-info-1 — typed `ApcliGroupError` exception**
97
+ (`builtin_group.py:107`). Cross-SDK parity with Rust `ApcliGroupError`;
98
+ embedders previously had no stable error class to match on for
99
+ built-in-group config validation. `ApcliGroupError(ValueError)`
100
+ preserves backwards compat — existing `except ValueError` callers
101
+ still catch it. The invalid-name regex check in `__init__` now raises
102
+ `ApcliGroupError`. Re-exported from `apcore_cli`.
103
+
104
+ ### Fixed
105
+
106
+ - **D11-B-006 — `discovery.py:208` sort direction inverted**. `apcli list --sort calls|errors|latency` now defaults to DESCENDING (highest call count first) per spec T-LST-04, matching Rust `discovery.rs:209` and TypeScript `discovery.ts:186`. Previously the user's raw `--reverse` flag (default False) was passed directly to `sort_modules_by_usage(..., reverse=...)`, producing ASCENDING output by default — the inverse of the spec. Fix passes `reverse=not reverse` for the data path AND adds a re-sort at the call site for the audit-log-empty fallback so id-fallback continues to default ASCENDING per spec.
107
+ - **D11-B-002 — `system_cmd.py` collapsed every error to exit 1**. The 7 `except Exception as e: sys.exit(1)` sites bypassed Python's own `_ERROR_CODE_MAP` (canonical 44/46/47/77) — scripted operators could not distinguish "module not found" from "ACL denied" from generic failure. All 7 sites now route through the new `_exit_on_system_error(e)` helper which calls `exit_code_for_error(e)` from `apcore_cli.exit_codes`. The 4 audit-log entries previously hardcoding `exit_code=1` now log the resolved code.
108
+ - **D11-NEW-005 — RESERVED_PROPERTY_NAMES no longer raises generic `ValueError`**. `schema_to_click_options` previously raised `ValueError` when a schema property collided with a built-in CLI option — opaque to scripted callers and inconsistent with the neighbour flag-collision branch (which already exited 48). Now writes a user-facing `Error:` line to stderr and calls `sys.exit(48)` per spec, matching TS `process.exit(EXIT_CODES.SCHEMA_CIRCULAR_REF)` and Rust `CliError::SchemaParserFailure → EXIT_SCHEMA_CIRCULAR_REF`. Tests tightened from `pytest.raises((ValueError, Exception))` to `pytest.raises(SystemExit)` with `code == 48` assertion.
109
+ - **D9-NEW-002 — `ref_resolver.py` `allOf required` not deduplicated**. `_resolve_node`'s `allOf` branch concatenated parent `required` + each branch's `required` without dedup, producing duplicate entries in the merged schema's `required` array. JSON Schema validators ignore duplicates so observable validation behaviour was unchanged, but cross-SDK byte-comparison tooling (and the `anyOf`/`oneOf` paths, which already deduped) flagged the divergence. Fix: explicit seen-set dedup preserving first-seen order, matching TS `[...new Set(...)]` and Rust `merge_allof`.
110
+ - **D10-003 — `build_module_command` leaked `RefResolverError`
111
+ tracebacks** (`cli.py:538`). The `resolve_refs` catch clause re-raised
112
+ unchanged, so callers saw a Python traceback instead of a clean
113
+ documented exit code. Now translates `CircularRefError` /
114
+ `MaxDepthExceededError` to `sys.exit(48)` and `UnresolvableRefError`
115
+ (plus generic `RefResolverError`) to `sys.exit(45)`, mirroring
116
+ `schema_parser.py:111` and the Rust/TS contracts.
117
+ - **D11-NEW-003 — `ref_resolver` `max_depth` over-counted plain nested
118
+ `properties`** (`ref_resolver.py`). `_resolve_node` previously
119
+ incremented `depth + 1` when recursing into nested `properties`
120
+ values, so a schema with >32 levels of nested objects (no `$ref` at
121
+ all) was rejected with `MaxDepthExceededError`. The spec wording is
122
+ "Maximum `$ref` resolution recursion depth" — `$ref` hops along a
123
+ single chain, not total stack depth. `depth` is now only incremented
124
+ on `$ref` traversal, aligning with Rust `ref_resolver.rs:297`. Also
125
+ adds 4 regression tests for `anyOf`/`oneOf` sibling-required
126
+ preservation and `anyOf` overlap dedup.
127
+ - **D10-info-1 — `APCORE_CLI_APCLI` env var not trimmed on read**
128
+ (`builtin_group.py:414`). Spec invariant 2 requires the parser to be
129
+ case-insensitive AND trim-on-read. Surrounding whitespace previously
130
+ caused a silent Tier-3/Tier-4 fall-through. Now strips before
131
+ lowercasing, matching Rust/TS.
132
+ - **D11-010 — `AuditLogger` write-failure warnings deduplicated**
133
+ (`audit.py:55`). Previously warned on every failed write, flooding
134
+ logs when an audit dir is unwritable. An instance flag now gates the
135
+ warning so it fires once per logger instance, matching the TS
136
+ `writeFailureWarned` flag.
137
+
138
+ ### Changed
139
+
140
+ - **`apcli system *` and `apcli strategy describe-pipeline` `--format` choices**
141
+ expanded from `[table, json]` to `[table, json, csv, yaml, jsonl]`, matching
142
+ the existing `apcli list` / `apcli exec` choice set. `markdown` and `skill`
143
+ are deliberately excluded from these subcommands — their payloads are
144
+ health / strategy results, not `ScannedModule` data. Issue
145
+ [#20](https://github.com/aiperceivable/apcore-cli/issues/20).
146
+ - **Dependency bump**: requires `apcore >= 0.21.0` (was `>= 0.19.0`) and the
147
+ optional `[toolkit]` extra now requires `apcore-toolkit >= 0.6` (was `>= 0.5`).
148
+ Aligns with upstream `apcore 0.21.0` (Module.preview / PreflightResult.predicted_changes,
149
+ ephemeral.* namespace pilot) and `apcore-toolkit 0.6.0` (surface-aware formatters).
150
+ No CLI-visible behavioural breaks — apcore 0.20→0.21 deprecations
151
+ (`TaskStore.put`/`save`, `TaskStatus.RETRYING`, `CircuitOpenError`) keep
152
+ legacy aliases for one minor release; the cli does not call those surfaces directly.
153
+ - **D1-002 — `CliModuleNotFoundError` renamed to `ModuleNotFoundError`**
154
+ for cross-language port-ability with TS / Rust `ModuleNotFoundError`.
155
+ The class intentionally shadows `builtins.ModuleNotFoundError` inside
156
+ the `apcore_cli` namespace. A deprecation alias
157
+ `CliModuleNotFoundError = ModuleNotFoundError` is kept for backwards
158
+ compatibility and will be removed in v0.10.0. Reverses the D2-001
159
+ rename which predated the cross-SDK parity policy.
160
+ - **Issue #19 — drop "apcore" branding from embedded-mode `--help`**:
161
+ `create_cli()` now resolves the top-level CLI description from the new
162
+ `description=` parameter (defaults to `f"{prog_name} CLI"`), the `apcli`
163
+ subgroup advertises itself as `Built-in commands` rather than
164
+ `apcore-cli built-in commands`, and the `--verbose` option / footer drop
165
+ the trailing `apcore` from `(including built-in apcore options)`. Standalone
166
+ bin entry (`apcore_cli/__main__.py:main()`) passes
167
+ `description="<prog> — execute apcore modules from the command line"`
168
+ explicitly so the standalone surface is unchanged.
169
+
170
+ ### Added
171
+
172
+ - **`--format markdown` and `--format skill`** for `apcli list` and `apcli describe`
173
+ (issue [#20](https://github.com/aiperceivable/apcore-cli/issues/20)). Both
174
+ delegate to `apcore_toolkit.format_module(s)` (≥0.6) so the output is
175
+ byte-identical to the same toolkit call in the TypeScript and Rust SDKs.
176
+ `--format skill` produces vendor-neutral SKILL.md content directly loadable
177
+ by Claude Code (`.claude/skills/<id>/SKILL.md`) and Gemini CLI
178
+ (`.gemini/skills/<id>/SKILL.md`):
179
+
180
+ ```bash
181
+ apcli describe users.create --format skill > .claude/skills/users.create/SKILL.md
182
+ ```
183
+
184
+ A new internal adapter `_descriptor_to_scanned()` maps `ModuleDescriptor`
185
+ (apcore registry) to `ScannedModule` (apcore-toolkit). A `ClickException` with
186
+ a clear install hint is raised if the optional `[toolkit]` extra is missing.
187
+ - **Issue #18 — host-app `--version` opt-in**: new `version: str | None = None`
188
+ parameter on `create_cli()`. When supplied, registers `-V/--version` with
189
+ the host's version string. **When omitted, the `--version` flag is no
190
+ longer registered** — embedded CLIs that do not opt in stop leaking the
191
+ SDK's own version through `-V/--version`. The standalone bin entry
192
+ passes `version=apcore_cli.__version__` explicitly so the
193
+ `apcore-cli` binary's behaviour is preserved.
194
+ - **Issue #19 — `description: str | None = None`** on `create_cli()`.
195
+ - **Issue #17 — `system.usage` aggregator + `list --sort calls|errors|latency`**:
196
+ new module `apcore_cli.system_usage` reads `~/.apcore-cli/audit.jsonl`,
197
+ filters by period (default 24h), and returns per-module aggregates
198
+ (`calls`, `errors`, `avg latency_ms`). `list --sort {calls,errors,latency}`
199
+ now consults the aggregator instead of falling back to id-sort with a
200
+ buried `logger.warning`. When the audit log has no entries in the period
201
+ window the discovery layer prints a user-visible note to stderr
202
+ (`note: no usage data available for --sort <field>; sorted by id. ...`)
203
+ and falls back to id-sort. Module-protocol registration of
204
+ `system.usage.summary` / `system.usage.module` as registry-callable
205
+ built-ins is tracked as a follow-up — today the readers are invoked
206
+ directly by the discovery layer.
207
+ - New file: `apcore_cli/system_usage.py`.
208
+
209
+ ---
210
+
211
+ ## [0.7.0] - 2026-04-23
212
+
213
+ ### Changed
214
+
215
+ - **Dependency bump**: requires `apcore >= 0.18.0` (was `>= 0.17.1`). Aligns with upstream `apcore 0.18.0` and `apcore-toolkit 0.4.2` breaking changes.
216
+ - **`MAX_MODULE_ID_LENGTH` 128 → 192**: `validate_module_id()` and all references updated to the new 192-character limit introduced in `apcore 0.18.0` (`apcore.registry.registry.MAX_MODULE_ID_LENGTH`).
217
+ - **`describe-pipeline` renders `StrategyInfo`**: `executor.describe_pipeline(strategy)` now returns a `StrategyInfo` dataclass (`name`, `step_count`, `step_names`, `description`). `strategy.py` updated to use `StrategyInfo` fields; header line is `Pipeline: {info.name} ({info.step_count} steps)`. Falls back gracefully to the legacy `_resolve_strategy_name` path when `describe_pipeline` is unavailable.
218
+ - **CI — spec-repo checkout**: `.github/workflows/ci.yml` now checks out `aiperceivable/apcore-cli` into `.apcore-cli-spec/` and exposes it to `pytest` via `APCORE_CLI_SPEC_REPO`. Mirrors the pattern established in `apcore-python` / `apcore-cli-typescript`.
219
+
220
+ ### Added
221
+
222
+ - **`create_cli(app=...)` parameter**: `create_cli()` accepts an optional `app: APCore` unified client (introduced in `apcore 0.18.0`). `app` is mutually exclusive with `registry`/`executor` (raises `ValueError`). When `app` is provided, `registry` and `executor` are extracted from `app.registry` and `app.executor`. Filesystem discovery is skipped if `app.registry` already contains registered modules; otherwise normal discovery proceeds into `app.registry`.
223
+ - **Cross-language conformance test harness** (`tests/conformance/`) consuming the shared apcli-visibility fixtures from the `aiperceivable/apcore-cli` spec repo (`conformance/fixtures/apcli-visibility/`). Behavioral assertions (apcli group visibility, registered subcommand set for `include`/`exclude` modes, always-registered `exec`) run today across all five canonical scenarios (`standalone-default`, `embedded-default`, `cli-override`, `env-override`, `yaml-include`). Byte-matching against `expected_help.txt` is marked `xfail` until Click's `HelpFormatter` is replaced with a canonical clap v4 / GNU-style emitter, tracked for parity with `apcore-cli-typescript/src/canonical-help.ts`.
224
+ - **`APCORE_CLI_SPEC_REPO` env var** — overrides the spec-repo lookup path for conformance fixtures. Defaults to a sibling checkout (`../apcore-cli/`). Tests are skipped (not failed) when the spec repo is absent.
225
+ - **FE-12: Module Exposure Filtering** — Declarative control over which discovered modules are exposed as CLI commands.
226
+ - `ExposureFilter` class in `exposure.py` with `is_exposed(module_id)` and `filter_modules(ids)` methods.
227
+ - Three modes: `all` (default), `include` (whitelist), `exclude` (blacklist) with glob-pattern matching.
228
+ - `ExposureFilter.from_config(dict)` classmethod for loading from `apcore.yaml` `expose` section.
229
+ - `create_cli(expose=...)` parameter accepting `dict` or `ExposureFilter` instance.
230
+ - `list --exposure {exposed,hidden,all}` filter flag in discovery commands.
231
+ - `GroupedModuleGroup._build_group_map()` integration: calls `ExposureFilter.is_exposed()` to filter command registration.
232
+ - `ConfigResolver` gains `expose.*` config keys.
233
+ - 4-tier config precedence: `CliConfig.expose` > `--expose-mode` CLI flag > env var > `apcore.yaml`.
234
+ - Hidden modules remain invocable via `exec <module_id>`.
235
+ - New file: `exposure.py`.
236
+
237
+ ---
238
+
239
+ ## [0.6.0] - 2026-04-06
240
+
241
+ ### Changed
242
+
243
+ - **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.
244
+ - **Schema parser**: Required schema properties now correctly enforced at CLI option level (was silently optional).
245
+ - **Approval gate**: Fixed inverted logic in annotation type guard; `check_approval()` now accepts `timeout` parameter.
246
+
247
+ ### Added
248
+
249
+ - **FE-11: Usability Enhancements** — 11 new capabilities:
250
+ - `--dry-run` preflight mode via `Executor.validate()`. Standalone `validate` command.
251
+ - System management commands: `health`, `usage`, `enable`, `disable`, `reload`, `config get`/`config set`. Graceful no-op when system modules unavailable.
252
+ - Enhanced error output: structured JSON with `ai_guidance`, `suggestion`, `retryable`, `user_fixable`, `details`. TTY hides machine-only fields.
253
+ - `--trace` pipeline visualization via `call_with_trace()`.
254
+ - `CliApprovalHandler` class implementing apcore `ApprovalHandler` protocol, wired to `Executor.set_approval_handler()`. `--approval-timeout`, `--approval-token` flags.
255
+ - `--stream` JSONL output via `Executor.stream()`.
256
+ - Enhanced `list` command: `--search`, `--status`, `--annotation`, `--sort`, `--reverse`, `--deprecated`, `--deps`.
257
+ - `--strategy` selection: `standard`, `internal`, `testing`, `performance`, `minimal`. `describe-pipeline` command.
258
+ - Output format extensions: `--format csv|yaml|jsonl`, `--fields` dot-path field selection.
259
+ - Multi-level grouping: `cli.group_depth` config key.
260
+ - Custom command extension: `create_cli(extra_commands=[...])` with collision detection.
261
+ - New error code: `CONFIG_ENV_MAP_CONFLICT`.
262
+ - New config keys: `cli.approval_timeout` (60), `cli.strategy` ("standard"), `cli.group_depth` (1).
263
+ - New environment variables: `APCORE_CLI_APPROVAL_TIMEOUT`, `APCORE_CLI_STRATEGY`, `APCORE_CLI_GROUP_DEPTH`.
264
+ - New files: `system_cmd.py`, `strategy.py`.
265
+
266
+ ---
267
+
268
+ ## [0.5.1] - 2026-04-03
269
+
270
+ ### Added
271
+ - **Pre-populated registry support** — `create_cli()` accepts optional `registry` and `executor` parameters. When a pre-populated `Registry` is provided, filesystem discovery is skipped entirely. This enables frameworks that register modules at runtime (e.g. apflow's bridge) to generate CLI commands from their existing registry without requiring an extensions directory.
272
+ - Passing `registry` alone auto-builds an `Executor`; passing `executor` without `registry` raises `ValueError`.
273
+
274
+ ---
275
+
276
+ ## [0.4.1] - 2026-03-30
277
+
278
+ ### Fixed
279
+ - prevent click parameter mismatch by setting expose_value=False for the --man option
280
+
281
+ ## [0.4.0] - 2026-03-29
282
+
283
+ ### Added
284
+ - **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.
285
+ - **Universal man page generation** — `build_program_man_page()` generates a complete roff man page covering all registered commands. `configure_man_help()` adds `--help --man` support to any Click CLI, enabling downstream projects to get man pages for free.
286
+ - **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.
287
+
288
+ ### Changed
289
+ - `build_module_command()` respects the global verbose help flag to control built-in option visibility.
290
+ - `--sandbox` is now always hidden from help (not yet implemented). Only four built-in options (`--input`, `--yes`, `--large-input`, `--format`) toggle with `--verbose`.
291
+ - Improved built-in option descriptions for clarity.
292
+
293
+ ---
294
+
295
+ ## [0.3.1] - 2026-03-27
296
+
297
+ ### Added
298
+
299
+ - **DisplayResolver integration** — `__main__.py` integrates `DisplayResolver` from `apcore-toolkit` (optional) when `--binding` option is provided; gracefully skipped when not installed.
300
+ - **`init` to `BUILTIN_COMMANDS`** — `init` subcommand is now registered in the builtin commands set.
301
+ - **`APCORE_AUTH_API_KEY` to man page** — environment variable documented in generated roff man page.
302
+ - **Grouped shell completion with `_APCORE_GRP`** — bash/zsh/fish completion scripts now support two-level group/command completion via the `_APCORE_GRP` environment variable (`shell.py`).
303
+ - **Path traversal validation for `--dir` in `init` command** — rejects paths containing `..` segments to prevent directory escape (`init_cmd.py`).
304
+
305
+ ### Fixed
306
+
307
+ - **`RegistryWriter` API call** — constructor now called without parameters; fixes `TypeError` introduced by upstream API change.
308
+
309
+ ### Changed
310
+
311
+ - `apcore` dependency bumped to `>=0.14.0`.
312
+
313
+ ---
314
+
315
+ ## [0.3.0] - 2026-03-23
316
+
317
+ ### Added
318
+
319
+ - **Display overlay routing** (§5.13) — `LazyModuleGroup` now reads `metadata["display"]["cli"]` for alias and description when building the command list and routing `get_command()`. Commands are exposed under their CLI alias instead of raw module_id.
320
+ - `_alias_map`: built from `metadata["display"]["cli"]["alias"]` (with module_id fallback), enabling `apcore-cli alias-name` invocation.
321
+ - `_descriptor_cache`: populated during alias map build to avoid double `registry.get_definition()` calls in `get_command()`.
322
+ - `_alias_map_built` flag only set on successful build, allowing retry after transient registry errors.
323
+ - **Display overlay in JSON output** — `format_module_list(..., "json")` now reads `metadata["display"]["cli"]` for `id`, `description`, and `tags`, consistent with the table output branch.
324
+
325
+ ### Changed
326
+
327
+ - `_ERROR_CODE_MAP.get(error_code, 1)`: guarded with `isinstance(error_code, str)` to prevent `None`-key lookup.
328
+ - Runtime companion: `apcore-toolkit >= 0.4.0` enables `DisplayResolver` and `ConventionScanner` (graceful fallback when not installed).
329
+
330
+ ### Tests
331
+
332
+ - `TestDisplayOverlayAliasRouting` (6 tests): `list_commands` uses CLI alias, `get_command` by alias, cache hit path, module_id fallback, `build_module_command` alias and description.
333
+ - `test_format_list_json_uses_display_overlay`: JSON output uses display overlay alias/description/tags.
334
+ - `test_format_list_json_falls_back_to_scanner_when_no_overlay`: JSON output falls back to scanner values.
335
+
336
+ ### Added (Grouped Commands — FE-09)
337
+
338
+ - **`GroupedModuleGroup(LazyModuleGroup)`** — organizes modules into nested `click.Group` subcommands based on namespace prefixes. Auto-groups by first `.` segment, with `display.cli.group` override from binding.yaml.
339
+ - `_resolve_group()` — 3-tier group resolution: explicit `display.cli.group` > first `.` segment of CLI alias > top-level.
340
+ - `_build_group_map()` — lazy, idempotent group map builder with builtin collision detection and shell-safe group name validation.
341
+ - `format_help()` — collapsed root help with Commands, Modules, and Groups sections (with command counts).
342
+ - **`_LazyGroup(click.Group)`** — nested group that lazily builds subcommands from module descriptors.
343
+ - **`list --flat` flag** — opt-in flat display mode for `list` command; default is now grouped display.
344
+ - **`format_grouped_module_list()`** — Rich table output grouped by namespace.
345
+ - **Updated shell completions** — bash/zsh/fish completion scripts handle two-level group/command structure.
346
+
347
+ ### Changed (Grouped Commands)
348
+
349
+ - `create_cli()` now uses `GroupedModuleGroup` instead of `LazyModuleGroup`.
350
+
351
+ ### Tests (Grouped Commands)
352
+
353
+ - 48 new tests: `TestResolveGroup` (8+), `TestBuildGroupMap` (5+), `TestGroupedModuleGroupRouting` (7), `TestLazyGroupInner` (4), `TestGroupedHelpDisplay` (5), `TestCreateCliGrouped` (1), `TestGroupedE2E` (5), `TestGroupedDiscovery` (7+), `TestGroupedCompletion` (6).
354
+
355
+ ### Added (Convention Module Discovery — §5.14)
356
+
357
+ - **`apcore-cli init module <id>`** — scaffolding command with `--style` (decorator, convention, binding) and `--description` options. Generates module templates in the appropriate directory.
358
+ - **`--commands-dir` CLI option** — path to a convention commands directory. When set, `ConventionScanner` from `apcore-toolkit` scans for plain functions and registers them as modules.
359
+
360
+ ### Tests (Convention Module Discovery)
361
+
362
+ - 6 new tests in `tests/test_init_cmd.py` covering all three styles and options.
363
+
364
+ ---
365
+
366
+ ## [0.2.2] - 2026-03-22
367
+
368
+ ### Changed
369
+ - Rebrand: aipartnerup → aiperceivable
370
+
371
+ ## [0.2.1] - 2026-03-19
372
+
373
+ ### Changed
374
+ - Help text truncation limit increased from 200 to 1000 characters (configurable via `cli.help_text_max_length` config key)
375
+ - `_extract_help`: added `max_length: int = 1000` parameter (`schema_parser.py`)
376
+ - `schema_to_click_options`: added `max_help_length: int = 1000` parameter (`schema_parser.py`)
377
+ - `build_module_command`: added `help_text_max_length: int = 1000` parameter, threaded through to schema parser (`cli.py`)
378
+ - `LazyModuleGroup`: constructor accepts `help_text_max_length: int = 1000`, passes to `build_module_command` (`cli.py`)
379
+ - `create_cli`: resolves `cli.help_text_max_length` from `ConfigResolver` and passes to `LazyModuleGroup` (`__main__.py`)
380
+ - `format_exec_result`: nested dict/list values in table mode now rendered with `json.dumps` instead of `str()` (`output.py`)
381
+
382
+ ### Added
383
+ - `cli.help_text_max_length` config key (default: 1000) in `ConfigResolver.DEFAULTS` (`config.py`)
384
+ - `APCORE_CLI_HELP_TEXT_MAX_LENGTH` environment variable support for configuring help text max length
385
+ - `test_help_truncation_default`: tests default 1000-char truncation
386
+ - `test_help_no_truncation_within_limit`: tests no truncation at 999 chars
387
+ - `test_help_truncation_custom_max`: tests custom max_length parameter
388
+ - 263 tests (up from 261)
389
+
390
+ ## [0.2.0] - 2026-03-16
391
+
392
+ ### Added
393
+ - `APCORE_CLI_LOGGING_LEVEL` env var — CLI-specific log level that takes priority over `APCORE_LOGGING_LEVEL`; 3-tier precedence: `--log-level` flag > `APCORE_CLI_LOGGING_LEVEL` > `APCORE_LOGGING_LEVEL` > `WARNING` (`__main__.py`)
394
+ - `test_cli_logging_level_takes_priority_over_global` — verifies `APCORE_CLI_LOGGING_LEVEL=DEBUG` wins over `APCORE_LOGGING_LEVEL=ERROR`
395
+ - `test_cli_logging_level_fallback_to_global` — verifies fallback when CLI-specific var is unset
396
+ - `test_builtin_name_collision_exits_2` — schema property named `format` (or other reserved names) causes `build_module_command` to exit 2
397
+ - `test_exec_result_table_format` — `--format table` renders Rich Key/Value table to stdout
398
+ - `test_bash_completion_quotes_prog_name_in_directive` — verifies `shlex.quote()` applied to `complete -F` directive, not just embedded subshell
399
+ - `test_zsh_completion_quotes_prog_name_in_directives` — verifies `compdef` line uses quoted prog_name
400
+ - `test_fish_completion_quotes_prog_name_in_directives` — verifies `complete -c` lines use quoted prog_name
401
+ - 17 new tests (244 → 261 total)
402
+
403
+ ### Changed
404
+ - `--log-level` accepted choices: `WARN` → `WARNING` (`__main__.py`)
405
+ - `schema_to_click_options`: schema-derived options now always have `required=False`; required fields marked `[required]` in help text instead of Click enforcement — allows `--input -` STDIN to supply required values without Click rejecting first (`schema_parser.py`)
406
+ - `format_exec_result`: now routes through `resolve_format()` and renders Rich table when `--format table` is specified; previously ignored its `format` parameter (`output.py`)
407
+ - `_generate_bash_completion`, `_generate_zsh_completion`, `_generate_fish_completion`: `shlex.quote()` applied to ALL prog_name positions in generated scripts (complete directives, compdef, complete -c), not only embedded subshell commands (`shell.py`)
408
+ - `check_approval`: removed unused `ctx: click.Context` parameter (`approval.py`)
409
+ - `set_audit_logger`: broadened type annotation from `AuditLogger` to `AuditLogger | None` (`cli.py`)
410
+ - `collect_input`: simplified redundant condition `if not raw or raw_size == 0:` → `if not raw:` (`cli.py`)
411
+ - Example `Input` models: all 7 modules updated with `Field(description=...)` on every field so CLI `--help` shows descriptive text for each flag
412
+
413
+ ### Fixed
414
+ - **`--input -` STDIN blocked by Click required enforcement**: `schema_to_click_options` was generating `required=True` Click options; Click validated before the callback ran, rejecting STDIN-only invocations. Resolved by always using `required=False` and delegating required validation to `jsonschema.validate()` after input collection. Fixes all 6 `TestRealStdinPiping` failures.
415
+ - **`--log-level` had no effect**: `logging.basicConfig()` is a no-op after the first call; subsequent `create_cli()` calls in tests retained the prior handler's level. Fixed by calling `logging.getLogger().setLevel()` explicitly after `basicConfig()`.
416
+ - **`test_log_level_flag_takes_effect` false pass**: `--help` is an eager flag that exits before the group callback, so `--log-level DEBUG --help` never applied the log level. Test updated to use `completion bash` subcommand instead.
417
+ - **Shell completion directives not shell-safe**: prog names with spaces or special characters were unquoted in `complete -F`, `compdef`, and `complete -c` lines. Fixed by assigning `quoted = shlex.quote(prog_name)` and using it in all directive positions.
418
+ - **Audit `set_audit_logger(None)` type error**: type annotation rejected `None`; broadened to `AuditLogger | None`.
419
+ - **Test logger level leakage**: tests modifying root logger level affected subsequent tests; fixed with `try/finally` that restores the original level.
420
+
421
+ ### Security
422
+ - `AuditLogger._hash_input`: now uses `secrets.token_bytes(16)` per-invocation salt before hashing, preventing cross-invocation input correlation via SHA-256 rainbow tables
423
+ - `build_module_command`: added reserved-name collision guard — exits 2 if a schema property (`input`, `yes`, `large_input`, `format`, `sandbox`) conflicts with a built-in CLI option name
424
+ - `_prompt_with_timeout` (SIGALRM path): wrapped in `try/finally` to guarantee signal handler restoration regardless of exit path
425
+
426
+ ## [0.1.0] - 2026-03-15
427
+
428
+ ### Added
429
+ - `--sandbox` flag for subprocess-isolated module execution (FE-05)
430
+ - `ModuleExecutionError` exception class for sandbox failures
431
+ - Windows approval timeout support via `threading.Timer` + `ctypes` (FE-03)
432
+ - Approval timeout clamping to 1..3600 seconds range (FE-03)
433
+ - Tag format validation (`^[a-z][a-z0-9_-]*$`) in `list --tag` (FE-04)
434
+ - `cli.auto_approve` config key with `False` default (FE-07)
435
+ - Extensions directory readability check with exit code 47 (FE-01)
436
+ - Missing required property warning in schema parser (FE-02)
437
+ - DEBUG log `"Loading extensions from {path}"` before registry discovery (FE-01)
438
+ - `TYPE_CHECKING` imports for proper type annotations (`Registry`, `Executor`, `ModuleDescriptor`, `ConfigResolver`, `AuditLogger`)
439
+ - `_get_module_id()` helper for `canonical_id`/`module_id` resolution
440
+ - `APCORE_AUTH_API_KEY` and `APCORE_CLI_SANDBOX` to README environment variables table
441
+ - `--sandbox` to README module execution options table
442
+ - CHANGELOG.md
443
+ - Core Dispatcher (FE-01): `LazyModuleGroup`, `build_module_command`, `collect_input`, `validate_module_id`
444
+ - Schema Parser (FE-02): `schema_to_click_options`, `_map_type`, `_extract_help`, `reconvert_enum_values`
445
+ - Ref Resolver (FE-02): `resolve_refs`, `_resolve_node` with `$ref`, `allOf`, `anyOf`, `oneOf` support
446
+ - Config Resolver (FE-07): `ConfigResolver` with 4-tier precedence (CLI > Env > File > Default)
447
+ - Approval Gate (FE-03): `check_approval`, `_prompt_with_timeout` with TTY detection and Unix SIGALRM
448
+ - Discovery (FE-04): `list` and `describe` commands with tag filtering and TTY-adaptive output
449
+ - Output Formatter (FE-08): `format_module_list`, `format_module_detail`, `format_exec_result` with Rich rendering
450
+ - Security Manager (FE-05): `AuthProvider`, `ConfigEncryptor` (keyring + AES-256-GCM), `AuditLogger` (JSON Lines), `Sandbox` (subprocess isolation)
451
+ - Shell Integration (FE-06): bash/zsh/fish completion generators, roff man page generator
452
+ - 8 example modules: `math.add`, `math.multiply`, `text.upper`, `text.reverse`, `text.wordcount`, `sysutil.info`, `sysutil.env`, `sysutil.disk`
453
+ - 244 tests (unit, integration, end-to-end)
454
+ - CI workflow with pytest and coverage
455
+ - Pre-commit hooks configuration
@@ -0,0 +1,60 @@
1
+ # CLAUDE.md — apcore-cli-python
2
+
3
+ ## Build & Test
4
+
5
+ - `pytest` — run all tests. **Must pass before considering any task complete.**
6
+ - `pytest --cov` — run with coverage report.
7
+ - `ruff check .` — lint check.
8
+ - `ruff format .` — format all code. **Run after every code change.**
9
+
10
+ ## Code Style
11
+
12
+ - Python 3.11+ with `from __future__ import annotations`.
13
+ - All code must pass `ruff check` and `ruff format --check`.
14
+ - Type annotations on all public function signatures.
15
+ - Use `click.echo()` for user-facing output, `logger.*` for debug/diagnostic output.
16
+ - Prefer `sys.exit(code)` with exit code constants over raising exceptions for CLI errors.
17
+
18
+ ## Project Conventions
19
+
20
+ - Spec repo (single source of truth): `../apcore-cli/docs/`
21
+ - Package structure: `src/apcore_cli/` with `__init__.py` exporting `__version__` only.
22
+ - Entry point: `apcore_cli.__main__:main`.
23
+ - Security modules live in `src/apcore_cli/security/` sub-package.
24
+ - ConfigResolver.DEFAULTS values are Python-typed (str, int, bool).
25
+ - Tests organized by module: `tests/test_<module>.py`, security tests in `tests/test_security/`.
26
+
27
+ ## Environment
28
+
29
+ - Python >= 3.11
30
+ - Key dependencies: click >= 8.1, rich >= 13.0, jsonschema >= 4.20, pyyaml >= 6.0, keyring >= 24, cryptography >= 41
31
+ - Runtime: apcore >= 0.19.0 (v0.7.0 bump, was 0.17.1)
32
+ - Optional: apcore-toolkit >= 0.4 (install via `pip install apcore-cli[toolkit]`)
33
+ - Dev: pytest, pytest-asyncio, pytest-cov, mypy, ruff
34
+
35
+ ## v0.7.0 Conventions
36
+
37
+ - Public surface (`__init__.py`): `__version__`, `create_cli`, `ExposureFilter`,
38
+ `ApcliGroup`, `ApcliMode`, `RESERVED_GROUP_NAMES`, `CliApprovalHandler`,
39
+ `resolve_refs`, `schema_to_click_options`, `format_exec_result`,
40
+ `ConfigResolver`, `AuditLogger`, `AuthProvider`, `ConfigEncryptor`, `Sandbox`,
41
+ plus error classes (AuthenticationError, ConfigDecryptionError,
42
+ ModuleExecutionError, ApprovalTimeoutError, ApprovalDeniedError).
43
+ Non-listed symbols (e.g. `GroupedModuleGroup`) must be imported via full
44
+ submodule path (e.g., `from apcore_cli.cli import GroupedModuleGroup`).
45
+ - ExposureFilter + `expose=` kwarg on create_cli (FE-12).
46
+ - `extra_commands=[...]` kwarg on create_cli as the FE-11 extension point (with
47
+ collision detection against RESERVED_GROUP_NAMES — `BUILTIN_COMMANDS` retired v0.7.0).
48
+ - Default click Group class is `GroupedModuleGroup` (multi-level grouping since v0.3.0).
49
+ - All apcore-cli commands live under the `apcli` group (FE-13). Deprecation shims
50
+ remain at root level for back-compat until v0.8.
51
+ - `system_cmd` module registers runtime system commands (health/usage/enable/disable/
52
+ reload/config) — FE-11.
53
+ - `strategy` module registers describe-pipeline + --strategy flag — FE-11.
54
+ - `validate` module + --dry-run flag — FE-11.
55
+ - `CliApprovalHandler` async protocol (request_approval/check_approval) — FE-11 §3.5.1.
56
+ - Config Bus namespace registration at package import time (apcore >= 0.15.0).
57
+ - New env vars (v0.6.0): APCORE_CLI_APPROVAL_TIMEOUT, APCORE_CLI_STRATEGY, APCORE_CLI_GROUP_DEPTH.
58
+ - New config keys (v0.6.0): cli.approval_timeout, cli.strategy, cli.group_depth.
59
+ - New exit codes from apcore 0.17.1: CONFIG_BIND_ERROR (65), CONFIG_MOUNT_ERROR (66),
60
+ ERROR_FORMATTER_DUPLICATE (70), CONFIG_NAMESPACE_* (78).