diff --git a/CHANGELOG.md b/CHANGELOG.md index c173b62..6b8dbf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/). +## [0.77.1] - 2026-08-31 + +### Fixed +- **binding を持たない manifest で SessionStart の通知が無音になる問題を修正(#166)**: `check-pending.sh` の `_load_manifest_binding` は、runtime binding の有無を `jq -cse` の結果で判定していた。「binding なし」を表すセンチネルは `null` だが、`jq -e` は最後の出力が `null` のとき終了コード 1 を返す。このため binding を持たない manifest は常に「不正な manifest」と分類され、`_MANIFEST_BINDING_STATE` が `legacy` ではなく `invalid` になっていた。下流の `_resolve_kit_repo` が失敗して `KIT_REPO` が解決されず、カタログを読めないまま hook が exit 0 で終了するため、**新機能・新プラグインの通知が何の表示もなく抑止されていた** + - 対象は `~/.claude/.starter-kit-manifest.json` が存在し、かつ `kit_repo` / `config_file` を持たないインストール。`write_manifest` は MDM 環境で `policy_sha256` 形式を出力するため MDM 管理下の環境が該当し、binding 導入前の古い manifest も同様。`~/.claude-starter-kit.conf` に有効な `KIT_REPO` があっても、既定のチェックアウトが存在しても使われなかった + - `-e` を外して修正した。`null` は正常なセンチネルであって失敗ではない。部分的な binding と壊れた manifest は引き続き `error()` 経由で非ゼロ終了するため、**fail-closed の挙動は変わらない**。同じフィルタを持つ `wizard/runtime-binding.sh` は元から `-e` なしで、そちらが意図した形だった + - 回帰テストを 3 件追加した(MDM 形式 manifest / 旧 v1 manifest で通知が出ること、壊れた manifest では引き続き出ないこと)。既存のテストは manifest を作らない経路と完全な binding・部分的な binding しか通っておらず、「manifest はあるが binding が無い」ケースが未検査だった + ## [0.77.0] - 2026-08-31 ### Fixed diff --git a/features/feature-recommendation/scripts/check-pending.sh b/features/feature-recommendation/scripts/check-pending.sh index 4a3d5f1..bfb933c 100755 --- a/features/feature-recommendation/scripts/check-pending.sh +++ b/features/feature-recommendation/scripts/check-pending.sh @@ -107,7 +107,13 @@ _load_manifest_binding() { return 0 fi - binding="$(jq -cse ' + # No -e: "no runtime binding" is reported as a null result, and `jq -e` treats + # a trailing null as failure. With -e the legacy branch below was unreachable, + # so every manifest without a binding — MDM's policy_sha256 form and every + # pre-binding manifest — was marked invalid and silenced the notification. + # A malformed or partial binding still exits non-zero through error(), so the + # fail-closed path is unchanged. Matches wizard/runtime-binding.sh. + binding="$(jq -cs ' if length == 1 and (.[0] | type == "object") then .[0] | if (has("kit_repo") or has("config_file")) then diff --git a/tests/unit/test-check-pending.sh b/tests/unit/test-check-pending.sh index 3cb23c6..f0531e1 100644 --- a/tests/unit/test-check-pending.sh +++ b/tests/unit/test-check-pending.sh @@ -347,6 +347,42 @@ else fail "check-pending: partial manifest binding fell through (got '$_cp_out')" fi +# A manifest with no runtime binding at all is not an error: MDM writes the +# policy_sha256 form and every pre-binding install predates the pair. Those +# keep the legacy config/default-checkout lookup. The filter reports "no +# binding" as null, and `jq -e` reads a trailing null as failure — which marked +# them invalid and silenced the notification entirely. +_cp_setup '{"version":1,"plugins":["claude-security"]}' +printf '%s' '{"version":2,"mdm_managed":true,"policy_sha256":"abc"}' \ + > "$_cp_home/.claude/.starter-kit-manifest.json" +_cp_out="$(_cp_run)" +if [[ "$_cp_out" == *"claude-security"* ]]; then + pass "check-pending: an MDM manifest without a binding keeps the legacy lookup" +else + fail "check-pending: a bindingless MDM manifest must not silence the notice (got '$_cp_out')" +fi + +_cp_setup '{"version":1,"plugins":["claude-security"]}' +printf '%s' '{"version":1,"files":[]}' \ + > "$_cp_home/.claude/.starter-kit-manifest.json" +_cp_out="$(_cp_run)" +if [[ "$_cp_out" == *"claude-security"* ]]; then + pass "check-pending: a pre-binding manifest keeps the legacy lookup" +else + fail "check-pending: an older manifest must not silence the notice (got '$_cp_out')" +fi + +# The fail-closed direction must survive the same change: a manifest that is +# not a single JSON object is rejected, not treated as "no binding". +_cp_setup '{"version":1,"plugins":["claude-security"]}' +printf '%s' 'not json' > "$_cp_home/.claude/.starter-kit-manifest.json" +_cp_out="$(_cp_run)" +if [[ -z "$_cp_out" ]]; then + pass "check-pending: an unparseable manifest still fails closed" +else + fail "check-pending: malformed manifest must not fall through (got '$_cp_out')" +fi + # /update-kit is advertised only when the command was actually deployed from # this checkout. INSTALL_COMMANDS=false leaves it absent, so the hook points to # the setup.sh update path instead of promising a nonexistent slash command.