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
package/README.md CHANGED
@@ -196,7 +196,7 @@ Already using [pre-commit](https://pre-commit.com)? Three lines in the file you
196
196
  ```yaml
197
197
  repos:
198
198
  - repo: https://github.com/arsen-ask-lx/Agent_Quality_Kit
199
- rev: v0.5.0
199
+ rev: v0.7.0
200
200
  hooks:
201
201
  - id: aqk # runs what the repository declares; blocks below AQK-1
202
202
  # - id: aqk-doctor # read-only: the level and what is missing, blocks nothing
@@ -217,7 +217,7 @@ layer AQK adds.
217
217
  [![on the GitHub Marketplace](https://img.shields.io/badge/GitHub%20Marketplace-Agent%20Quality%20Kit-2ea44f?logo=github)](https://github.com/marketplace/actions/agent-quality-kit-aqk)
218
218
 
219
219
  ```yaml
220
- - uses: arsen-ask-lx/Agent_Quality_Kit@v0.5.0
220
+ - uses: arsen-ask-lx/Agent_Quality_Kit@v0.7.0
221
221
  with:
222
222
  min: 1 # the build fails below AQK-1, or if any declared gate failed
223
223
  ```
@@ -236,13 +236,46 @@ aqk find "print statements in production" # is there already such a gate — m
236
236
  aqk doctor # what applies to this repository and what is missing
237
237
  aqk add secrets-not-in-code # copies the check and its samples in, declares it
238
238
  aqk doctor --run # runs the declared gates and shows the result
239
+ aqk doctor --run --since main # ... but only what the diff introduced
239
240
  aqk ratchet no-print-in-prod # existing violations become debt, new ones are blocked
240
241
  ```
241
242
 
243
+ ### The first run on a real project
244
+
245
+ An established repository carries years of debt. Run every gate over all of it and you get a wall
246
+ of red that nobody reads — so the tool gets switched off. `--since <ref>` narrows the output to
247
+ files the diff touched:
248
+
249
+ ```bash
250
+ aqk doctor --run --since main # only what this branch introduced
251
+ ```
252
+
253
+ Three outcomes, all of them said out loud. Findings inside the diff — red, as usual. Findings only
254
+ outside it — green, with the number that was hidden, never a silent "all clear". And a gate whose
255
+ output carries no paths at all (a commit-message check, a CI-config check) **cannot** be narrowed:
256
+ it stays red, and says why. Calling it green because there was nothing to narrow would be exactly
257
+ the silence this tool exists to remove.
258
+
242
259
  Every `doctor --run` rewrites `.aqk/last-run.md` — a short report of what actually ran and how
243
260
  long it took. The list of gates in the manifest says nothing about how many of them are alive
244
261
  right now; the report does. The file is ephemeral — keep it in your own `.gitignore`.
245
262
 
263
+ ### Introducing a rule into a live project
264
+
265
+ Three ways, and each has a price. A big clean-up is put off forever because it is big. The
266
+ ratchet turns existing violations into debt and blocks new ones — right once the rule is agreed.
267
+ And while it is still being argued about, an advisory gate shows findings without failing the run:
268
+
269
+ ```yaml
270
+ advisory:
271
+ - complexity-limit
272
+ ```
273
+
274
+ Declared in the manifest, not passed as a flag. A flag that says "fail nothing" downgrades every
275
+ check at once, is invisible in the diff, and is never named in the summary — that is
276
+ `continue-on-error`, which this tool marks red elsewhere. The list is printed on **every** run:
277
+ an advisory gate everyone forgot about is a switched-off check.
278
+
246
279
  ## When a bug slips past the guards
247
280
 
248
281
  ```bash
@@ -289,6 +322,24 @@ catalogue may grow to hundreds of entries; a given project still sees about a do
289
322
  An entry is accepted only if its arbiter goes red on the red sample, stays quiet on the green
290
323
  one, and names a real failure it caught. A machine checks this: `bash tool/selfcheck/gates.sh`.
291
324
 
325
+ ### Four entries that watch the agent, not the code
326
+
327
+ Ruff, ESLint and gitleaks already find bad code, and AQK calls them where it can rather than
328
+ reinventing them. These four look elsewhere — at the moment the **signal** about bad code is
329
+ switched off, which is what a coding agent does when the task is phrased as "make it pass":
330
+
331
+ | Entry | What it catches |
332
+ |---|---|
333
+ | `gate-not-weakened` | the fix was a suppression, not a fix: bare `# noqa`, `eslint-disable` with no rule named, `@ts-ignore`, `--no-verify` |
334
+ | `ci-actually-fails` | a pipeline step that renders a verdict but cannot fail — `run: pytest \|\| true`, `continue-on-error: true` |
335
+ | `test-has-assertion` | a test that cannot fail: empty body, `assert True`, a skip with no reason given |
336
+ | `promise-has-gate` | a rule in `AGENTS.md` with no enforcer named — neither a gate nor, honestly, a human |
337
+
338
+ Each was measured on nineteen third-party repositories (~25 000 files) before it entered the
339
+ catalogue, and two further entries were **cancelled by that measurement**: one because
340
+ [`agents-lint`](https://github.com/giacomo/agents-lint) already does it better, one because
341
+ 91 of its 120 findings turned out to be a legitimate pattern.
342
+
292
343
  ## The guides as a single file
293
344
 
294
345
  ```bash
package/README.ru.md CHANGED
@@ -198,7 +198,7 @@ aqk badge --check # в конвейере: код 1 в тот день, ког
198
198
  ```yaml
199
199
  repos:
200
200
  - repo: https://github.com/arsen-ask-lx/Agent_Quality_Kit
201
- rev: v0.5.0
201
+ rev: v0.6.0
202
202
  hooks:
203
203
  - id: aqk # запускает объявленное; роняет коммит ниже AQK-1
204
204
  # - id: aqk-doctor # только осмотр: уровень и чего не хватает, ничего не роняет
@@ -286,6 +286,40 @@ vendor/
286
286
  Запись принимается, только если её арбитр краснеет на красном образце, молчит на зелёном и
287
287
  назван реальный отказ, который она поймала. Проверяет это машина: `bash tool/selfcheck/gates.sh`.
288
288
 
289
+ ### Четыре записи, которые смотрят на агента, а не на код
290
+
291
+ Ruff, ESLint и gitleaks и так находят плохой код — AQK зовёт их, где может, вместо того чтобы
292
+ писать своё. Эти четыре смотрят в другое место: на момент, когда **сигнал** о плохом коде
293
+ выключают. Именно это делает агент, когда задача сформулирована как «сделай, чтобы прошло»:
294
+
295
+ | Запись | Что ловит |
296
+ |---|---|
297
+ | `gate-not-weakened` | починкой было подавление: голый `# noqa`, `eslint-disable` без имени правила, `@ts-ignore`, `--no-verify` |
298
+ | `ci-actually-fails` | шаг конвейера, который выносит вердикт, но не может провалиться — `run: pytest \|\| true`, `continue-on-error: true` |
299
+ | `test-has-assertion` | тест, который не может провалиться: пустое тело, `assert True`, пропуск без причины |
300
+ | `promise-has-gate` | правило в `AGENTS.md`, у которого не назван сторож — ни гейт, ни, честно, человек |
301
+
302
+ Каждая измерена на девятнадцати чужих репозиториях (~25 000 файлов) до внесения в каталог, и ещё
303
+ две записи этот же замер **отменил**: одну — потому что
304
+ [`agents-lint`](https://github.com/giacomo/agents-lint) делает это лучше, другую — потому что
305
+ 91 находка из 120 оказалась законным приёмом.
306
+
307
+ ### Как ввести правило в живой проект
308
+
309
+ Три способа, и у каждого своя цена. Большая чистка откладывается навсегда, потому что она
310
+ большая. Храповик превращает старые нарушения в долг и блокирует новые — верно, когда правило
311
+ уже принято. А пока о правиле спорят, совещательный гейт показывает находки, не роняя прогон:
312
+
313
+ ```yaml
314
+ advisory:
315
+ - complexity-limit
316
+ ```
317
+
318
+ Объявлением в манифесте, а не флагом. Флаг «не роняй ничего» понижает все проверки разом, не
319
+ виден в дифе и не назван в итоге — это тот самый `continue-on-error`, который мы сами красим.
320
+ Список печатается **каждый** прогон: совещательный гейт, о котором забыли, — это выключенная
321
+ проверка.
322
+
289
323
  ## Методички одним файлом
290
324
 
291
325
  ```bash
@@ -193,7 +193,7 @@
193
193
  |---|---|---|---|
194
194
  | `block-dangerous-commands.sh` | PreToolUse (Bash **и** PowerShell) | exit 2 + причина в stderr на необратимое: force-push, reset --hard, `DROP DATABASE`, `TRUNCATE`, `docker volume rm`, `rm -rf /` | fail-safe: не распарсил JSON → грепай сырой ввод. Штатный сброс дев-БД (`compose down -v`) — НЕ блокировать |
195
195
  | `auto-format.sh` | PostToolUse (Write\|Edit) | `ruff format` + `ruff check --fix` на изменённый `.py` | агент физически не оставляет неотформатированный код, контекст не тратится |
196
- | `stop-gate.sh` | Stop | красный `ruff` по прод-путям → exit 2 + хвост ошибок → агент чинит, а не «сдаёт» | обязателен гард `stop_hook_active` (иначе вечный цикл); проверять только СВОЙ домен, не параллельную работу человека |
196
+ | `stop-gate.sh` | Stop | красный `ruff` по прод-путям → exit 2 + хвост ошибок → агент чинит, а не «сдаёт» | обязателен гард `stop_hook_active`; проверять только СВОЙ домен, не параллельную работу человека. Поле настоящее — Stop и SubagentStop получают его на вход; но вечного цикла не будет и без гарда: «Claude Code overrides the hook and ends the turn after 8 consecutive blocks» (code.claude.com/docs/en/hooks, раздел Stop input, сверено 2026-09-07). Цена ошибки — восемь ходов, а не вечность |
197
197
 
198
198
  - [ ] ⚠️ **Грабля №1 (Windows): `jq` нет в Git Bash.** Хук с `command -v jq || exit 0` молча
199
199
  превращается в no-op — защита «есть», но не работает. Парсить JSON через
@@ -149,6 +149,54 @@ ruff check --select TRY400 --statistics . # сколько находок У
149
149
 
150
150
  ---
151
151
 
152
+ ## Заглушка вместо реализации: почти всё уже покрыто
153
+
154
+ Самый ожидаемый способ, которым агент «заканчивает» задачу, — подпись без работы. Мы собирались
155
+ писать на это запись каталога и не стали: замер по девятнадцати репозиториям (~25 000 файлов)
156
+ показал, что своей доли почти не остаётся.
157
+
158
+ | Что | Чем ловится | Не забыть |
159
+ |---|---|---|
160
+ | пустое тело функции в JS и TS | `eslint` [`no-empty-function`](https://eslint.org/docs/latest/rules/no-empty-function) | функция с комментарием внутри не считается пустой |
161
+ | абстрактный метод, не переопределённый в конкретном классе | `pylint` [`W0223`](https://pylint.readthedocs.io/en/latest/user_guide/messages/warning/abstract-method.html) | `--disable=all --enable=W0223` — остальное берёт ruff |
162
+ | `raise NotImplemented` вместо `NotImplementedError` | `ruff` [`F901`](https://docs.astral.sh/ruff/rules/raise-not-implemented/) | входит в группу `F` |
163
+ | лишний `pass` рядом с настоящим кодом | `ruff` `PIE790` | |
164
+
165
+ **Чего мы НЕ стали делать и почему.** Пустое тело (`pass`) — не признак недоделки: из 120 находок
166
+ первой версии 91 оказалась законной. Это null-объекты (`NoOpSpan` в sentry-python), безопасные
167
+ заглушки провайдера в pr-agent — там прямо стоит комментарий «safe no-op stubs», —
168
+ необязательные обработчики. А `raise NotImplementedError` в методе класса и есть питоновский
169
+ способ объявить абстракцию, даже без `abc`: так написаны `ContentDecoder` в httpx и интерфейс
170
+ плагина в pre-commit. За вычетом этих двух классов на 25 000 файлах не осталось ни одной находки.
171
+
172
+ ## Обвес самого агента: тоже есть готовое
173
+
174
+ К осени 2026 появился отдельный класс инструментов — линтеры не кода, а того, что читает агент:
175
+ `AGENTS.md`, `CLAUDE.md`, файлы навыков, конфиги хуков и MCP. Писать своё здесь незачем.
176
+
177
+ | Инструмент | Что проверяет | Состояние — дата замера в самой ячейке |
178
+ |---|---|---|
179
+ | [`agnix`](https://github.com/agent-sh/agnix) | 455 правил: структура `CLAUDE.md`/`AGENTS.md`/`SKILL.md`, синтаксис конфигов MCP и хуков, соглашения об именах, **мёртвые ссылки на файлы**. Есть автопочинка и LSP | 404 ⭐, Rust, активен на 2026-09-06. `npm i -g agnix`, `brew`, `pip`, `cargo` |
180
+ | [`agents-lint`](https://github.com/giacomo/agents-lint) | мёртвые npm-скрипты, упомянутые в `AGENTS.md`, устаревшие рамки, деревья каталогов в контексте | 13 ⭐, TypeScript, последний коммит март 2026 — снято 2026-09-06 |
181
+ | [`claudelint`](https://github.com/pdugan20/claudelint) | 116 правил: схема `.claude/settings.json`, синтаксис правил доступа, имена переменных окружения, ссылки на несуществующие файлы, разбор навыков, плагинов, MCP и LSP. Ловит `"allow": ["*"]` | TypeScript, MIT, коммит 2026-09-08. `npx claude-code-lint`. **Код возврата 0 даже на находке**, если её строгость — `warn`: блокирует только `--strict`. Проверено прогоном 0.8.0 |
182
+ | [`slopcheck`](https://github.com/mattschaller/slopcheck) — **не путать с `0xToxSec/slopcheck`, это разные проекты** | имена npm-пакетов из команд установки в `.md`, `.mdc`, `.yml`, `.yaml`, `.json`, `.cursorrules` сверяет с реестром: пакет, которого не существует, — приманка для захвата имени | MIT, ноль зависимостей, TypeScript, пуш 2026-09-06. `npx slopcheck .`, код возврата 1 на находке — проверено прогоном 0.2.0 от 2026-09-08. Без сети выходит с нулём: «не проверено» у него неотличимо от «чисто» |
183
+
184
+ ```bash
185
+ npm install -g agnix && agnix --strict .
186
+ ```
187
+
188
+ **Чего они НЕ делают — и почему у AQK остаётся своя половина.** Все они проверяют документ:
189
+ формат, существование путей, наличие скриптов. Ни один не спрашивает, **подкреплено ли обещание
190
+ командой с кодом возврата**. «Мы никогда не коммитим секреты» — грамматически безупречная
191
+ строка, на которую ни один из них ничего не скажет. Разделение простое: обвес агента проверяет
192
+ `agnix`, права и схему настроек — `claudelint`, исполнимость обещаний — `promise-has-gate`.
193
+
194
+ **Одну дыру в обвесе пришлось закрыть самим.** Имя события хука: ошибка в нём не показывается,
195
+ хук просто не вызывается. У `claudelint` правило `hooks-invalid-event` есть в исходниках, но
196
+ живой прогон версии 0.8.0 на файле с `"PoToolUse"` даёт «No problems found». Замер по 48 чужим
197
+ настройкам нашёл шесть таких хуков в четырёх репозиториях — среди них `typecheck && test` перед
198
+ коммитом, не запускавшийся ни разу. Это запись `hook-actually-fires`.
199
+
152
200
  ## А если проект не на Python?
153
201
 
154
202
  Ничего не меняется. Запись каталога держит **одно намерение и несколько исполнителей**, и
@@ -161,6 +209,23 @@ ruff check --select TRY400 --statistics . # сколько находок У
161
209
  **Одна строка в существующей записи — самый дешёвый и самый ценный вклад.** Намерение уже
162
210
  доказано отказом, образцы уже лежат, проверять нечего кроме самой команды.
163
211
 
212
+ ## Приёмы агента, названные практиками: чем закрыт каждый
213
+
214
+ Список взят из разбора 1154 обсуждений с r/programming, r/learnprogramming, r/ExperiencedDevs
215
+ и Hacker News (Baltes, Cheong, Treude, «An Endless Stream of AI Slop», arxiv 2603.27249,
216
+ январь–сентябрь 2025). Это не подборка мнений, а размеченный корпус.
217
+
218
+ | Приём | Чем закрыт |
219
+ |---|---|
220
+ | «test subversion»: правка теста, чтобы прошёл сломанный код | [`checkwash`](https://github.com/taipei49314/checkwash) — запись `test-not-adjusted` делегирует ему целиком |
221
+ | «deleting methods instead of fixing them» | он же, детектор `TEST_DISABLED` |
222
+ | «casting to `any` to silence type errors» | `@typescript-eslint/no-explicit-any`. Своей записи нет намеренно: замер по `zod` — 769 вхождений на 501 файл, первый прогон даёт стену |
223
+ | «using `setTimeout` as a band-aid fix» | ничем. Замера нет, риск ложных высок: `setTimeout` законен сплошь и рядом |
224
+ | «hallucinating external services, then mocking» | ничем. Отличить выдуманную службу от настоящей статически нечем |
225
+ | выдуманная зависимость (slopsquatting) | [`slopcheck`](https://github.com/mattschaller/slopcheck) — тот, что лежит в npm под этим именем; на нём стоит запись `no-phantom-package`. **Проектов с именем `slopcheck` два**: [`0xToxSec/slopcheck`](https://github.com/0xToxSec/slopcheck) тоже MIT, но последний пуш апрель 2026 и в npm его нет. Проверено 2026-09-08 |
226
+ | подавление проверки без адреса | наш `gate-not-weakened` плюс `eslint-plugin-eslint-comments`, `flake8-noqa` |
227
+ | шаг конвейера, который не может провалиться | наш `ci-actually-fails`; у `checkwash` есть смежный `CI_WORKFLOW_TOUCHED` |
228
+
164
229
  ## Если готового нет
165
230
 
166
231
  Тогда свой гейт — и в его `README.md` пишется, **что именно проверено**: какой инструмент
@@ -70,6 +70,64 @@
70
70
  Плюс шестое, без которого запись не принимается: **доказательство** — реальный отказ, который
71
71
  она поймала. «Это хорошая практика» не принимается.
72
72
 
73
+ ## Находка обязана кончаться действием
74
+
75
+ Проверка, покрасневшая молча, ничем не лучше молчащей. Проверка, сказавшая «плохо» и не
76
+ сказавшая «делай так», — немногим лучше: человек, открывший тридцать находок, хочет команду,
77
+ а не оценку.
78
+
79
+ Поэтому последняя строка вывода — совет, и он начинается с метки:
80
+
81
+ ```
82
+ почини: замени на вызов системы логов — тогда запись попадёт в общий журнал
83
+ ```
84
+
85
+ Приёмка требует эту строку на красном образце, а `doctor --run` печатает её **всегда**: находки
86
+ обрезаются до трёх, совет не обрезается никогда. До этой правки строка существовала для приёмки
87
+ и не существовала для человека — обрезка съедала ровно её.
88
+
89
+ Требование действует для записей с переносимым рецептом. Запись, целиком делегирующая готовому
90
+ инструменту, печатает вывод этого инструмента, и требовать от чужого вывода нашу строку значит
91
+ требовать невозможного.
92
+
93
+ ## Зрелость записи не пишут руками
94
+
95
+ Седьмого поля нет: зрелость **считается** из доказательства. Ссылается `proof` на журнал шишек —
96
+ запись зрелая; не ссылается — условная, и это видно в приёмке каталога. Написать себе
97
+ `lifecycle: stable` нельзя, приёмка такую запись отклонит.
98
+
99
+ Это не придирка к форме. У всех трёх соседей, чей каталог мы разбирали, поле зрелости есть, и
100
+ у всех троих его заполняет автор: `lifecycle` у зондов Scorecard, `future`/`obsolete` у
101
+ критериев значка OpenSSF. Значение, написанное автором, означает доверие к автору. Каталог, где
102
+ зрелость объявляют, к сотне записей превращается в список, в котором нельзя выбрать.
103
+
104
+ Объявляется ровно одно состояние — **`deprecated`**, потому что «эту запись больше не ставят»
105
+ из её файлов не выводится никак. Вместе с ним обязателен `superseded_by` с именем существующей
106
+ записи, и `aqk add` тогда отказывает в установке, назвав преемника:
107
+
108
+ ```yaml
109
+ lifecycle: deprecated
110
+ superseded_by: no-print-in-prod
111
+ ```
112
+
113
+ ## Программа, без которой запись не работает
114
+
115
+ **`requires:`** называет её явно. Нужно там, где переносимый рецепт — обёртка вокруг готового
116
+ инструмента: первое слово команды тогда `bash`, который есть всегда, и по нему не видно, чего
117
+ не хватает. Без этого поля гейт ставился бы и вставал при первом же запуске с «not found» —
118
+ отсутствие сигнала неотличимо от успеха.
119
+
120
+ ```yaml
121
+ recipes:
122
+ any: bash {gate}/check.sh {dir}
123
+ requires: checkwash
124
+ ```
125
+
126
+ `add` отказывает с названной причиной и говорит, что поставить. Приёмка и мутационная проверка
127
+ пропускают запись со словами «НЕ ПРОВЕРЕНА здесь — нужен «…»». Строгий режим
128
+ (`AQK_GATES_STRICT=1`, поднят в нашем конвейере) делает такой пропуск ошибкой: на машине,
129
+ которая инструменты сама и ставит, «нечем проверить» обязано быть красным.
130
+
73
131
  ## Запись без переносимого рецепта
74
132
 
75
133
  Иногда переносимой проверки быть не может: чтобы понять, вызывают ли функцию, нужен граф
@@ -173,6 +231,8 @@ samples_for: python
173
231
  | `has_deps: true` | есть файл зависимостей |
174
232
  | `has_tests: true` | есть каталог тестов или файлы вида `*_test.*` |
175
233
  | `has_env: true` | есть файл окружения |
234
+ | `has_agent_entry: true` | агента здесь используют: есть `CLAUDE.md`, `AGENTS.md`, каталог `.claude`, `.cursor/rules` или инструкции copilot |
235
+ | `has_agent_config: true` | есть настройки самого агента: `.claude/settings.json`, `.claude/settings.local.json`, `.claude/hooks.json`, `.claude/hooks/hooks.json`. Второй файл лежит в `.gitignore` — признак сработает у человека и не сработает в конвейере |
176
236
  | `has_ui: true` | есть стили или однофайловые компоненты (`.css`, `.scss`, `.vue`, `.svelte`, `.astro`) |
177
237
 
178
238
  Любое из `has_*` принимает и `false` — «показывать тем, у кого этого нет». Условие, которого
@@ -0,0 +1,54 @@
1
+ # Проверка в конвейере может провалиться
2
+
3
+ **Намерение.** Шаг выполнен, круг зелёный, проверка не сработала. `run: pytest || true` —
4
+ это строка в логе, а не проверка.
5
+
6
+ **Какой отказ это поймало.** Дыру нашли у себя. Запись `gates-run-in-ci` отвечает на вопрос
7
+ «упомянут ли гейт в конфиге конвейера» и на этом останавливается — то есть конфиг с
8
+ `run: pytest || true` проходил её зелёным. «Упомянут» и «работает» — разные утверждения, и весь
9
+ этот стандарт стоит на том, чтобы их не путать; у себя мы их спутали.
10
+
11
+ Замер по пятнадцати чужим репозиториям подтвердил, что класс живой: у `reviewdog` два шага с
12
+ его собственными линтерами идут под `continue-on-error: true`. Запись в журнале:
13
+ `incidents/README.md`, 2026-09-06.
14
+
15
+ **Что именно проверяется.** Конфиг разбирается по шагам. Шаг красный, если он **и** выносит
16
+ вердикт, **и** не может провалиться.
17
+
18
+ | Выносит вердикт, если | Не может провалиться, если |
19
+ |---|---|
20
+ | команда есть в списке запускалок (`pytest`, `eslint`, `go test`, `golangci-lint`, `mypy`, `cargo clippy`, …) | шаг помечен `continue-on-error: true` |
21
+ | команда объявлена гейтом в `.aqk.yml` **этого** проекта | задача помечена `allow_failure: true` (gitlab) |
22
+ | в **имени шага** стоит слово `lint`, `test`, `check`, `verify`, `audit`, `scan`, `coverage` | провал погашен в самой команде: `\|\| true`, `\|\| :`, `\|\| exit 0` |
23
+
24
+ Гашение прямо в команде красится только для закрытого списка запускалок: `docker network create … || true` — это идемпотентность, а не выключенная проверка.
25
+
26
+ **Готовый аналог.** Не нашли. [`actionlint`](https://github.com/rhysd/actionlint) разбирает
27
+ синтаксис workflow, [`zizmor`](https://github.com/woodruffw/zizmor) ищет в них дыры
28
+ безопасности — ни тот, ни другой не спрашивает, может ли шаг провалиться. `continue-on-error`
29
+ для них — законная настройка, каковой она и является: незаконной её делает то, ЧТО под ней
30
+ стоит, а это знает только проект.
31
+
32
+ **Исход переспрашивают — маска законна.** `continue-on-error` иногда стоит не ради прощения
33
+ провала, а чтобы дать выполниться шагам ПОСЛЕ проверки; вердикт выносится отдельным шагом
34
+ `if: steps.<id>.outcome == 'failure'` → `exit 1`. Так устроен `pre-commit.yml` в `fastapi`:
35
+ проверка идёт под маской, потом чинит файлы и пушит их в ветку, и только в конце роняет
36
+ сборку. Такой шаг больше не находка. Условие узкое: в файле должна быть И ссылка на исход,
37
+ И падение — переспросить исход и ничего с ним не сделать значит простить провал длиннее на
38
+ три строки, и это по-прежнему красное (образец `red/.github/workflows/soft.yml`).
39
+
40
+ Замер 2026-09-07 по пяти чужим репозиториям: молчал на всех пяти. На `fastapi` — по
41
+ случайности, слово-примета не совпало с именем шага «Run prek - pre-commit»; назови они его
42
+ «Run lint», гейт покрасил бы законный уклад. Дефект найден разбором молчания, а не находки.
43
+
44
+ **Чего НЕ ловит.**
45
+
46
+ - **Проверку, которую не по чему опознать.** Задача `mutation-diff` с именем «Mutation score on
47
+ changed files» под `continue-on-error` (нашлась в `kodus-ai`, автор сам пометил её «advisory —
48
+ does not block») не опознаётся: в имени нет слова-приметы, а команда не из списка. Объяви такую
49
+ проверку гейтом в `.aqk.yml` — тогда она станет видна точно, а не по догадке.
50
+ - **Провал, погашенный внутри скрипта.** `set +e`, `trap`, `exit 0` в конце `run: |` — это уже
51
+ логика скрипта, а не конфиг конвейера.
52
+ - **Шаг, который проходит по другой причине.** Тест, всегда возвращающий 0, — не эта запись,
53
+ а `test-has-assertion`.
54
+ - **`if: always()`** маскировкой не считается: он про порядок выполнения, а не про вердикт.
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env sh
2
+ # Конвейер, который гасит провал проверки: шаг выполнен, круг зелёный, проверка не сработала.
3
+ #
4
+ # ЗАЧЕМ ОТДЕЛЬНО ОТ «гейт запускается конвейером». Та проверка отвечает на вопрос «упомянут ли»,
5
+ # и на этом останавливается. Мы нашли дыру в собственной оснастке: конфиг с `run: pytest || true`
6
+ # проходил её зелёным — команда упомянута, а провалиться не может никогда. «Упомянут» и
7
+ # «работает» — разные утверждения, и весь этот стандарт стоит на том, чтобы их не путать.
8
+ DIR="${1:-.}"
9
+
10
+ CI=$(find "$DIR/.github/workflows" "$DIR/.gitlab-ci.yml" "$DIR/.circleci" "$DIR/Jenkinsfile" \
11
+ -type f 2>/dev/null)
12
+ [ -z "$CI" ] && { echo "конвейера нет — эта проверка не про тебя"; exit 0; }
13
+
14
+ # Что считается ПРОВЕРКОЙ. Список намеренно закрытый: маскировка бывает законной — необязательная
15
+ # выгрузка отчёта, публикация артефакта, уведомление. Красить всякий `continue-on-error` значит
16
+ # получить гейт, который выключат первым. Красим только гашение того, что выносит вердикт.
17
+ RUNNERS='aqk|doctor --run|pytest|tox|nox|unittest|jest|vitest|mocha|jasmine|karma|playwright|cypress|eslint|tsc|ruff|flake8|pylint|mypy|pyright|bandit|semgrep|gitleaks|trivy|rubocop|golangci-lint|golint|govet|go vet|go test|staticcheck|shellcheck|hadolint|actionlint|codespell|reviewdog|cargo test|cargo clippy|mvn|gradle|phpstan|psalm|npm test|npm run (test|lint|check|typecheck)|yarn (test|lint)|pnpm (test|lint)|make (test|lint|check)'
18
+
19
+ # Закрытый список не поспевает: замер по чужим репозиториям нашёл шаг «Run reviewdog
20
+ # (github-pr-check)» под `continue-on-error: true`, и ни одно имя из списка в нём не звучало.
21
+ # Поэтому вторая примета — СЛОВО в имени шага или в команде. Целым словом: «checkout» не
22
+ # «check», иначе первый же `actions/checkout` красил бы каждый конвейер на свете.
23
+ WORDS='([Ll]int|[Tt]est|[Cc]heck|[Vv]erify|[Aa]udit|[Ss]can|[Tt]ypecheck|[Cc]overage)([^A-Za-z]|$)'
24
+
25
+ # Проверка из манифеста — тоже проверка, как бы она ни называлась в этом проекте.
26
+ MAN="$DIR/.aqk.yml"
27
+ if [ -f "$MAN" ]; then
28
+ KEYS=$(tr -d '\r' < "$MAN" | awk '/^gates:/{g=1;next} /^[A-Za-z]/{g=0} g && /^[[:space:]]+[A-Za-z0-9_-]+:/{
29
+ sub(/^[[:space:]]*[A-Za-z0-9_-]*:[[:space:]]*/,""); gsub(/^"|"$/,"");
30
+ n=split($0,w," "); for(i=1;i<=n;i++) if (index(w[i],"/")) { print w[i]; break }
31
+ }')
32
+ fi
33
+
34
+ BAD=""
35
+ for F in $CI; do
36
+ # Шаги, чей исход ПЕРЕСПРАШИВАЮТ ниже: `continue-on-error` на них стоит не ради прощения
37
+ # провала, а чтобы дали выполниться шагам после — а вердикт выносится отдельным шагом
38
+ # `if: steps.<id>.outcome == 'failure'` → `exit 1`. Найдено замером по fastapi
39
+ # (`.github/workflows/pre-commit.yml`): проверка идёт под маской, потом чинит файлы и пушит
40
+ # их в ветку, и только в конце роняет сборку. Гейт молчал там по случайности — слово-примета
41
+ # не совпало; назови они шаг «lint», он покрасил бы законный уклад.
42
+ #
43
+ # Засчитывается только когда в файле есть И ссылка на исход, И падение: переспросить исход и
44
+ # ничего с ним не сделать — то же самое прощение, только длиннее.
45
+ REDEEMED=""
46
+ if tr -d '\r' < "$F" | grep -qE '^[[:space:]]*(-[[:space:]]+)?run[[:space:]]*:.*(exit[[:space:]]+1|^[[:space:]]*false[[:space:]]*$)'; then
47
+ REDEEMED=$(tr -d '\r' < "$F" | sed -n "s/.*steps\.\([A-Za-z0-9_-]*\)\.\(outcome\|conclusion\|result\).*/\1/p" | sort -u)
48
+ fi
49
+ # Разбор ПО ШАГАМ, а не по строкам. Построчно проверка врала в обе стороны: законный
50
+ # `continue-on-error` на шаге выгрузки отчёта красил соседний шаг с тестами, а слово «test»
51
+ # внутри перечисления типов коммита («feat|fix|test|chore») делало проверкой строку, которая
52
+ # ничего не проверяет. Замер по чужим конвейерам дал 4 ложных из 10 — переписано на блоки.
53
+ #
54
+ # Шаг начинается элементом списка («- ») или ключом верхнего уровня: так устроен и github,
55
+ # и gitlab, где `allow_failure` живёт на уровне задачи.
56
+ RES=$(tr -d '\r' < "$F" | awk -v runners="$RUNNERS" -v words="$WORDS" -v keys="$KEYS" -v file="$F" -v redeemed="$REDEEMED" '
57
+ function isComment(l) { return l ~ /^[[:space:]]*#/ }
58
+ function looksLikeCheck(l, j, nk) {
59
+ if (isComment(l)) return 0
60
+ # `uses:` — чужое действие. Его провал бывает законно необязательным: выгрузка отчёта,
61
+ # комментарий в пул-реквест, уведомление. Вердикт выносит то, что ЗАПУСКАЮТ.
62
+ if (l ~ /^[[:space:]]*(-[[:space:]]+)?uses[[:space:]]*:/) return 0
63
+ if (l ~ runners) return 1
64
+ # Проверка, объявленная в манифесте ЭТОГО проекта, — тоже проверка, как бы она ни
65
+ # называлась. Это самая точная примета из трёх: не догадка по имени, а список, который
66
+ # проект написал сам.
67
+ nk = split(keys, K, "\n")
68
+ for (j = 1; j <= nk; j++) if (K[j] != "" && index(l, K[j])) return 1
69
+ # Слово-примета — ТОЛЬКО в имени шага. В теле команды оно ловит своё же упоминание:
70
+ # «grep -oE (feat|fix|test|chore)» — это разбор заголовка коммита, а не проверка.
71
+ if (l ~ /^[[:space:]]*(-[[:space:]]+)?name[[:space:]]*:/ && l ~ words) return 1
72
+ return 0
73
+ }
74
+ function isMask(l) {
75
+ return !isComment(l) && l ~ /^[[:space:]]*(continue-on-error|allow_failure|ignore_failure)[[:space:]]*:[[:space:]]*(true|yes)/
76
+ }
77
+ function isBoundary(l) { return l ~ /^[[:space:]]*-[[:space:]]/ || l ~ /^[A-Za-z_.-]+[[:space:]]*:/ }
78
+ # Исход этого шага переспрашивают ниже — маска на нём законна.
79
+ function isRedeemed(id, j, nr) {
80
+ if (id == "") return 0
81
+ nr = split(redeemed, R, "\n")
82
+ for (j = 1; j <= nr; j++) if (R[j] != "" && R[j] == id) return 1
83
+ return 0
84
+ }
85
+ function flush( ) {
86
+ if (blockStart && blockCheck && blockMask && !isRedeemed(blockId))
87
+ printf "%s:%d: проверка не может провалиться — шаг под %s\n", file, blockCheckLine, blockMaskText
88
+ blockStart = 0; blockCheck = 0; blockMask = 0; blockId = ""
89
+ }
90
+ {
91
+ # Гашение прямо в команде красится только для ЗАКРЫТОГО списка запускалок: «|| true» на
92
+ # вспомогательной команде внутри скрипта (`docker network create … || true`) — это
93
+ # идемпотентность, а не выключенная проверка.
94
+ if (!isComment($0) && $0 ~ runners && $0 ~ /\|\|[[:space:]]*(true|:|exit[[:space:]]+0)/) {
95
+ line = $0; sub(/^[[:space:]]+/, "", line)
96
+ printf "%s:%d: провал погашен прямо в команде: %s\n", file, NR, substr(line, 1, 90)
97
+ }
98
+ if (isBoundary($0)) flush()
99
+ if (!blockStart) blockStart = NR
100
+ if (!blockCheck && looksLikeCheck($0)) { blockCheck = 1; blockCheckLine = NR }
101
+ if (isMask($0)) { blockMask = 1; blockMaskText = $0; sub(/^[[:space:]]+/, "", blockMaskText) }
102
+ if (!isComment($0) && $0 ~ /^[[:space:]]*(-[[:space:]]+)?id[[:space:]]*:/) {
103
+ blockId = $0; sub(/^[^:]*:[[:space:]]*/, "", blockId); gsub(/[[:space:]"'"'"']/, "", blockId)
104
+ }
105
+ }
106
+ END { flush() }' 2>/dev/null)
107
+ [ -z "$RES" ] || BAD="$BAD$RES
108
+ "
109
+ done
110
+
111
+ LEFT="$(printf '%s' "$BAD" | grep -v '^$')"
112
+ [ -z "$LEFT" ] && exit 0
113
+ printf '%s\n' "$LEFT"
114
+ echo " почини: убери «|| true» и «continue-on-error» с шага, который выносит вердикт."
115
+ echo " шаг, который не может провалиться, — это не проверка, а строка в логе."
116
+ exit 1
@@ -0,0 +1,14 @@
1
+ intent: шаг конвейера, выносящий вердикт, может провалиться — а не только выполниться
2
+ intent_en: a pipeline step that renders a verdict can actually fail, not merely run
3
+
4
+ # Только там, где конвейер есть. Проверять его отсутствие — дело другой записи.
5
+ trigger:
6
+ has_ci: true
7
+
8
+ recipes:
9
+ any: bash {gate}/check.sh {dir}
10
+
11
+ proof: incidents/README.md, 2026-09-06 «упомянут и работает — разные утверждения» — дыра
12
+ найдена в собственной оснастке (`gates-run-in-ci` пропускал `pytest || true` зелёным),
13
+ замер по пятнадцати чужим репозиториям подтвердил класс: у `reviewdog` два шага
14
+ собственных линтеров идут под `continue-on-error: true`
@@ -0,0 +1,30 @@
1
+ name: ci
2
+ on: [push]
3
+ jobs:
4
+ build:
5
+ runs-on: ubuntu-latest
6
+ steps:
7
+ - run: npm ci
8
+ - name: тесты
9
+ run: pytest
10
+ - name: типы
11
+ run: tsc --noEmit
12
+ - name: выгрузить отчёт
13
+ run: bash scripts/upload-report.sh
14
+ continue-on-error: true
15
+
16
+ # Законный уклад: шаг помечен continue-on-error не для того, чтобы простить провал, а
17
+ # чтобы дать выполниться шагам после него; вердикт выносится ниже, по его исходу.
18
+ # Найдено замером по fastapi (.github/workflows/pre-commit.yml): проверка прогоняется
19
+ # под continue-on-error, потом чинит файлы и пушит их в ветку, а в конце падает, если
20
+ # проверка была красной. Гейт, красящий такое, требует убрать то, без чего уклад не
21
+ # работает, — и его выключат целиком.
22
+ - name: линтер
23
+ id: lint
24
+ run: ruff check .
25
+ continue-on-error: true
26
+ - name: применить починки и запушить
27
+ run: bash scripts/push-fixes.sh
28
+ - name: провалить сборку, если линтер был красным
29
+ if: steps.lint.outcome == 'failure'
30
+ run: exit 1
@@ -0,0 +1,12 @@
1
+ name: ci
2
+ on: [push]
3
+ jobs:
4
+ build:
5
+ runs-on: ubuntu-latest
6
+ steps:
7
+ - run: npm ci
8
+ - name: тесты
9
+ run: pytest || true
10
+ - name: типы
11
+ run: tsc --noEmit
12
+ continue-on-error: true
@@ -0,0 +1,15 @@
1
+ # Исход проверки переспрашивают — и ничего с ним не делают. Ровно то же прощение, что и
2
+ # голый continue-on-error, только длиннее на три строки и убедительнее на вид.
3
+ name: soft
4
+ on: [push]
5
+ jobs:
6
+ build:
7
+ runs-on: ubuntu-latest
8
+ steps:
9
+ - name: линтер
10
+ id: lint
11
+ run: ruff check .
12
+ continue-on-error: true
13
+ - name: сказать вслух
14
+ if: steps.lint.outcome == 'failure'
15
+ run: echo "линтер был красным"
@@ -5,7 +5,19 @@
5
5
  # который в тёмной теме остаётся светлым пятном, — и это видно не автору, а пользователю,
6
6
  # который переключил тему. Дефект тихий: в теме автора всё выглядит правильно.
7
7
  DIR="${1:-.}"
8
- . "$(dirname "$0")/../_skip.sh" 2>/dev/null || SKIP_NAMES=".git .aqk node_modules .venv"
8
+ # Существование файла проверяется ДО `.`, а не запасной веткой после. Прежняя строка
9
+ # `. файл 2>/dev/null || запасной_вариант` выглядела страховкой и ею не была: под `sh` (dash)
10
+ # неудачный `.` завершает скрипт немедленно, и ветка после `||` не выполняется никогда; под
11
+ # `bash`, которым гейты и запускаются из манифеста, она выполняется, но подставляет только
12
+ # ПЕРЕМЕННУЮ — функции обхода остаются неопределёнными, конвейер печатает пустоту, и проверка
13
+ # выходит с НУЛЁМ. Замерено 2026-09-08 на файле с настоящим нарушением: гейт сказал «чисто».
14
+ SKIP_LIB="$(dirname "$0")/../_skip.sh"
15
+ if [ ! -f "$SKIP_LIB" ]; then
16
+ echo "рядом с проверкой нет _skip.sh — обход не собран, проверка не состоялась"
17
+ echo " почини: скопируй гейт вместе с файлом kit/gates/_skip.sh, он общий на весь каталог"
18
+ exit 2
19
+ fi
20
+ . "$SKIP_LIB"
9
21
 
10
22
  # Расширения, где живёт ТЕМИЗИРУЕМЫЙ интерфейс. Своё, а не общий CODE_EXT: там нет ни css, ни
11
23
  # vue — они не код в смысле «отладочная печать», но именно в них живёт цвет.
@@ -12,8 +12,18 @@
12
12
  **Почему машина, а не внимательность.** Написать отчёт «на будущее» — первое, что пропускают под
13
13
  давлением дедлайна. Гейт делает это ценой, а не пожеланием.
14
14
 
15
- **Готовый аналог.** Не искал: это не класс проверок, который держат готовые линтеры они читают
16
- код, а не историю коммитов.
15
+ **Готовый аналог есть, и раньше здесь было написано «не искал».** Искали 2026-09-07, при ревизии
16
+ каталога. Историю коммитов держат [`gitlint`](https://jorisroovers.com/gitlint/) и
17
+ [`commitlint`](https://commitlint.js.org/): проверяют форму заголовка, тип по Conventional
18
+ Commits, длину строк, пустую строку между заголовком и телом; у `gitlint` есть даже
19
+ `body-min-length` и возможность дописать своё правило на Python.
20
+
21
+ **Почему рецепта под них здесь нет.** Оба проверяют, что тело **есть** и как оно оформлено. Эта
22
+ запись требует другого: чтобы в теле стояли два названных раздела — что сделано и **в чём агент
23
+ не уверен**. Второго нет ни в Conventional Commits, ни во встроенных правилах обоих. Написать
24
+ своё правило `gitlint` можно, но это код на Python в проекте, который может быть не на Python, —
25
+ и он всё равно наш, только в чужой обёртке. Если `commitlint` у вас уже стоит — он закрывает
26
+ форму заголовка, чего не делаем мы; записи это не отменяет.
17
27
 
18
28
  **Чего НЕ ловит.** Не проверяет качество отчёта, только его наличие — «Сделано: починил» и «Не
19
29
  уверен: не знаю» формально пройдут. Не проверяет, что отчёт правдив. Это ограничение того же
@@ -39,7 +49,7 @@
39
49
 
40
50
  Гейт всё-таки местный: подсказка «допиши в тело коммита» выполнима до пуша, а не после.
41
51
 
42
- **Образцы.** `red/COMMIT_MSG` — обычное тело коммита без отчёта. `green/COMMIT_MSG` — то же самое
52
+ **Образцы.** `red/.aqk-commit-msg` — обычное тело коммита без отчёта. `green/.aqk-commit-msg` — то же самое
43
53
  плюс `Сделано:` и `Не уверен:`. Образцы — текстовые файлы, а не настоящий git: арбитр в реальном
44
54
  проекте читает `git log -1`, а вложенный `.git` внутри каталога комплекта создал бы embedded-
45
55
  репозиторий, который сам по себе стал бы проблемой версионирования.