diff --git a/CHANGELOG.md b/CHANGELOG.md index 2699982..c75970d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ 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.62.0] - 2026-06-12 + +LLM 性能監査(#121)P2 の第 2 弾(#112)。doc-blocker を allowlist 全拒否から slop パターン拒否方式に反転した。 + +### Changed +- **doc-blocker を deny-by-pattern 方式に反転(#112)**: 旧実装は allowlist 外の全 .md/.txt Write を exit 2 で無条件ブロックし、ユーザーが明示的に依頼した文書(CHANGELOG.md・LICENSE.txt・docs/ 配下の設計文書・ADR 等)まで一律に妨げていた。新実装: (1) SUMMARY / REPORT / FINDINGS / ANALYSIS / NOTES / RESULTS / TAKEAWAYS 系のアドホック文書名(大文字小文字不問・アンダースコア結合変形含む)のみ対象、(2) ブロックではなく PreToolUse の `permissionDecision: ask` で確認に降格(ユーザー依頼なら 1 確認で通せる)、(3) 確認メッセージに対象パス・常時許可パス・無効化手段(ENABLE_DOC_BLOCKER=false)を明記、(4) キットコマンドの出力先 allowlist(HANDOVER.md / .reports/ / docs/CODEMAPS/ 等)は「常時許可」として維持。一般文書は素通しになり、exit 2 のハードブロックは全廃 + ## [0.61.0] - 2026-06-12 LLM 性能監査(#121)P2 の第 1 弾(#111)。tmux-hooks のハードブロックをリマインダーに降格した。 diff --git a/README.en.md b/README.en.md index 13350dd..98ac607 100644 --- a/README.en.md +++ b/README.en.md @@ -192,7 +192,7 @@ Hooks are automated safety checks that run automatically when Claude Code execut | **Auto Update** | Checks for starter kit updates on session start and applies them in the background | | Tmux Reminder | Suggests run_in_background for foreground dev servers (non-blocking) | | Git Push Review | Pauses before git push for code review | -| Doc Blocker | Prevents creation of unnecessary .md/.txt files | +| Doc Blocker | Asks for confirmation only on ad-hoc SUMMARY/REPORT style docs (general docs pass) | | Prettier Auto-format | Formats JS/TS files after edits | | Biome Auto-format | Formats and lints JS/TS files after edits (Full uses Biome instead of Prettier) | | Console.log Guard | Warns about console.log statements left in code | diff --git a/README.md b/README.md index 0ea374a..73c8edc 100644 --- a/README.md +++ b/README.md @@ -472,7 +472,7 @@ URL・公式ドキュメント・ブログ・ニュース・OSS ページを読 | **自動アップデート** | **セッション開始時にスターターキットの最新版を自動で適用** | | Tmux リマインダー | foreground の dev サーバーに run_in_background を提案(非ブロック) | | Git Push レビュー | コードを共有する前に確認を促す | -| Doc ブロッカー | 不要なドキュメントファイルの作成を防止 | +| Doc ブロッカー | SUMMARY/REPORT 等のアドホック文書のみ確認に降格(一般文書は素通し) | | Prettier 自動フォーマット | JS/TS ファイルを自動で見た目を整える | | Biome 自動フォーマット | JS/TS ファイルを整形・lint(Full は Prettier の代わりに Biome) | | Console.log ガード | デバッグ用コードの消し忘れを警告 | diff --git a/docs/wizard-config-mapping.en.md b/docs/wizard-config-mapping.en.md index bc216bd..2050614 100644 --- a/docs/wizard-config-mapping.en.md +++ b/docs/wizard-config-mapping.en.md @@ -60,7 +60,7 @@ These flags are used to merge the corresponding `features/*/hooks.json` fragment | `ENABLE_WEB_CONTENT_UPDATE` | Web Content Update | Check web extraction skill dependency updates | Yes | | `ENABLE_TMUX_HOOKS` | Tmux Reminder | Suggest run_in_background for foreground dev servers (non-blocking) | Yes | | `ENABLE_GIT_PUSH_REVIEW` | Git Push Review | Pause before push and open a diff | Yes | -| `ENABLE_DOC_BLOCKER` | Doc Blocker | Prevent unnecessary `.md` / `.txt` files | Yes | +| `ENABLE_DOC_BLOCKER` | Doc Blocker | Ask-only guard for slop doc patterns (general docs allowed) | Yes | | `ENABLE_PRETTIER_HOOKS` | Prettier Auto-format | Format JS / TS edits | Yes | | `ENABLE_BIOME_HOOKS` | Biome Auto-format | Format and lint JS / TS edits | Yes | | `ENABLE_CONSOLE_LOG_GUARD` | Console Log Guard | Warn on leftover `console.log` | Yes | diff --git a/docs/wizard-config-mapping.md b/docs/wizard-config-mapping.md index 6dd5dbc..695fc98 100644 --- a/docs/wizard-config-mapping.md +++ b/docs/wizard-config-mapping.md @@ -60,7 +60,7 @@ | `ENABLE_WEB_CONTENT_UPDATE` | Web Content Update | Web 取得スキル依存の更新確認 | はい | | `ENABLE_TMUX_HOOKS` | Tmux Reminder | foreground の dev サーバーに run_in_background を提案(非ブロック) | はい | | `ENABLE_GIT_PUSH_REVIEW` | Git Push Review | push 前に差分確認 | はい | -| `ENABLE_DOC_BLOCKER` | Doc Blocker | 不要な `.md` / `.txt` 作成を抑制 | はい | +| `ENABLE_DOC_BLOCKER` | Doc Blocker | SUMMARY/REPORT 等の slop パターンのみ確認に降格(一般文書は許可) | はい | | `ENABLE_PRETTIER_HOOKS` | Prettier Auto-format | JS / TS 編集後の整形 | はい | | `ENABLE_BIOME_HOOKS` | Biome Auto-format | JS / TS 編集後の整形・lint | はい | | `ENABLE_CONSOLE_LOG_GUARD` | Console Log Guard | `console.log` の取り残し検知 | はい | diff --git a/features/doc-blocker/feature.json b/features/doc-blocker/feature.json index 0584013..6d5b934 100644 --- a/features/doc-blocker/feature.json +++ b/features/doc-blocker/feature.json @@ -1,10 +1,14 @@ { "name": "doc-blocker", "displayName": "Doc File Blocker", - "description": "Block creation of random .md/.txt files to keep docs consolidated", + "description": "Ask for confirmation when writing ad-hoc SUMMARY/REPORT style docs; general documentation passes through", "category": "hook", "default": false, - "profiles": { "minimal": false, "standard": false, "full": true }, + "profiles": { + "minimal": false, + "standard": false, + "full": true + }, "dependencies": [], "conflicts": [] } diff --git a/features/doc-blocker/scripts/check-doc-write.sh b/features/doc-blocker/scripts/check-doc-write.sh index 528e44e..f37f850 100755 --- a/features/doc-blocker/scripts/check-doc-write.sh +++ b/features/doc-blocker/scripts/check-doc-write.sh @@ -1,10 +1,13 @@ #!/bin/bash -# Block ad-hoc docs while allowing documented Starter Kit output locations. +# Deny-by-pattern doc guard: only ad-hoc summary/report style docs trigger a +# confirmation; everything else (CHANGELOG.md, LICENSE.txt, docs/**, ADRs, ...) +# passes through untouched. set -euo pipefail input="$(cat)" file_path="$(printf '%s' "$input" | jq -r '.tool_input.file_path // ""')" +# Kit command/agent output locations: always allowed, even slop-like names. _doc_blocker_allowed_path() { local path="$1" case "$path" in @@ -19,9 +22,31 @@ _doc_blocker_allowed_path() { return 1 } +# Ad-hoc "model slop" doc names: SUMMARY.md, FINAL_REPORT.md, analysis.md, +# NOTES_2026.txt, ... (case-insensitive; underscore-joined variants included). +_doc_blocker_slop_name() { + local base="$1" + local matched=1 + shopt -s nocasematch + if [[ "$base" =~ ^(.*_)?(SUMMARY|REPORT|FINDINGS|ANALYSIS|NOTES|RESULTS|TAKEAWAYS)(_.*)?\.(md|txt)$ ]]; then + matched=0 + fi + shopt -u nocasematch + return "$matched" +} + if [[ "$file_path" =~ \.(md|txt)$ ]] && ! _doc_blocker_allowed_path "$file_path"; then - echo "[Hook] BLOCKED: Use README.md or an approved Starter Kit output path for documentation" >&2 - exit 2 + base="$(basename "$file_path")" + if _doc_blocker_slop_name "$base"; then + jq -cn --arg path "$file_path" '{ + hookSpecificOutput: { + hookEventName: "PreToolUse", + permissionDecision: "ask", + permissionDecisionReason: ("doc-blocker: \($path) matches an ad-hoc summary/report doc pattern (SUMMARY/REPORT/FINDINGS/ANALYSIS/NOTES/RESULTS/TAKEAWAYS). Approve only if the user explicitly asked for this file. Kit output paths (HANDOVER.md, .reports/, docs/CODEMAPS/, ...) are always allowed; the guard can be disabled with ENABLE_DOC_BLOCKER=false.") + } + }' + exit 0 + fi fi printf '%s\n' "$input" diff --git a/i18n/en/strings.sh b/i18n/en/strings.sh index 2602e79..a84dc04 100644 --- a/i18n/en/strings.sh +++ b/i18n/en/strings.sh @@ -86,7 +86,7 @@ STR_HOOKS_AUTO_UPDATE="Auto Update - Automatically update starter kit on session STR_HOOKS_WEB_CONTENT_UPDATE="Web Content Update - Auto-update web-content-extraction skill deps on session start (opt-in)" STR_HOOKS_TMUX="Tmux Reminder - Suggest run_in_background for foreground dev servers (non-blocking)" STR_HOOKS_GIT_PUSH="Git Push Review - Pause before git push for review" -STR_HOOKS_DOC_BLOCK="Doc Blocker - Block creation of random .md/.txt files" +STR_HOOKS_DOC_BLOCK="Doc Blocker - Confirm ad-hoc SUMMARY/REPORT style docs (general docs pass)" STR_HOOKS_PRETTIER="Prettier Auto-format - Format JS/TS files after edits" STR_HOOKS_BIOME="Biome Auto-format - Format and lint JS/TS files after edits" STR_HOOKS_CONSOLE="Console.log Guard - Warn about console.log statements" diff --git a/i18n/ja/strings.sh b/i18n/ja/strings.sh index 56e296c..2e5940c 100644 --- a/i18n/ja/strings.sh +++ b/i18n/ja/strings.sh @@ -84,7 +84,7 @@ STR_HOOKS_AUTO_UPDATE="自動アップデート - セッション開始時にス STR_HOOKS_WEB_CONTENT_UPDATE="Web取得スキル更新 - セッション開始時に web-content-extraction の依存を自動更新(opt-in)" STR_HOOKS_TMUX="Tmux リマインダー - foreground の dev サーバーに run_in_background を提案(非ブロック)" STR_HOOKS_GIT_PUSH="Git Push レビュー - push 前に一時停止してレビュー" -STR_HOOKS_DOC_BLOCK="ドキュメントブロッカー - 不要な .md/.txt ファイルの作成を防止" +STR_HOOKS_DOC_BLOCK="Doc ブロッカー - SUMMARY/REPORT 等のアドホック文書のみ確認(一般文書は素通し)" STR_HOOKS_PRETTIER="Prettier 自動フォーマット - JS/TS ファイルを編集後にフォーマット" STR_HOOKS_BIOME="Biome 自動フォーマット - JS/TS ファイルを編集後に整形と lint 修正" STR_HOOKS_CONSOLE="Console.log ガード - console.log の警告" diff --git a/tests/unit/test-doc-blocker.sh b/tests/unit/test-doc-blocker.sh index 700e694..0d8498d 100644 --- a/tests/unit/test-doc-blocker.sh +++ b/tests/unit/test-doc-blocker.sh @@ -1,38 +1,40 @@ #!/bin/bash -# tests/unit/test-doc-blocker.sh - doc-blocker path allowlist tests +# tests/unit/test-doc-blocker.sh - deny-by-pattern doc guard tests _doc_blocker_script="$PROJECT_DIR/features/doc-blocker/scripts/check-doc-write.sh" _run_doc_blocker_path() { local path="$1" - printf '{"tool_input":{"file_path":"%s"}}' "$path" | "$_doc_blocker_script" >/dev/null + printf '{"tool_input":{"file_path":"%s"}}' "$path" | "$_doc_blocker_script" +} + +_doc_blocker_decision() { + local path="$1" + _run_doc_blocker_path "$path" | jq -r '.hookSpecificOutput.permissionDecision // "allow"' 2>/dev/null || printf 'allow' } { - test_name="doc-blocker: allows Starter Kit command and agent output paths" + test_name="doc-blocker: kit output paths stay allowed (even slop-like names)" allowed=( "$PROJECT_DIR/HANDOVER.md" "$PROJECT_DIR/research.md" - "$PROJECT_DIR/notes/research.md" - "$PROJECT_DIR/sessions/HANDOVER.md" "$PROJECT_DIR/docs/CONTRIB.md" "$PROJECT_DIR/docs/RUNBOOK.md" - "$PROJECT_DIR/docs/CODEMAPS/frontend.md" "$PROJECT_DIR/docs/CODEMAPS/architecture.md" "$PROJECT_DIR/codemaps/backend.md" "$PROJECT_DIR/docs/GUIDES/setup.md" - "$PROJECT_DIR/docs/DELETION_LOG.md" "$PROJECT_DIR/.reports/dead-code-analysis.md" "$PROJECT_DIR/.reports/codemap-diff.txt" + "$PROJECT_DIR/.reports/SUMMARY.md" + "$PROJECT_DIR/docs/CODEMAPS/ANALYSIS.md" "$PROJECT_DIR/.claude/evals/feature-name.md" "$HOME/.claude/skills/learned/pattern.md" "$PROJECT_DIR/.specify/memory/constitution.md" ) ok_all=true for path in "${allowed[@]}"; do - _run_doc_blocker_path "$path" || ok_all=false + [[ "$(_doc_blocker_decision "$path")" == "allow" ]] || ok_all=false done - if [[ "$ok_all" == "true" ]]; then pass "$test_name" else @@ -41,9 +43,20 @@ _run_doc_blocker_path() { } { - test_name="doc-blocker: still blocks ad-hoc markdown and text files" - if ! _run_doc_blocker_path "$PROJECT_DIR/random-notes.md" \ - && ! _run_doc_blocker_path "$PROJECT_DIR/tmp/scratch.txt"; then + test_name="doc-blocker: general docs pass without confirmation (inverted from allowlist)" + general=( + "$PROJECT_DIR/CHANGELOG.md" + "$PROJECT_DIR/LICENSE.txt" + "$PROJECT_DIR/docs/architecture.md" + "$PROJECT_DIR/docs/adr/0001-record.md" + "$PROJECT_DIR/release-notes.md" + "$PROJECT_DIR/market-research.md" + ) + ok_all=true + for path in "${general[@]}"; do + [[ "$(_doc_blocker_decision "$path")" == "allow" ]] || ok_all=false + done + if [[ "$ok_all" == "true" ]]; then pass "$test_name" else fail "$test_name" @@ -51,13 +64,38 @@ _run_doc_blocker_path() { } { - test_name="doc-blocker: rejects suffix variants of research.md / HANDOVER.md with exit 2" - rc_research=0 - _run_doc_blocker_path "$PROJECT_DIR/market-research.md" 2>/dev/null || rc_research=$? - rc_handover=0 - _run_doc_blocker_path "$PROJECT_DIR/PROJECT-HANDOVER.md" 2>/dev/null || rc_handover=$? + test_name="doc-blocker: slop doc names are downgraded to a permission ask (exit 0)" + slop=( + "$PROJECT_DIR/SUMMARY.md" + "$PROJECT_DIR/FINAL_REPORT.md" + "$PROJECT_DIR/analysis.md" + "$PROJECT_DIR/sub/dir/FINDINGS.md" + "$PROJECT_DIR/NOTES_2026.txt" + "$PROJECT_DIR/results.md" + ) + ok_all=true + for path in "${slop[@]}"; do + rc=0 + out="$(_run_doc_blocker_path "$path")" || rc=$? + [[ "$rc" -eq 0 ]] || ok_all=false + [[ "$(printf '%s' "$out" | jq -r '.hookSpecificOutput.permissionDecision // empty')" == "ask" ]] || ok_all=false + printf '%s' "$out" | jq -e '.hookSpecificOutput.permissionDecisionReason | contains("doc-blocker")' >/dev/null || ok_all=false + done + if [[ "$ok_all" == "true" ]]; then + pass "$test_name" + else + fail "$test_name" + fi +} - if [[ "$rc_research" -eq 2 && "$rc_handover" -eq 2 ]]; then +{ + test_name="doc-blocker: non-doc files pass and hard blocks are gone (no exit 2)" + rc_code=0 + _run_doc_blocker_path "$PROJECT_DIR/src/main.ts" >/dev/null || rc_code=$? + rc_slop=0 + _run_doc_blocker_path "$PROJECT_DIR/SUMMARY.md" >/dev/null || rc_slop=$? + if [[ "$rc_code" -eq 0 && "$rc_slop" -eq 0 ]] \ + && ! grep -q 'exit 2' "$_doc_blocker_script"; then pass "$test_name" else fail "$test_name" diff --git a/tests/unit/test-hook-fixtures.sh b/tests/unit/test-hook-fixtures.sh index 6534649..d287f8e 100644 --- a/tests/unit/test-hook-fixtures.sh +++ b/tests/unit/test-hook-fixtures.sh @@ -72,15 +72,15 @@ else fi _doc_fixture="$_hook_fixture_dir/pretooluse-write-doc.json" -_doc_err="$_hook_tmp/doc.err" +_doc_out="$_hook_tmp/doc.out" _doc_rc=0 -bash "$PROJECT_DIR/features/doc-blocker/scripts/check-doc-write.sh" <"$_doc_fixture" >/dev/null 2>"$_doc_err" || _doc_rc=$? +bash "$PROJECT_DIR/features/doc-blocker/scripts/check-doc-write.sh" <"$_doc_fixture" >"$_doc_out" 2>/dev/null || _doc_rc=$? -if [[ "$_doc_rc" -eq 2 ]] \ - && assert_matches "\\[Hook\\] BLOCKED: Use README.md" "$(cat "$_doc_err")"; then - pass "hook-fixtures: doc-blocker consumes PreToolUse Write fixture" +if [[ "$_doc_rc" -eq 0 ]] \ + && [[ "$(jq -r '.hookSpecificOutput.permissionDecision // empty' "$_doc_out")" == "ask" ]]; then + pass "hook-fixtures: doc-blocker downgrades slop doc Write to a permission ask" else - fail "hook-fixtures: doc-blocker should block ad-hoc docs from fixture input" + fail "hook-fixtures: doc-blocker should emit an ask decision for slop docs" fi _post_edit_fixture="$_hook_tmp/post-edit.json"