From cc4af9d7568b63ba24a245062fb4f9976ef519ef Mon Sep 17 00:00:00 2001 From: Shinji Saito Date: Mon, 31 Aug 2026 14:37:42 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20binding=20=E3=82=92=E6=8C=81=E3=81=9F?= =?UTF-8?q?=E3=81=AA=E3=81=84=20manifest=20=E3=81=A7=20SessionStart=20?= =?UTF-8?q?=E3=81=AE=E9=80=9A=E7=9F=A5=E3=81=8C=E7=84=A1=E9=9F=B3=E3=81=AB?= =?UTF-8?q?=E3=81=AA=E3=82=8B=E5=95=8F=E9=A1=8C=E3=82=92=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=EF=BC=88v0.77.1=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #166 check-pending.sh の _load_manifest_binding は、runtime binding の有無を jq -cse の結果で判定していた。 binding="$(jq -cse '... else null end ...' "$manifest" 2>/dev/null)" || { _MANIFEST_BINDING_STATE="invalid" return 0 } [[ "$binding" != "null" ]] || return 0 「binding なし」を表すセンチネルは null だが、jq -e は最後の出力が null または false のとき終了コード 1 を返す。このため binding を持たない manifest では常に || 分岐に落ち、_MANIFEST_BINDING_STATE が legacy ではなく invalid になっていた。 直後の null 判定はこの経路では到達しない。 同関数のコメントは「旧形式と MDM manifest は binding を持たないので legacy の 探索を維持する」と述べており、実装は逆の動作をしていた。 影響: invalid は下流で [[ "$_MANIFEST_BINDING_STATE" != "invalid" ]] || return 1 と して効くため、_resolve_kit_repo が失敗して KIT_REPO が解決されない。カタログが 空のまま hook が exit 0 で終了するので、新機能・新プラグインの通知が何の表示も なく抑止される。~/.claude-starter-kit.conf に有効な KIT_REPO があっても、既定の チェックアウトが存在しても使われない。 対象は manifest が存在し kit_repo / config_file を持たないインストール。 write_manifest は MDM 環境で policy_sha256 形式を出力するため MDM 管理下の環境が 該当し、binding 導入前の古い manifest も同様。 修正: -e を外した。null は正常なセンチネルであって失敗ではない。部分的な binding と 壊れた manifest は引き続き error() 経由で非ゼロ終了するため fail-closed の挙動は 変わらない。同じフィルタを持つ wizard/runtime-binding.sh は元から -e なしで、 そちらが意図した形だった。 実測(同一の HOME・pending ファイル・カタログで manifest だけを変えたもの): manifest 修正前 修正後 なし 通知あり 通知あり {"version":2,"mdm_managed":true,...} (空) 通知あり {"version":1,"files":[]} (空) 通知あり {"version":2,"kit_repo":"/tmp/incomplete"} (空) (空) 壊れた JSON (空) (空) 回帰テストを 3 件追加した。既存のテストは manifest を作らない経路と、完全な binding・部分的な binding しか通っておらず、「manifest はあるが binding が無い」 ケースが未検査だった。 検証: - shellcheck -S warning: check-pending.sh / test-check-pending.sh ともにクリーン - tests/unit/test-check-pending.sh: 32/32 通過(修正前は 30/32) Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 8 +++++ .../scripts/check-pending.sh | 8 ++++- tests/unit/test-check-pending.sh | 36 +++++++++++++++++++ 3 files changed, 51 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90fae83..f3f3194 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.76.0] - 2026-08-24 ### Added 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.