agent-quality-kit 0.5.0 → 0.7.0

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 (135) hide show
  1. package/README.md +53 -2
  2. package/README.ru.md +35 -1
  3. package/kit/docs/ai/agent-harness-playbook.md +1 -1
  4. package/kit/docs/ready-made-rules.md +65 -0
  5. package/kit/gates/README.md +60 -0
  6. package/kit/gates/ci-actually-fails/README.md +54 -0
  7. package/kit/gates/ci-actually-fails/check.sh +116 -0
  8. package/kit/gates/ci-actually-fails/gate.yml +14 -0
  9. package/kit/gates/ci-actually-fails/green/.github/workflows/ci.yml +30 -0
  10. package/kit/gates/ci-actually-fails/red/.github/workflows/ci.yml +12 -0
  11. package/kit/gates/ci-actually-fails/red/.github/workflows/soft.yml +15 -0
  12. package/kit/gates/color-from-token/check.sh +13 -1
  13. package/kit/gates/commit-explains-itself/README.md +13 -3
  14. package/kit/gates/commit-explains-itself/check.sh +8 -4
  15. package/kit/gates/complexity-limit/README.md +5 -0
  16. package/kit/gates/complexity-limit/check.sh +21 -2
  17. package/kit/gates/complexity-limit/green/test_fixtures.py +14 -0
  18. package/kit/gates/deps-are-pinned/README.md +14 -1
  19. package/kit/gates/deps-are-pinned/check.sh +6 -1
  20. package/kit/gates/deps-are-pinned/green/pyproject-with-requirements/pyproject.toml +12 -0
  21. package/kit/gates/deps-are-pinned/green/pyproject-with-requirements/requirements.txt +3 -0
  22. package/kit/gates/deps-are-pinned/red/pyproject-loose/pyproject.toml +12 -0
  23. package/kit/gates/deps-are-pinned/red/pyproject-loose/requirements.txt +3 -0
  24. package/kit/gates/duplicate-code/README.md +11 -2
  25. package/kit/gates/duplicate-code/check.sh +31 -4
  26. package/kit/gates/duplicate-code/gate.yml +8 -0
  27. package/kit/gates/duplicate-code/green/imports_a.go +20 -0
  28. package/kit/gates/duplicate-code/green/imports_b.go +19 -0
  29. package/kit/gates/entry-links-exist/README.md +5 -0
  30. package/kit/gates/entry-links-exist/check.sh +6 -0
  31. package/kit/gates/entry-links-exist/green/AGENTS.md +3 -0
  32. package/kit/gates/file-size-limit/README.md +9 -2
  33. package/kit/gates/file-size-limit/check.sh +13 -1
  34. package/kit/gates/gate-not-weakened/README.md +54 -0
  35. package/kit/gates/gate-not-weakened/check.sh +84 -0
  36. package/kit/gates/gate-not-weakened/gate.yml +15 -0
  37. package/kit/gates/gate-not-weakened/green/checkout.ts +8 -0
  38. package/kit/gates/gate-not-weakened/green/payments.py +6 -0
  39. package/kit/gates/gate-not-weakened/green/release.sh +2 -0
  40. package/kit/gates/gate-not-weakened/red/checkout.ts +9 -0
  41. package/kit/gates/gate-not-weakened/red/payments.py +6 -0
  42. package/kit/gates/gate-not-weakened/red/release.sh +2 -0
  43. package/kit/gates/hook-actually-fires/README.md +74 -0
  44. package/kit/gates/hook-actually-fires/check.sh +183 -0
  45. package/kit/gates/hook-actually-fires/gate.yml +15 -0
  46. package/kit/gates/hook-actually-fires/green/.claude/hooks/hooks.json +3 -0
  47. package/kit/gates/hook-actually-fires/green/.claude/settings.json +74 -0
  48. package/kit/gates/hook-actually-fires/green/.claude/settings.local.json +74 -0
  49. package/kit/gates/hook-actually-fires/red/.claude/hooks/hooks.json +4 -0
  50. package/kit/gates/hook-actually-fires/red/.claude/settings.json +53 -0
  51. package/kit/gates/no-phantom-package/README.md +84 -0
  52. package/kit/gates/no-phantom-package/check.sh +161 -0
  53. package/kit/gates/no-phantom-package/gate.yml +20 -0
  54. package/kit/gates/no-phantom-package/green/AGENTS.md +15 -0
  55. package/kit/gates/no-phantom-package/red/AGENTS.md +15 -0
  56. package/kit/gates/no-print-in-prod/README.md +33 -39
  57. package/kit/gates/no-print-in-prod/gate.yml +14 -6
  58. package/kit/gates/personal-config-not-shared/README.md +66 -0
  59. package/kit/gates/personal-config-not-shared/check.sh +103 -0
  60. package/kit/gates/personal-config-not-shared/gate.yml +16 -0
  61. package/kit/gates/personal-config-not-shared/green/.aqk-tracked +9 -0
  62. package/kit/gates/personal-config-not-shared/red/.aqk-tracked +6 -0
  63. package/kit/gates/promise-has-gate/README.md +50 -0
  64. package/kit/gates/promise-has-gate/check.sh +88 -0
  65. package/kit/gates/promise-has-gate/gate.yml +14 -0
  66. package/kit/gates/promise-has-gate/green/.aqk.yml +6 -0
  67. package/kit/gates/promise-has-gate/green/AGENTS.md +7 -0
  68. package/kit/gates/promise-has-gate/red/.aqk.yml +6 -0
  69. package/kit/gates/promise-has-gate/red/AGENTS.md +7 -0
  70. package/kit/gates/secrets-not-in-code/check.sh +13 -1
  71. package/kit/gates/swallowed-error/README.md +36 -18
  72. package/kit/gates/swallowed-error/gate.yml +13 -3
  73. package/kit/gates/test-has-assertion/README.md +47 -0
  74. package/kit/gates/test-has-assertion/check.sh +206 -0
  75. package/kit/gates/test-has-assertion/gate.yml +15 -0
  76. package/kit/gates/test-has-assertion/green/checkout.test.ts +9 -0
  77. package/kit/gates/test-has-assertion/green/test_billing.py +17 -0
  78. package/kit/gates/test-has-assertion/red/checkout.test.ts +8 -0
  79. package/kit/gates/test-has-assertion/red/test_billing.py +14 -0
  80. package/kit/gates/test-not-adjusted/README.md +79 -0
  81. package/kit/gates/test-not-adjusted/check.sh +136 -0
  82. package/kit/gates/test-not-adjusted/gate.yml +19 -0
  83. package/kit/gates/test-not-adjusted/green/after/calc.py +6 -0
  84. package/kit/gates/test-not-adjusted/green/after/tests/test_calc.py +9 -0
  85. package/kit/gates/test-not-adjusted/green/before/calc.py +2 -0
  86. package/kit/gates/test-not-adjusted/green/before/tests/test_calc.py +5 -0
  87. package/kit/gates/test-not-adjusted/red/after/calc.py +2 -0
  88. package/kit/gates/test-not-adjusted/red/after/tests/test_calc.py +5 -0
  89. package/kit/gates/test-not-adjusted/red/before/calc.py +2 -0
  90. package/kit/gates/test-not-adjusted/red/before/tests/test_calc.py +7 -0
  91. package/kit/gates/todo-without-task/README.md +6 -0
  92. package/kit/gates/todo-without-task/check.sh +13 -1
  93. package/kit/ratchet/ratchet.sh +70 -2
  94. package/kit/rules/general.md +23 -0
  95. package/kit/rules-en/general.md +82 -0
  96. package/kit/rules-en/security.md +33 -0
  97. package/kit/rules-en/testing.md +48 -0
  98. package/llms.txt +2 -1
  99. package/package.json +4 -2
  100. package/tool/commands/badge.mjs +7 -1
  101. package/tool/commands/doctor.mjs +90 -10
  102. package/tool/commands/gates.mjs +19 -5
  103. package/tool/commands/project.mjs +15 -2
  104. package/tool/commands/prove.mjs +67 -0
  105. package/tool/commands/report.mjs +4 -1
  106. package/tool/i18n/en-docs.mjs +70 -0
  107. package/tool/i18n/en.mjs +66 -54
  108. package/tool/i18n/ru-docs.mjs +70 -0
  109. package/tool/i18n/ru.mjs +66 -54
  110. package/tool/i18n/templates-en.mjs +9 -9
  111. package/tool/i18n/templates-ru.mjs +9 -9
  112. package/tool/lib/core.mjs +7 -1
  113. package/tool/lib/manifest.mjs +72 -5
  114. package/tool/lib/prove.mjs +160 -0
  115. package/tool/lib/repo.mjs +31 -2
  116. package/tool/lib/scope.mjs +131 -0
  117. package/tool/lib/templates.mjs +2 -0
  118. package/tool/program.mjs +6 -0
  119. package/tool/selfcheck/gates.sh +86 -3
  120. package/tool/selfcheck/lifecycle.mjs +29 -0
  121. package/tool/selfcheck/mutation.sh +21 -1
  122. package/tool/selfcheck/smoke.sh +329 -36
  123. package/tool/selfcheck/units-level.mjs +60 -0
  124. package/tool/selfcheck/units.mjs +196 -1
  125. package/kit/gates/no-print-in-prod/check.sh +0 -38
  126. package/kit/gates/no-print-in-prod/green/docs.ts +0 -15
  127. package/kit/gates/no-print-in-prod/green/main.go +0 -8
  128. package/kit/gates/no-print-in-prod/green/main.rs +0 -4
  129. package/kit/gates/no-print-in-prod/red/main.go +0 -8
  130. package/kit/gates/no-print-in-prod/red/main.rs +0 -4
  131. package/kit/gates/swallowed-error/check.sh +0 -54
  132. package/kit/gates/swallowed-error/green/run.js +0 -8
  133. package/kit/gates/swallowed-error/red/run.js +0 -3
  134. /package/kit/gates/commit-explains-itself/green/{COMMIT_MSG → .aqk-commit-msg} +0 -0
  135. /package/kit/gates/commit-explains-itself/red/{COMMIT_MSG → .aqk-commit-msg} +0 -0
@@ -0,0 +1,74 @@
1
+ # Хук агента правда срабатывает
2
+
3
+ **Намерение.** Человек заводит хук, чтобы машина держала то, что он держать не может: не дать
4
+ сделать `git push --force`, отформатировать после правки, не отпустить работу с красным
5
+ линтером. Ошибку в имени события Claude Code **не показывает** — хук просто никогда не
6
+ вызывается. Настройка выглядит как защита и защитой не является.
7
+
8
+ **Какой отказ это поймало.** Замер по 48 чужим настройкам `.claude/settings.json`, снятым с
9
+ GitHub 2026-09-07. Шесть настоящих находок в четырёх репозиториях:
10
+
11
+ ```json
12
+ "PreToolCall": [
13
+ { "matcher": "Bash(git commit*)",
14
+ "command": "npm run typecheck && npm run test",
15
+ "description": "Run typecheck and tests before committing to ensure CI checks will pass" }
16
+ ]
17
+ ```
18
+
19
+ События `PreToolCall` у Claude Code нет. Человек написал в описании, ради чего это стоит, — и не
20
+ запускается оно никогда. Такие же `PostToolCall` и `preToolCall` нашлись ещё в трёх проектах.
21
+
22
+ **Вторая половина записи — `matcher`, которого нет.** Документация дословно: *«If you add a
23
+ `matcher` field to an event without matcher support, it is silently ignored.»* События без
24
+ поддержки `matcher` — `UserPromptSubmit`, `PostToolBatch`, `Stop`, `TeammateIdle`, `TaskCreated`,
25
+ `TaskCompleted`, `WorktreeCreate`, `WorktreeRemove`, `MessageDisplay`, `CwdChanged`. Написав
26
+ `"Stop": [{ "matcher": "Bash" }]`, автор думает, что отобрал, а хук срабатывает на всём.
27
+
28
+ **Готовый аналог есть, и он этого не ловит — проверено прогоном.**
29
+ [`claudelint`](https://github.com/pdugan20/claudelint) (npm `claude-code-lint`, MIT, обновлялся
30
+ вчера) проверяет схему `settings.json`, синтаксис прав и ловит `"allow": ["*"]` — это мы не
31
+ дублируем, ставьте его рядом. В его исходниках есть и правило `hooks-invalid-event`, но живой
32
+ прогон версии 0.8.0 на файле с `"PoToolUse"` даёт «No problems found»: категория `hooks` на
33
+ `.claude/settings.json` не срабатывает. Причина найдена в их исходнике 2026-09-08 — правила
34
+ категории `Hooks` запускает только `HooksValidator` (`src/validators/hooks.ts`), а он ищет файлы
35
+ по единственному образцу `hooks/hooks.json` (`src/utils/filesystem/patterns.ts:45`). Сообщено:
36
+ [issue #217](https://github.com/pdugan20/claudelint/issues/217). [`cclint`](https://github.com/carlrannaberg/cclint)
37
+ последний раз трогали в сентябре 2025 и лицензии у него нет. `AgentLint` знает 12 событий из 33.
38
+
39
+ **Почему список, а не «похоже на опечатку».** Первая версия краснела только на близких промахах —
40
+ чтобы не объявить опечаткой событие, появившееся после нас. Замер эту конструкцию убил:
41
+ `PreToolCall` отстоит от `PreToolUse` на четыре правки, и проверка его пропускала. Правило,
42
+ красящее **всё** незнакомое, на 39 случайных чужих настройках дало **ноль** ложных.
43
+
44
+ Размен назван вслух: событие, добавленное в Claude Code после нас, эта проверка объявит
45
+ незнакомым. Такое ложное срабатывание видно, оно громкое и чинится одной строкой в списке.
46
+ Пропуск не виден никак — хук молчит, и молчание неотличимо от того, что всё хорошо. Из двух
47
+ ошибок выбираем шумную.
48
+
49
+ **Откуда список.** https://code.claude.com/docs/en/hooks — раздел Configuration, таблица
50
+ «Each event type matches on a different field» и заголовки разделов событий. Снято 2026-09-07,
51
+ 33 события. Дата стоит в самой проверке: список устареет, и это должно быть видно.
52
+
53
+ **Чего НЕ ловит.**
54
+
55
+ - **Хук с верным именем, который падает или ничего не делает.** Проверяется только то, что он
56
+ будет вызван, а не то, что он работает. Вызванный и сломанный хук — предмет другой записи.
57
+ - **Права.** `"allow": ["*"]`, слишком широкие разрешения, ошибки в синтаксисе правил — это
58
+ `claudelint`, и он делает это лучше.
59
+ - **Команду хука, указывающую на несуществующий скрипт.** Искали и **не нашли материала**:
60
+ шесть чужих репозиториев склонированы целиком, 30 путей к скриптам хуков проверены на диске,
61
+ несуществующих — ноль. Это не «не искали»: выборка содержала предмет, и предмет оказался
62
+ здоров. Запись не начата. У `claudelint` правило `hooks-missing-script` есть, но на
63
+ `.claude/settings.json` оно, как и `hooks-invalid-event`, не срабатывает — проверено прогоном
64
+ версии 0.8.0.
65
+ - **Хуки в плагинах и в `~/.claude/`.** Смотрим только настройки проекта: домашний каталог не
66
+ лежит в репозитории и в дифе не виден.
67
+ - **`stop_hook_active`.** Хук на `Stop`, не проверяющий это поле, зациклится — но Claude Code
68
+ сам обрывает его после восьми подряд блокировок, так что цена ошибки восемь ходов, а не
69
+ вечность. Проверять содержимое скрипта на любом языке дороже, чем стоит.
70
+
71
+ **Образцы.** `red/` — опечатка (`PoToolUse`), чужое написание (`post_tool_use`), незнакомое имя
72
+ (`PreToolCall`) и `matcher` на `Stop`. `green/` — те же хуки с верными именами, `matcher: ""` и
73
+ `matcher: "*"` на событиях без поддержки matcher (они означают «всё» — ровно то, что и
74
+ происходит, автор не обманут).
@@ -0,0 +1,183 @@
1
+ #!/usr/bin/env sh
2
+ # Хук, который не сработает никогда: имя события с опечаткой, либо `matcher` на событии,
3
+ # которое его не поддерживает.
4
+ #
5
+ # ЗАЧЕМ. Человек заводит хук, чтобы машина держала то, что он держать не может: не дать
6
+ # сделать force-push, отформатировать после правки, не отпустить работу с красным линтером.
7
+ # Ошибку в имени события Claude Code НЕ показывает — хук просто никогда не вызывается.
8
+ # Настройка выглядит как защита и защитой не является. Это тот же класс, что `pytest || true`:
9
+ # зелёное, полученное по причине, не имеющей отношения к предмету.
10
+ #
11
+ # ОТКУДА СПИСОК СОБЫТИЙ. https://code.claude.com/docs/en/hooks — раздел Configuration, таблица
12
+ # «Each event type matches on a different field» и заголовки разделов. Снято 2026-09-07.
13
+ # Дословно оттуда же про matcher: "If you add a `matcher` field to an event without matcher
14
+ # support, it is silently ignored."
15
+ #
16
+ # ПОЧЕМУ ВСЁ-ТАКИ СПИСОК, А НЕ «БЛИЗКИЙ ПРОМАХ». Первая версия краснела только на именах,
17
+ # отличающихся от известного одной-двумя буквами: список стареет, и не хотелось объявлять
18
+ # опечаткой событие, появившееся после нас. Замер эту конструкцию убил. В `kevinreber/watch-party`
19
+ # лежит `"PreToolCall"` — хук, гоняющий typecheck и тесты перед `git commit` и `git push`,
20
+ # который не срабатывает никогда. От `PreToolUse` это имя отстоит на четыре правки, и проверка
21
+ # по близости его пропускала. Правило же, красящее всё незнакомое, на 39 чужих настройках дало
22
+ # ноль ложных.
23
+ #
24
+ # Размен назван вслух: событие, добавленное в Claude Code после нас, эта проверка объявит
25
+ # незнакомым. Это ложное срабатывание — видимое, громкое и чинится одной строкой здесь. Пропуск
26
+ # же не виден никак: хук молчит, и молчание неотличимо от того, что всё хорошо. Из двух ошибок
27
+ # мы выбираем шумную.
28
+ DIR="${1:-.}"
29
+
30
+ # ПО ОДНОМУ ФАЙЛУ ЗА ПРОХОД, а не списком в один awk. Разбор идёт в END по накопленному тексту:
31
+ # при нескольких файлах в общем буфере FILENAME остаётся последним, а счётчик строк — сквозным,
32
+ # и находка из `settings.json` печаталась как `settings.local.json:71` вместо `settings.json:8`.
33
+ # Мало того что путь чужой: `--since` сверяет напечатанный путь с дифом, не находит его и
34
+ # считает находок ноль — гейт зеленеет. Найдено код-ревью 2026-09-07.
35
+ OUT=""
36
+ ERR=""
37
+ FOUND=0
38
+ for F in "$DIR/.claude/settings.json" "$DIR/.claude/settings.local.json" \
39
+ "$DIR/.claude/hooks/hooks.json" "$DIR/.claude/hooks.json"; do
40
+ [ -f "$F" ] || continue
41
+ FOUND=1
42
+
43
+ # Файл хуков без обёртки `"hooks": { … }` держит карту событий прямо в корне. Такой уклад
44
+ # встречается в `hooks.json`, и без этой поправки триггер срабатывал, а проверять было нечего:
45
+ # тишина неотличима от чистой настройки. В `settings.json` корневые ключи — `permissions`,
46
+ # `env`, `model`, — и считать их событиями нельзя, поэтому послабление только для hooks.json.
47
+ ROOT=0
48
+ # Ищем именно ОБЪЕКТ на верхнем уровне: `"hooks": {`. Просто `"hooks":` не годится — этот же
49
+ # ключ стоит внутри каждой группы («"matcher": "Bash", "hooks": [ … ]»), и грубый греп находил
50
+ # его всегда, из-за чего поблажка не включалась никогда. Найдено прогоном образца.
51
+ case "$F" in
52
+ *hooks.json) tr -d '\n' < "$F" | grep -q '"hooks"[[:space:]]*:[[:space:]]*{' || ROOT=1 ;;
53
+ esac
54
+
55
+ RES=$(awk -v rootIsHooks="$ROOT" '
56
+ BEGIN {
57
+ split("SessionStart Setup InstructionsLoaded UserPromptSubmit UserPromptExpansion " \
58
+ "MessageDisplay PreToolUse PermissionRequest PermissionDenied PostToolUse " \
59
+ "PostToolUseFailure PostToolBatch Notification SubagentStart SubagentStop " \
60
+ "TaskCreated TaskCompleted Stop StopFailure TeammateIdle ConfigChange CwdChanged " \
61
+ "DirectoryAdded FileChanged WorktreeCreate WorktreeRemove PreCompact PostCompact " \
62
+ "PreModelSwitch PostModelSwitch Elicitation ElicitationResult SessionEnd", KNOWN, " ")
63
+ # События, у которых matcher не поддерживается вовсе: он молча игнорируется, и хук
64
+ # срабатывает на каждом событии — шире, чем думает автор.
65
+ split("UserPromptSubmit PostToolBatch Stop TeammateIdle TaskCreated TaskCompleted " \
66
+ "WorktreeCreate WorktreeRemove MessageDisplay CwdChanged", NOMATCH, " ")
67
+ for (i in NOMATCH) NOMATCHER[NOMATCH[i]] = 1
68
+ for (i in KNOWN) NORM[normalize(KNOWN[i])] = KNOWN[i]
69
+ }
70
+ function normalize(s, t) { t = tolower(s); gsub(/[^a-z0-9]/, "", t); return t }
71
+ function min3(a, b, c) { if (a <= b && a <= c) return a; if (b <= c) return b; return c }
72
+ function edit(a, b, la, lb, i, j, prev, cur, cost) {
73
+ la = length(a); lb = length(b)
74
+ if (la == 0) return lb
75
+ if (lb == 0) return la
76
+ for (j = 0; j <= lb; j++) prev[j] = j
77
+ for (i = 1; i <= la; i++) {
78
+ cur[0] = i
79
+ for (j = 1; j <= lb; j++) {
80
+ cost = (substr(a, i, 1) == substr(b, j, 1)) ? 0 : 1
81
+ cur[j] = min3(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + cost)
82
+ }
83
+ for (j = 0; j <= lb; j++) prev[j] = cur[j]
84
+ }
85
+ return prev[lb]
86
+ }
87
+ function judge(ev, ln, nz, best, bestName, d, i) {
88
+ for (i in KNOWN) if (KNOWN[i] == ev) return
89
+ nz = normalize(ev)
90
+ if (nz in NORM) {
91
+ printf "%s:%d: событие «%s» написано не так, как его зовёт Claude Code — «%s». Хук не сработает никогда\n", file, ln, ev, NORM[nz]
92
+ return
93
+ }
94
+ best = 99; bestName = ""
95
+ for (i in KNOWN) { d = edit(nz, normalize(KNOWN[i])); if (d < best) { best = d; bestName = KNOWN[i] } }
96
+ if (best <= 3)
97
+ printf "%s:%d: событие «%s» Claude Code не знает — похоже на «%s». Хук не сработает никогда\n", file, ln, ev, bestName
98
+ else
99
+ printf "%s:%d: событие «%s» Claude Code не знает — хук не сработает никогда. Либо опечатка, либо событие новее этой проверки (список снят 2026-09-07)\n", file, ln, ev
100
+ }
101
+ # Разбор посимвольный с учётом строк и экранирования: `{` внутри строкового значения не
102
+ # меняет глубину. Построчный греп здесь врал бы на любом однострочном json.
103
+ FNR == 1 { file = FILENAME }
104
+ { text = text $0 "\n" }
105
+ END {
106
+ n = length(text); depth = 0; instr = 0; esc = 0; line = 1
107
+ hooksDepth = -1; evDepth = -1; ev = ""; key = ""; buf = ""; awaitMatcher = 0
108
+ for (p = 1; p <= n; p++) {
109
+ c = substr(text, p, 1)
110
+ if (c == "\n") { line++; continue }
111
+ if (instr) {
112
+ if (esc) { esc = 0; buf = buf c; continue }
113
+ if (c == "\\") { esc = 1; continue }
114
+ if (c == "\"") {
115
+ instr = 0; pending = buf; pendingLine = line
116
+ # Значение matcher прочитано. Красим ТОЛЬКО осмысленный фильтр: пустая строка и «*»
117
+ # означают «всё» — ровно то, что и происходит на событии без поддержки matcher, то
118
+ # есть автор не обманут. Замер по 39 чужим настройкам: без этого сужения гейт краснел
119
+ # на четырёх, и все четыре были «matcher»: "" либо "*", то есть шум.
120
+ if (awaitMatcher) {
121
+ awaitMatcher = 0
122
+ if (pending != "" && pending != "*" && pending != ".*")
123
+ printf "%s:%d: у события «%s» matcher не поддерживается — «%s» молча игнорируется, и хук срабатывает на каждом событии, а не на отобранных\n", file, matcherLine, matcherEv, pending
124
+ }
125
+ continue
126
+ }
127
+ buf = buf c; continue
128
+ }
129
+ if (c == "\"") { instr = 1; buf = ""; continue }
130
+ if (c == ":") {
131
+ key = pending; keyLine = pendingLine
132
+ if (ev != "" && key == "matcher" && (ev in NOMATCHER)) { awaitMatcher = 1; matcherLine = keyLine; matcherEv = ev }
133
+ continue
134
+ }
135
+ # ЛЮБОЙ структурный символ снимает ожидание значения matcher. Без этого нестроковое
136
+ # значение (`"matcher": null`, число, массив) оставляло флаг взведённым, и первая же
137
+ # следующая строка документа — обычно ключ «hooks» — печаталась как значение matcher.
138
+ # Найдено код-ревью 2026-09-07.
139
+ if (c == "{" || c == "[") {
140
+ awaitMatcher = 0
141
+ depth++
142
+ if (c == "{" && rootIsHooks == 1 && depth == 1 && hooksDepth == -1) hooksDepth = 1
143
+ else if (c == "{" && key == "hooks" && hooksDepth == -1) hooksDepth = depth
144
+ else if (hooksDepth != -1 && depth == hooksDepth + 1 && key != "") { ev = key; evDepth = depth; judge(key, keyLine) }
145
+ key = ""; continue
146
+ }
147
+ if (c == "}" || c == "]") {
148
+ awaitMatcher = 0
149
+ if (depth == evDepth) { ev = ""; evDepth = -1 }
150
+ if (depth == hooksDepth) hooksDepth = -1
151
+ depth--; key = ""; continue
152
+ }
153
+ if (c == ",") { awaitMatcher = 0; key = ""; continue }
154
+ }
155
+ }
156
+ ' "$F" 2>/tmp/.hookerr.$$)
157
+ CODE=$?
158
+ E=$(cat /tmp/.hookerr.$$ 2>/dev/null); rm -f /tmp/.hookerr.$$
159
+ # Отказ инструмента и чистая настройка дают одинаково пустой вывод и противоположные выводы.
160
+ # Разделяем их кодом возврата: «не смогли разобрать» — это 2, а не молчаливый ноль.
161
+ if [ "$CODE" -ne 0 ] || [ -n "$E" ]; then
162
+ ERR="$ERR$F: разобрать не удалось${E:+ — }$E
163
+ "
164
+ fi
165
+ [ -n "$RES" ] && OUT="$OUT$RES
166
+ "
167
+ done
168
+
169
+ [ "$FOUND" -eq 0 ] && exit 0
170
+
171
+ if [ -n "$ERR" ]; then
172
+ printf '%s' "$ERR"
173
+ echo " почини: покажи файл настроек глазами — проверка не смогла его разобрать."
174
+ echo " «не смогли проверить» и «нарушений нет» дают одинаково пустой список и разные выводы."
175
+ exit 2
176
+ fi
177
+
178
+ ALL=$(printf '%s' "$OUT" | grep -v '^$')
179
+ [ -z "$ALL" ] && exit 0
180
+ printf '%s\n' "$ALL"
181
+ echo " почини: сверь имя события с https://code.claude.com/docs/en/hooks и убери matcher там, где его нет."
182
+ echo " хук с неверным именем не вызывается и об этом не сообщается — защита существует только на бумаге."
183
+ exit 1
@@ -0,0 +1,15 @@
1
+ intent: хук агента правда срабатывает — имя события известно, matcher не игнорируется молча
2
+ intent_en: an agent hook actually fires — the event name is real and the matcher is not silently ignored
3
+
4
+ # Только там, где агента настраивали. В проекте без `.claude/settings.json` проверять нечего,
5
+ # а запись, показанная не тому, стоит доверия всему каталогу.
6
+ trigger:
7
+ has_agent_config: true
8
+
9
+ recipes:
10
+ any: bash {gate}/check.sh {dir}
11
+
12
+ proof: incidents/README.md, 2026-09-07 «хук, которого никогда не было» — замер по 48 чужим
13
+ настройкам: ноль ложных на 39 случайных и шесть настоящих находок в четырёх репозиториях
14
+ (`PreToolCall`, `PostToolCall`, `preToolCall` — событий с такими именами у Claude Code нет,
15
+ и хуки, гоняющие typecheck и тесты перед коммитом, не срабатывают там никогда)
@@ -0,0 +1,3 @@
1
+ {
2
+ "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": "guard.sh" } ] } ]
3
+ }
@@ -0,0 +1,74 @@
1
+ {
2
+ "permissions": {
3
+ "deny": [
4
+ "Read(./.env)"
5
+ ]
6
+ },
7
+ "hooks": {
8
+ "PreToolUse": [
9
+ {
10
+ "matcher": "Bash",
11
+ "hooks": [
12
+ {
13
+ "type": "command",
14
+ "command": ".claude/hooks/block-dangerous.sh"
15
+ }
16
+ ]
17
+ }
18
+ ],
19
+ "PostToolUse": [
20
+ {
21
+ "matcher": "Write|Edit",
22
+ "hooks": [
23
+ {
24
+ "type": "command",
25
+ "command": ".claude/hooks/auto-format.sh"
26
+ }
27
+ ]
28
+ }
29
+ ],
30
+ "Stop": [
31
+ {
32
+ "hooks": [
33
+ {
34
+ "type": "command",
35
+ "command": ".claude/hooks/stop-gate.sh"
36
+ }
37
+ ]
38
+ }
39
+ ],
40
+ "UserPromptSubmit": [
41
+ {
42
+ "matcher": "",
43
+ "hooks": [
44
+ {
45
+ "type": "command",
46
+ "command": ".claude/hooks/prompt.sh"
47
+ }
48
+ ]
49
+ }
50
+ ],
51
+ "TaskCompleted": [
52
+ {
53
+ "matcher": "*",
54
+ "hooks": [
55
+ {
56
+ "type": "command",
57
+ "command": ".claude/hooks/done.sh"
58
+ }
59
+ ]
60
+ }
61
+ ],
62
+ "TeammateIdle": [
63
+ {
64
+ "matcher": null,
65
+ "hooks": [
66
+ {
67
+ "type": "command",
68
+ "command": ".claude/hooks/idle.sh"
69
+ }
70
+ ]
71
+ }
72
+ ]
73
+ }
74
+ }
@@ -0,0 +1,74 @@
1
+ {
2
+ "permissions": {
3
+ "deny": [
4
+ "Read(./.env)"
5
+ ]
6
+ },
7
+ "hooks": {
8
+ "PreToolUse": [
9
+ {
10
+ "matcher": "Bash",
11
+ "hooks": [
12
+ {
13
+ "type": "command",
14
+ "command": ".claude/hooks/block-dangerous.sh"
15
+ }
16
+ ]
17
+ }
18
+ ],
19
+ "PostToolUse": [
20
+ {
21
+ "matcher": "Write|Edit",
22
+ "hooks": [
23
+ {
24
+ "type": "command",
25
+ "command": ".claude/hooks/auto-format.sh"
26
+ }
27
+ ]
28
+ }
29
+ ],
30
+ "Stop": [
31
+ {
32
+ "hooks": [
33
+ {
34
+ "type": "command",
35
+ "command": ".claude/hooks/stop-gate.sh"
36
+ }
37
+ ]
38
+ }
39
+ ],
40
+ "UserPromptSubmit": [
41
+ {
42
+ "matcher": "",
43
+ "hooks": [
44
+ {
45
+ "type": "command",
46
+ "command": ".claude/hooks/prompt.sh"
47
+ }
48
+ ]
49
+ }
50
+ ],
51
+ "TaskCompleted": [
52
+ {
53
+ "matcher": "*",
54
+ "hooks": [
55
+ {
56
+ "type": "command",
57
+ "command": ".claude/hooks/done.sh"
58
+ }
59
+ ]
60
+ }
61
+ ],
62
+ "TeammateIdle": [
63
+ {
64
+ "matcher": null,
65
+ "hooks": [
66
+ {
67
+ "type": "command",
68
+ "command": ".claude/hooks/idle.sh"
69
+ }
70
+ ]
71
+ }
72
+ ]
73
+ }
74
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "matcher_ne_stroka": "образец против дефекта, найденного ревью 2026-09-07",
3
+ "PoToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": "x.sh" } ] } ]
4
+ }
@@ -0,0 +1,53 @@
1
+ {
2
+ "permissions": {
3
+ "deny": [
4
+ "Read(./.env)"
5
+ ]
6
+ },
7
+ "hooks": {
8
+ "PoToolUse": [
9
+ {
10
+ "matcher": "Bash",
11
+ "hooks": [
12
+ {
13
+ "type": "command",
14
+ "command": ".claude/hooks/block-dangerous.sh"
15
+ }
16
+ ]
17
+ }
18
+ ],
19
+ "post_tool_use": [
20
+ {
21
+ "matcher": "Write|Edit",
22
+ "hooks": [
23
+ {
24
+ "type": "command",
25
+ "command": ".claude/hooks/auto-format.sh"
26
+ }
27
+ ]
28
+ }
29
+ ],
30
+ "Stop": [
31
+ {
32
+ "matcher": "Bash",
33
+ "hooks": [
34
+ {
35
+ "type": "command",
36
+ "command": ".claude/hooks/stop-gate.sh"
37
+ }
38
+ ]
39
+ }
40
+ ],
41
+ "PreToolCall": [
42
+ {
43
+ "matcher": "Bash(git commit*)",
44
+ "hooks": [
45
+ {
46
+ "type": "command",
47
+ "command": "npm run typecheck && npm test"
48
+ }
49
+ ]
50
+ }
51
+ ]
52
+ }
53
+ }
@@ -0,0 +1,84 @@
1
+ # Пакета с таким именем нет в реестре
2
+
3
+ **Намерение.** Агент, не знающий инструмента, придумывает правдоподобное имя: `reactCodemodHelper`,
4
+ `eslint-plugin-async-safe`, `@types/fetch-retry`. Имя попадает в `AGENTS.md`, в `SKILL.md`, в
5
+ пример из README. Дальше его читает следующий агент — человек или машина — и выполняет
6
+ `npm install`. Если к тому времени имя занято, в проект приезжает чужой код.
7
+
8
+ **Какой отказ это поймало.** README самого `slopcheck` открывается историей: модель написала
9
+ команду `npx` с именем `react-codeshift` в 47 файлах, пакета не существовало, кто-то его
10
+ зарегистрировал, и 237 репозиториев уже на него ссылались. Имя намеренно оторвано здесь от слова
11
+ `npx`: иначе эта строка стала бы находкой самой записи, а вердикт на `main` зависел бы от того,
12
+ не снимет ли реестр захваченный пакет с публикации. Красный образец выбирается так, чтобы его
13
+ нельзя было погасить снаружи, — и текст вокруг него тоже. Мы проверили этот пакет 2026-09-08:
14
+
15
+ ```
16
+ react-codeshift 1.0.0 создан 2026-01-14 maintainer: debugducky
17
+ ```
18
+
19
+ Доля: по [USENIX Security 2025](https://arxiv.org/abs/2406.10279) около 20% сгенерированного
20
+ кода ссылается на несуществующие пакеты, и 58% выдуманных имён повторяются от запроса к
21
+ запросу — то есть предсказуемы для того, кто захочет их занять.
22
+
23
+ **Почему машина, а не внимательность.** Отличить `jscodeshift` от `reactCodemodHelper` глазами
24
+ нельзя: оба выглядят как настоящие. Единственный арбитр — реестр.
25
+
26
+ **Готовый аналог есть, и мы его зовём.**
27
+ [`slopcheck`](https://github.com/mattschaller/slopcheck) (npm, MIT, ноль зависимостей) достаёт
28
+ имена из команд установки в `.md`, `.mdc`, `.yml`, `.yaml`, `.json`, `.cursorrules` и сверяет с
29
+ `registry.npmjs.org`. Своего разбора мы не писали. Обёртка отвечает за три вещи, которых он не
30
+ делает: какие файлы ему дать (иначе он находит наши собственные образцы), что считать браком и
31
+ что делать, когда реестр не ответил.
32
+
33
+ **Почему обёртка, а не прямой вызов.** Без сети `slopcheck` печатает `? имя — validation error`
34
+ и выходит **с нулём**. Для гейта это худший из возможных ответов: сборка зелёная, а не проверено
35
+ ничего — ровно та тишина, ради запрета которой существует весь стандарт. Обёртка в этом случае
36
+ выходит с кодом **2** и говорит «проверка не состоялась», отдельно от вердикта «брак».
37
+ Тот же код — если счётчик находок ненулевой, а разобрать их не вышло: значит `slopcheck` сменил
38
+ формат вывода, и молчать об этом нельзя. И тот же — если блока со счётчиками в ответе не нашлось
39
+ вовсе: так будет, если он начнёт печатать JSON одной строкой. Эту ветку нашли прогоном
40
+ подставного вывода **после** того, как написали «формат стабилен»: без неё все счётчики
41
+ оставались нулями и гейт выходил с нулём, не проверив ничего.
42
+
43
+ **Образцы.** В `red/AGENTS.md` кодмод вызван именем `reactCodemodHelper`. Имя выбрано не наугад: `npm`
44
+ запрещает заглавные буквы в именах новых пакетов, и это подтверждает та же библиотека, которой
45
+ пользуется реестр:
46
+
47
+ ```
48
+ $ node -e "console.log(require('validate-npm-package-name')('reactCodemodHelper'))"
49
+ validForNewPackages: false errors: [ 'name can no longer contain capital letters' ]
50
+ ```
51
+
52
+ Значит образец не сможет протухнуть: занять это имя нельзя никому, а выглядит оно как настоящая
53
+ галлюцинация — модели постоянно пишут camelCase. `green/AGENTS.md` — тот же файл, где кодмод
54
+ назван настоящим именем `jscodeshift`. Второй пакет, `prettier`, стоит в обоих: он показывает,
55
+ что зелёным гейт становится не от пустоты.
56
+
57
+ **Чего НЕ ловит.**
58
+
59
+ - **Пакет, который сквоттер уже зарегистрировал.** Проверка спрашивает у реестра только «есть
60
+ ли», и гаснет ровно в тот момент, когда становится опасно: `react-codeshift` сегодня зелёный.
61
+ Это предел приёма, а не недоделка — «есть ли пакет» и «чей он» разные вопросы, и второй
62
+ решают Socket и Snyk, которых мы не дублируем. Смысл записи в другом: до регистрации проходит
63
+ время, и большинство выдуманных имён так и остаются свободными.
64
+ - **Только npm.** PyPI, crates.io, Go-модули не проверяются: `slopcheck` их не умеет, а писать
65
+ своё — заводить второй разбор команд установки. Питоновский проект, где агент придумал имя
66
+ пакета, эта запись не прикроет.
67
+ - **Имя из прозы, принятое за пакет.** Разбор берёт слово после `npm i`/`npx`/`yarn add` и
68
+ спотыкается о текст, где эти слова стоят не как команда. Проверено на README самого
69
+ `slopcheck`: слово `Commands`, стоящее в прозе после имени команды, даёт находку
70
+ `Commands — not found on npm`. На
71
+ нашем репозитории — 89 файлов, 9 имён — ложных нет, но на чужом такое встретится. Первым,
72
+ кого эта запись покрасила, был её собственный README: имена образцов стояли там рядом с
73
+ `npx`. Текст переписан, проверка — нет. Ослепить её на README записей каталога значило бы
74
+ перестать видеть настоящие команды установки, которых там хватает.
75
+ - **Пакет, названный в исходнике, а не в документации.** `import` из несуществующего модуля —
76
+ предмет сборки, она об этом скажет сама.
77
+ - **Сеть.** Запись — единственная в каталоге, которой нужен интернет. В конвейере без выхода
78
+ наружу она честно выйдет с кодом 2, а не соврёт зелёным. Тем же кодом отвечает ответ реестра
79
+ «слишком часто» (HTTP 429): снаружи он неотличим от обрыва связи, и сообщение называет обе
80
+ причины, а не выбирает одну наугад.
81
+ - **Свой предел времени.** `doctor` даёт гейту 300 секунд, а здесь каждое имя — запрос наружу
82
+ (до трёх попыток по десять секунд, по десять имён разом). Проект с сотнями команд установки
83
+ при медленном реестре упрётся в предел и будет показан как `timeout` — не как «проверка не
84
+ состоялась». Эту разницу изнутри проверки не выразить: её съедает тот, кто её обрывает.