agent-quality-kit 0.6.0 → 0.8.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 (126) hide show
  1. package/README.md +61 -2
  2. package/README.ru.md +61 -2
  3. package/kit/docs/ai/agent-harness-playbook.md +1 -1
  4. package/kit/docs/ready-made-rules.md +29 -4
  5. package/kit/gates/README.md +40 -0
  6. package/kit/gates/_skip.sh +61 -1
  7. package/kit/gates/ci-actually-fails/README.md +12 -0
  8. package/kit/gates/ci-actually-fails/check.sh +26 -3
  9. package/kit/gates/ci-actually-fails/green/.github/workflows/ci.yml +16 -0
  10. package/kit/gates/ci-actually-fails/red/.github/workflows/soft.yml +15 -0
  11. package/kit/gates/ci-not-hijackable/README.md +56 -0
  12. package/kit/gates/ci-not-hijackable/check.sh +73 -0
  13. package/kit/gates/ci-not-hijackable/gate.yml +19 -0
  14. package/kit/gates/ci-not-hijackable/green/.github/workflows/triage.yml +19 -0
  15. package/kit/gates/ci-not-hijackable/red/.github/workflows/triage.yml +18 -0
  16. package/kit/gates/color-from-token/check.sh +19 -3
  17. package/kit/gates/color-from-token/green/Button.tsx +2 -0
  18. package/kit/gates/commit-explains-itself/README.md +13 -3
  19. package/kit/gates/commit-explains-itself/check.sh +8 -4
  20. package/kit/gates/complexity-limit/README.md +5 -0
  21. package/kit/gates/complexity-limit/check.sh +27 -9
  22. package/kit/gates/complexity-limit/green/test_fixtures.py +14 -0
  23. package/kit/gates/deps-are-pinned/README.md +14 -1
  24. package/kit/gates/deps-are-pinned/check.sh +6 -1
  25. package/kit/gates/deps-are-pinned/green/pyproject-with-requirements/pyproject.toml +12 -0
  26. package/kit/gates/deps-are-pinned/green/pyproject-with-requirements/requirements.txt +3 -0
  27. package/kit/gates/deps-are-pinned/red/pyproject-loose/pyproject.toml +12 -0
  28. package/kit/gates/deps-are-pinned/red/pyproject-loose/requirements.txt +3 -0
  29. package/kit/gates/duplicate-code/README.md +11 -2
  30. package/kit/gates/duplicate-code/check.sh +36 -5
  31. package/kit/gates/duplicate-code/gate.yml +8 -0
  32. package/kit/gates/duplicate-code/green/imports_a.go +20 -0
  33. package/kit/gates/duplicate-code/green/imports_b.go +19 -0
  34. package/kit/gates/entry-links-exist/README.md +5 -0
  35. package/kit/gates/entry-links-exist/check.sh +10 -1
  36. package/kit/gates/entry-links-exist/green/AGENTS.md +5 -0
  37. package/kit/gates/file-size-limit/README.md +9 -2
  38. package/kit/gates/file-size-limit/check.sh +14 -2
  39. package/kit/gates/gate-not-weakened/check.sh +13 -1
  40. package/kit/gates/hook-actually-fires/README.md +74 -0
  41. package/kit/gates/hook-actually-fires/check.sh +183 -0
  42. package/kit/gates/hook-actually-fires/gate.yml +15 -0
  43. package/kit/gates/hook-actually-fires/green/.claude/hooks/hooks.json +3 -0
  44. package/kit/gates/hook-actually-fires/green/.claude/settings.json +74 -0
  45. package/kit/gates/hook-actually-fires/green/.claude/settings.local.json +74 -0
  46. package/kit/gates/hook-actually-fires/red/.claude/hooks/hooks.json +4 -0
  47. package/kit/gates/hook-actually-fires/red/.claude/settings.json +53 -0
  48. package/kit/gates/no-phantom-package/README.md +84 -0
  49. package/kit/gates/no-phantom-package/check.sh +161 -0
  50. package/kit/gates/no-phantom-package/gate.yml +20 -0
  51. package/kit/gates/no-phantom-package/green/AGENTS.md +15 -0
  52. package/kit/gates/no-phantom-package/red/AGENTS.md +15 -0
  53. package/kit/gates/no-print-in-prod/README.md +33 -39
  54. package/kit/gates/no-print-in-prod/gate.yml +14 -6
  55. package/kit/gates/personal-config-not-shared/README.md +66 -0
  56. package/kit/gates/personal-config-not-shared/check.sh +103 -0
  57. package/kit/gates/personal-config-not-shared/gate.yml +16 -0
  58. package/kit/gates/personal-config-not-shared/green/.aqk-tracked +9 -0
  59. package/kit/gates/personal-config-not-shared/red/.aqk-tracked +6 -0
  60. package/kit/gates/secrets-not-in-code/check.sh +29 -4
  61. package/kit/gates/secrets-not-in-code/green/testdata/certificate/key.pem +3 -0
  62. package/kit/gates/swallowed-error/README.md +36 -18
  63. package/kit/gates/swallowed-error/gate.yml +13 -3
  64. package/kit/gates/test-has-assertion/check.sh +13 -1
  65. package/kit/gates/test-not-adjusted/README.md +79 -0
  66. package/kit/gates/test-not-adjusted/check.sh +136 -0
  67. package/kit/gates/test-not-adjusted/gate.yml +19 -0
  68. package/kit/gates/test-not-adjusted/green/after/calc.py +6 -0
  69. package/kit/gates/test-not-adjusted/green/after/tests/test_calc.py +9 -0
  70. package/kit/gates/test-not-adjusted/green/before/calc.py +2 -0
  71. package/kit/gates/test-not-adjusted/green/before/tests/test_calc.py +5 -0
  72. package/kit/gates/test-not-adjusted/red/after/calc.py +2 -0
  73. package/kit/gates/test-not-adjusted/red/after/tests/test_calc.py +5 -0
  74. package/kit/gates/test-not-adjusted/red/before/calc.py +2 -0
  75. package/kit/gates/test-not-adjusted/red/before/tests/test_calc.py +7 -0
  76. package/kit/gates/todo-without-task/README.md +6 -0
  77. package/kit/gates/todo-without-task/check.sh +14 -2
  78. package/kit/gates/todo-without-task/green/app.py +1 -0
  79. package/kit/ratchet/ratchet.sh +70 -2
  80. package/kit/rules/general.md +9 -0
  81. package/kit/rules-en/general.md +82 -0
  82. package/kit/rules-en/security.md +33 -0
  83. package/kit/rules-en/testing.md +48 -0
  84. package/llms.txt +22 -1
  85. package/package.json +6 -2
  86. package/tool/commands/badge.mjs +7 -1
  87. package/tool/commands/context.mjs +260 -0
  88. package/tool/commands/doctor.mjs +72 -25
  89. package/tool/commands/gates.mjs +10 -4
  90. package/tool/commands/learn.mjs +159 -0
  91. package/tool/commands/project.mjs +9 -1
  92. package/tool/commands/prove.mjs +67 -0
  93. package/tool/commands/report.mjs +37 -2
  94. package/tool/i18n/en-docs.mjs +154 -0
  95. package/tool/i18n/en.mjs +70 -90
  96. package/tool/i18n/ru-docs.mjs +156 -0
  97. package/tool/i18n/ru.mjs +69 -90
  98. package/tool/i18n/templates-en.mjs +1 -1
  99. package/tool/i18n/templates-ru.mjs +1 -1
  100. package/tool/lib/core.mjs +37 -2
  101. package/tool/lib/evidence.mjs +124 -0
  102. package/tool/lib/manifest.mjs +63 -5
  103. package/tool/lib/prove.mjs +172 -0
  104. package/tool/lib/repo.mjs +31 -2
  105. package/tool/lib/scope.mjs +46 -2
  106. package/tool/lib/templates.mjs +3 -0
  107. package/tool/program.mjs +23 -22
  108. package/tool/selfcheck/gates.sh +66 -0
  109. package/tool/selfcheck/mutation.sh +21 -1
  110. package/tool/selfcheck/smoke.sh +519 -39
  111. package/tool/selfcheck/units-context.mjs +186 -0
  112. package/tool/selfcheck/units-evidence.mjs +83 -0
  113. package/tool/selfcheck/units-learn.mjs +88 -0
  114. package/tool/selfcheck/units-level.mjs +122 -0
  115. package/tool/selfcheck/units.mjs +114 -2
  116. package/kit/gates/no-print-in-prod/check.sh +0 -38
  117. package/kit/gates/no-print-in-prod/green/docs.ts +0 -15
  118. package/kit/gates/no-print-in-prod/green/main.go +0 -8
  119. package/kit/gates/no-print-in-prod/green/main.rs +0 -4
  120. package/kit/gates/no-print-in-prod/red/main.go +0 -8
  121. package/kit/gates/no-print-in-prod/red/main.rs +0 -4
  122. package/kit/gates/swallowed-error/check.sh +0 -54
  123. package/kit/gates/swallowed-error/green/run.js +0 -8
  124. package/kit/gates/swallowed-error/red/run.js +0 -3
  125. /package/kit/gates/commit-explains-itself/green/{COMMIT_MSG → .aqk-commit-msg} +0 -0
  126. /package/kit/gates/commit-explains-itself/red/{COMMIT_MSG → .aqk-commit-msg} +0 -0
package/README.md CHANGED
@@ -63,6 +63,48 @@ tooling: [the dark factory and the minimum that isn't optional](kit/docs/ai/proj
63
63
  a promise without an exit code is just a sentence
64
64
  ```
65
65
 
66
+ ## Every command
67
+
68
+ ```
69
+ aqk doctor what this repository is at, and what is missing
70
+ aqk doctor --run run every gate the manifest declares
71
+ aqk doctor --run --since main only what the diff introduced
72
+ aqk doctor --run --min 1 fail a pipeline below a level
73
+ aqk doctor --baseline the minimum a project needs, confirmed by a run
74
+
75
+ aqk init lay the kit into an existing repository
76
+ aqk start start a new project from the kit
77
+ aqk add <name> install one guard from the catalogue
78
+ aqk new <name> scaffold a guard of your own
79
+ aqk find <text> find a guard by intent
80
+ aqk why <name> what failure this guard was written for
81
+
82
+ aqk prove run every declared gate against its own samples:
83
+ red on the red one, quiet on the green one
84
+ aqk report the report form, assembled by a run
85
+ aqk report --since main ...plus what proves this diff, file by file
86
+ aqk badge write the level badge into the README
87
+ aqk badge --check fail if the badge disagrees with a run
88
+
89
+ aqk context the repository state in one block, for an agent's context:
90
+ level, what is red now, rules nobody enforces, ratchets
91
+ aqk context --full the same plus the command map and the rulebook verbatim (~7000
92
+ tokens against ~375: the price of an agent that does not guess)
93
+ aqk context --install put a SessionStart hook into .claude/settings.json
94
+ (add --full to install the full block)
95
+
96
+ aqk learn rule candidates from local transcripts:
97
+ said out loud, never written down
98
+ aqk note "..." write a bruise into the journal
99
+ aqk ratchet <name> a debt registry for a declared gate: may only get shorter
100
+ aqk blob every guide as a single file
101
+ ```
102
+
103
+ Exit codes: `0` pass, `1` below the level or a gate failed. Two exceptions, both deliberate:
104
+ `learn` never fails a build — it reads transcripts and prints to the terminal only, writing
105
+ nothing. And `--baseline` is an inspection, not a run: it always exits `0`, so combining it with
106
+ `--run` or `--min` is refused outright rather than handing you a pipeline that cannot go red.
107
+
66
108
  ## What this looks like
67
109
 
68
110
  Someone else's project, three files, nothing configured:
@@ -123,6 +165,7 @@ The whole standard is one `.aqk.yml` file in the repository root:
123
165
  aqk: 1
124
166
  entry: [AGENTS.md] # what the agent reads first
125
167
  rules: .aqk/rules # where the standards live
168
+ docs: .aqk/docs # where the guides live (optional; this is the default)
126
169
  gates: # what must pass — as commands, not as prose
127
170
  lint: "npm run lint"
128
171
  secrets-not-in-code: "bash gates/secrets-not-in-code/check.sh ."
@@ -196,7 +239,7 @@ Already using [pre-commit](https://pre-commit.com)? Three lines in the file you
196
239
  ```yaml
197
240
  repos:
198
241
  - repo: https://github.com/arsen-ask-lx/Agent_Quality_Kit
199
- rev: v0.6.0
242
+ rev: v0.8.0
200
243
  hooks:
201
244
  - id: aqk # runs what the repository declares; blocks below AQK-1
202
245
  # - id: aqk-doctor # read-only: the level and what is missing, blocks nothing
@@ -217,7 +260,7 @@ layer AQK adds.
217
260
  [![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
261
 
219
262
  ```yaml
220
- - uses: arsen-ask-lx/Agent_Quality_Kit@v0.5.0
263
+ - uses: arsen-ask-lx/Agent_Quality_Kit@v0.8.0
221
264
  with:
222
265
  min: 1 # the build fails below AQK-1, or if any declared gate failed
223
266
  ```
@@ -260,6 +303,22 @@ Every `doctor --run` rewrites `.aqk/last-run.md` — a short report of what actu
260
303
  long it took. The list of gates in the manifest says nothing about how many of them are alive
261
304
  right now; the report does. The file is ephemeral — keep it in your own `.gitignore`.
262
305
 
306
+ ### Introducing a rule into a live project
307
+
308
+ Three ways, and each has a price. A big clean-up is put off forever because it is big. The
309
+ ratchet turns existing violations into debt and blocks new ones — right once the rule is agreed.
310
+ And while it is still being argued about, an advisory gate shows findings without failing the run:
311
+
312
+ ```yaml
313
+ advisory:
314
+ - complexity-limit
315
+ ```
316
+
317
+ Declared in the manifest, not passed as a flag. A flag that says "fail nothing" downgrades every
318
+ check at once, is invisible in the diff, and is never named in the summary — that is
319
+ `continue-on-error`, which this tool marks red elsewhere. The list is printed on **every** run:
320
+ an advisory gate everyone forgot about is a switched-off check.
321
+
263
322
  ## When a bug slips past the guards
264
323
 
265
324
  ```bash
package/README.ru.md CHANGED
@@ -64,6 +64,48 @@ flowchart LR
64
64
  обещание без кода возврата — просто предложение
65
65
  ```
66
66
 
67
+ ## Все команды
68
+
69
+ ```
70
+ aqk doctor где этот репозиторий и чего в нём не хватает
71
+ aqk doctor --run прогнать все гейты, объявленные в манифесте
72
+ aqk doctor --run --since main только то, что внёс диф
73
+ aqk doctor --run --min 1 уронить конвейер ниже ступени
74
+ aqk doctor --baseline обязательный минимум проекта, подтверждённый прогоном
75
+
76
+ aqk init разложить комплект в существующий репозиторий
77
+ aqk start начать новый проект с комплектом
78
+ aqk add <имя> поставить один гейт из каталога
79
+ aqk new <имя> завести свой гейт по форме
80
+ aqk find <текст> найти гейт по намерению
81
+ aqk why <имя> какой отказ этот гейт поймал
82
+
83
+ aqk prove прогнать каждый объявленный гейт по его образцам:
84
+ красный на красном, тишина на зелёном
85
+ aqk report форма отчёта, собранная прогоном
86
+ aqk report --since main ...и чем доказан этот диф, файл за файлом
87
+ aqk badge вписать значок уровня в README
88
+ aqk badge --check упасть, если значок расходится с прогоном
89
+
90
+ aqk context состояние репозитория одним блоком, для контекста агента:
91
+ уровень, что красное сейчас, правила без арбитра, храповики
92
+ aqk context --full то же плюс карта команд и свод правил дословно (≈7000 токенов
93
+ против ≈375 — плата за то, чтобы агент не догадывался)
94
+ aqk context --install поставить хук SessionStart в .claude/settings.json
95
+ (с --full ставится полный блок)
96
+
97
+ aqk learn кандидаты в правила из локальной переписки:
98
+ что сказано вслух и не записано
99
+ aqk note "..." записать шишку в журнал
100
+ aqk ratchet <имя> реестр долга для объявленного гейта: может только укорачиваться
101
+ aqk blob все методички одним файлом
102
+ ```
103
+
104
+ Коды возврата: `0` — прошло, `1` — ниже ступени или упал гейт. Два исключения, оба намеренные:
105
+ `learn` не роняет сборку никогда — он читает переписку и печатает только в терминал, не записывая
106
+ ничего. А `--baseline` — осмотр, а не прогон: он выходит с нулём всегда, поэтому вместе с `--run`
107
+ или `--min` он теперь отказывает вслух, а не выдаёт конвейер, который не может покраснеть.
108
+
67
109
  ## Что это выглядит так
68
110
 
69
111
  Чужой проект, три файла, ничего не настроено:
@@ -124,6 +166,7 @@ Issue» — ничего не постится сама, только текст
124
166
  aqk: 1
125
167
  entry: [AGENTS.md] # что агент читает первым
126
168
  rules: .aqk/rules # где стандарты
169
+ docs: .aqk/docs # где методички (необязательно, это и есть умолчание)
127
170
  gates: # что обязано пройти — командами, не словами
128
171
  lint: "npm run lint"
129
172
  secrets-not-in-code: "bash gates/secrets-not-in-code/check.sh ."
@@ -198,7 +241,7 @@ aqk badge --check # в конвейере: код 1 в тот день, ког
198
241
  ```yaml
199
242
  repos:
200
243
  - repo: https://github.com/arsen-ask-lx/Agent_Quality_Kit
201
- rev: v0.6.0
244
+ rev: v0.8.0
202
245
  hooks:
203
246
  - id: aqk # запускает объявленное; роняет коммит ниже AQK-1
204
247
  # - id: aqk-doctor # только осмотр: уровень и чего не хватает, ничего не роняет
@@ -217,7 +260,7 @@ repos:
217
260
  [![в 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
261
 
219
262
  ```yaml
220
- - uses: arsen-ask-lx/Agent_Quality_Kit@v0.5.0
263
+ - uses: arsen-ask-lx/Agent_Quality_Kit@v0.8.0
221
264
  with:
222
265
  min: 1 # сборка падает ниже AQK-1 или если упал любой объявленный гейт
223
266
  ```
@@ -304,6 +347,22 @@ Ruff, ESLint и gitleaks и так находят плохой код — AQK з
304
347
  [`agents-lint`](https://github.com/giacomo/agents-lint) делает это лучше, другую — потому что
305
348
  91 находка из 120 оказалась законным приёмом.
306
349
 
350
+ ### Как ввести правило в живой проект
351
+
352
+ Три способа, и у каждого своя цена. Большая чистка откладывается навсегда, потому что она
353
+ большая. Храповик превращает старые нарушения в долг и блокирует новые — верно, когда правило
354
+ уже принято. А пока о правиле спорят, совещательный гейт показывает находки, не роняя прогон:
355
+
356
+ ```yaml
357
+ advisory:
358
+ - complexity-limit
359
+ ```
360
+
361
+ Объявлением в манифесте, а не флагом. Флаг «не роняй ничего» понижает все проверки разом, не
362
+ виден в дифе и не назван в итоге — это тот самый `continue-on-error`, который мы сами красим.
363
+ Список печатается **каждый** прогон: совещательный гейт, о котором забыли, — это выключенная
364
+ проверка.
365
+
307
366
  ## Методички одним файлом
308
367
 
309
368
  ```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 через
@@ -174,10 +174,12 @@ ruff check --select TRY400 --statistics . # сколько находок У
174
174
  К осени 2026 появился отдельный класс инструментов — линтеры не кода, а того, что читает агент:
175
175
  `AGENTS.md`, `CLAUDE.md`, файлы навыков, конфиги хуков и MCP. Писать своё здесь незачем.
176
176
 
177
- | Инструмент | Что проверяет | Состояние на 2026-09-06 |
177
+ | Инструмент | Что проверяет | Состояние дата замера в самой ячейке |
178
178
  |---|---|---|
179
- | [`agnix`](https://github.com/agent-sh/agnix) | 455 правил: структура `CLAUDE.md`/`AGENTS.md`/`SKILL.md`, синтаксис конфигов MCP и хуков, соглашения об именах, **мёртвые ссылки на файлы**. Есть автопочинка и LSP | 404 ⭐, Rust, активен. `npm i -g agnix`, `brew`, `pip`, `cargo` |
180
- | [`agents-lint`](https://github.com/giacomo/agents-lint) | мёртвые npm-скрипты, упомянутые в `AGENTS.md`, устаревшие рамки, деревья каталогов в контексте | 13 ⭐, TypeScript, последний коммит март 2026 |
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. Без сети выходит с нулём: «не проверено» у него неотличимо от «чисто» |
181
183
 
182
184
  ```bash
183
185
  npm install -g agnix && agnix --strict .
@@ -187,7 +189,13 @@ npm install -g agnix && agnix --strict .
187
189
  формат, существование путей, наличие скриптов. Ни один не спрашивает, **подкреплено ли обещание
188
190
  командой с кодом возврата**. «Мы никогда не коммитим секреты» — грамматически безупречная
189
191
  строка, на которую ни один из них ничего не скажет. Разделение простое: обвес агента проверяет
190
- `agnix`, исполнимость обещаний — `promise-has-gate`.
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`.
191
199
 
192
200
  ## А если проект не на Python?
193
201
 
@@ -201,6 +209,23 @@ npm install -g agnix && agnix --strict .
201
209
  **Одна строка в существующей записи — самый дешёвый и самый ценный вклад.** Намерение уже
202
210
  доказано отказом, образцы уже лежат, проверять нечего кроме самой команды.
203
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
+
204
229
  ## Если готового нет
205
230
 
206
231
  Тогда свой гейт — и в его `README.md` пишется, **что именно проверено**: какой инструмент
@@ -70,6 +70,26 @@
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
+
73
93
  ## Зрелость записи не пишут руками
74
94
 
75
95
  Седьмого поля нет: зрелость **считается** из доказательства. Ссылается `proof` на журнал шишек —
@@ -90,6 +110,24 @@ lifecycle: deprecated
90
110
  superseded_by: no-print-in-prod
91
111
  ```
92
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
+
93
131
  ## Запись без переносимого рецепта
94
132
 
95
133
  Иногда переносимой проверки быть не может: чтобы понять, вызывают ли функцию, нужен граф
@@ -193,6 +231,8 @@ samples_for: python
193
231
  | `has_deps: true` | есть файл зависимостей |
194
232
  | `has_tests: true` | есть каталог тестов или файлы вида `*_test.*` |
195
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` — признак сработает у человека и не сработает в конвейере |
196
236
  | `has_ui: true` | есть стили или однофайловые компоненты (`.css`, `.scss`, `.vue`, `.svelte`, `.astro`) |
197
237
 
198
238
  Любое из `has_*` принимает и `false` — «показывать тем, у кого этого нет». Условие, которого
@@ -72,8 +72,68 @@ include_code() {
72
72
  # Сгенерированный файл не правят руками — предъявлять его размер или сложность человеку
73
73
  # бессмысленно и вредно: он выключит проверку целиком.
74
74
  # Опознаём по общепринятой шапке в первых пяти строках.
75
+ # Регистр кириллицы `grep -i` сворачивает НЕ ВЕЗДЕ: на linux сворачивает, в Git Bash под
76
+ # Windows — нет. Шапка `// СГЕНЕРИРОВАН` там не находилась никогда, и узнали мы об этом только
77
+ # 2026-09-08, когда нарочный набор впервые прогнали на windows-задании. Это не регрессия правки,
78
+ # а старая дыра, которую нечем было увидеть: на живых шести проектах таких шапок не было.
79
+ # Поэтому кириллица перечисляется явно, а не доверяется флагу: три написания, которые бывают
80
+ # на деле, — строчное в комментарии, с заглавной в начале фразы и капсом в баннере.
81
+ GENERATED_MARKERS='@generated|do not edit|autogenerated|auto-generated|generated by|сгенерирован|Сгенерирован|СГЕНЕРИРОВАН'
82
+
83
+ # Один файл: сгенерирован ли он. Осталось для тех, кто спрашивает про ОДИН файл
84
+ # (gate-not-weakened сверяет строку находки, а не список путей).
75
85
  is_generated() {
76
- head -5 "$1" 2>/dev/null | grep -qiE '@generated|do not edit|autogenerated|auto-generated|generated by|сгенерирован'
86
+ head -5 "$1" 2>/dev/null | grep -qiE "$GENERATED_MARKERS"
87
+ }
88
+
89
+ # СПИСОК файлов на stdin (по пути в строке) → тот же список без сгенерированных.
90
+ #
91
+ # ЗАЧЕМ ОТДЕЛЬНО ОТ is_generated. Цикл `while read; do is_generated; done` запускает ДВА
92
+ # процесса на каждый файл — head и grep. Замерено 2026-09-08 на 748 файлах проекта uv:
93
+ # обход и отбор 12 мс, подсчёт строк одним wc 37 мс, а этот цикл — 3862 мс. Сто крат от
94
+ # всего остального, и ни микросекунды из них не потрачено на сравнение строк: платили за
95
+ # fork+exec. Медленным был не язык — медленным было количество запусков.
96
+ #
97
+ # ПОЧЕМУ ИМЕННО grep, А НЕ awk. Соблазн был собрать всё одним awk с `nextfile`. Отвергнуто:
98
+ # `nextfile` — расширение, а `tolower` на кириллице врёт в mawk и busybox-awk, и «сгенерирован»
99
+ # перестал бы находиться там, где сейчас находится. Здесь ТОТ ЖЕ grep с ТЕМИ ЖЕ флагами, что
100
+ # и в is_generated, — значит совпадение считается ровно так же, как раньше, и меняется только
101
+ # число запусков. Равенство вывода проверено на шести чужих проектах: 0 расхождений.
102
+ #
103
+ # `-m1` останавливает grep на первом совпадении в файле, `/dev/null` заставляет его печатать
104
+ # имя файла даже когда xargs передал ровно один путь.
105
+ drop_generated() {
106
+ LIST=$(mktemp) || { cat; return; }
107
+ GEN=$(mktemp) || { rm -f "$LIST"; cat; return; }
108
+ cat > "$LIST"
109
+ # Пути передаются через НОЛЬ-разделитель: xargs по умолчанию рвёт по пробелам, и файл
110
+ # «src/my component.tsx» уехал бы в grep двумя несуществующими путями. Поймано нарочным
111
+ # набором до выпуска: старый цикл `while read` пробелы держал, и потерять это было нельзя.
112
+ tr '\n' '\0' < "$LIST" \
113
+ | xargs -0 -r grep -niE -m1 -e "$GENERATED_MARKERS" /dev/null 2>/dev/null \
114
+ | awk -v list="$LIST" '
115
+ # Строка вывода grep — «путь:номер:текст», и разобрать её по первому двоеточию нельзя:
116
+ # в пути тоже бывает двоеточие, а в тексте находки — тем более. Поэтому путь не
117
+ # угадывается, а СВЕРЯЕТСЯ со списком, который мы сами и передали: идём по двоеточиям
118
+ # слева направо, пока префикс не совпадёт с известным путём. Двоеточие в имени файла
119
+ # редкость, но «редко» и «никогда» — разные вещи, а тихо оставленный сгенерированный
120
+ # файл даёт находки на чужом коде.
121
+ BEGIN { while ((getline l < list) > 0) known[l] = 1 }
122
+ {
123
+ rest = $0; prefix = ""
124
+ while ((i = index(rest, ":")) > 0) {
125
+ prefix = prefix substr(rest, 1, i - 1)
126
+ rest = substr(rest, i + 1)
127
+ if (prefix in known) {
128
+ if (match(rest, /^[0-9]+:/) && substr(rest, 1, RLENGTH - 1) + 0 <= 5) print prefix
129
+ break
130
+ }
131
+ prefix = prefix ":"
132
+ }
133
+ }
134
+ ' | sort -u > "$GEN"
135
+ if [ -s "$GEN" ]; then grep -vxF -f "$GEN" "$LIST"; else cat "$LIST"; fi
136
+ rm -f "$LIST" "$GEN"
77
137
  }
78
138
 
79
139
  # Для grep: --exclude-dir на каждое имя. Образцы гейтов сюда не входят — см. own_samples_filter:
@@ -29,6 +29,18 @@
29
29
  для них — законная настройка, каковой она и является: незаконной её делает то, ЧТО под ней
30
30
  стоит, а это знает только проект.
31
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
+
32
44
  **Чего НЕ ловит.**
33
45
 
34
46
  - **Проверку, которую не по чему опознать.** Задача `mutation-diff` с именем «Mutation score on
@@ -33,6 +33,19 @@ fi
33
33
 
34
34
  BAD=""
35
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
36
49
  # Разбор ПО ШАГАМ, а не по строкам. Построчно проверка врала в обе стороны: законный
37
50
  # `continue-on-error` на шаге выгрузки отчёта красил соседний шаг с тестами, а слово «test»
38
51
  # внутри перечисления типов коммита («feat|fix|test|chore») делало проверкой строку, которая
@@ -40,7 +53,7 @@ for F in $CI; do
40
53
  #
41
54
  # Шаг начинается элементом списка («- ») или ключом верхнего уровня: так устроен и github,
42
55
  # и gitlab, где `allow_failure` живёт на уровне задачи.
43
- RES=$(tr -d '\r' < "$F" | awk -v runners="$RUNNERS" -v words="$WORDS" -v keys="$KEYS" -v file="$F" '
56
+ RES=$(tr -d '\r' < "$F" | awk -v runners="$RUNNERS" -v words="$WORDS" -v keys="$KEYS" -v file="$F" -v redeemed="$REDEEMED" '
44
57
  function isComment(l) { return l ~ /^[[:space:]]*#/ }
45
58
  function looksLikeCheck(l, j, nk) {
46
59
  if (isComment(l)) return 0
@@ -62,10 +75,17 @@ for F in $CI; do
62
75
  return !isComment(l) && l ~ /^[[:space:]]*(continue-on-error|allow_failure|ignore_failure)[[:space:]]*:[[:space:]]*(true|yes)/
63
76
  }
64
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
+ }
65
85
  function flush( ) {
66
- if (blockStart && blockCheck && blockMask)
86
+ if (blockStart && blockCheck && blockMask && !isRedeemed(blockId))
67
87
  printf "%s:%d: проверка не может провалиться — шаг под %s\n", file, blockCheckLine, blockMaskText
68
- blockStart = 0; blockCheck = 0; blockMask = 0
88
+ blockStart = 0; blockCheck = 0; blockMask = 0; blockId = ""
69
89
  }
70
90
  {
71
91
  # Гашение прямо в команде красится только для ЗАКРЫТОГО списка запускалок: «|| true» на
@@ -79,6 +99,9 @@ for F in $CI; do
79
99
  if (!blockStart) blockStart = NR
80
100
  if (!blockCheck && looksLikeCheck($0)) { blockCheck = 1; blockCheckLine = NR }
81
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
+ }
82
105
  }
83
106
  END { flush() }' 2>/dev/null)
84
107
  [ -z "$RES" ] || BAD="$BAD$RES
@@ -12,3 +12,19 @@ jobs:
12
12
  - name: выгрузить отчёт
13
13
  run: bash scripts/upload-report.sh
14
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,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 "линтер был красным"
@@ -0,0 +1,56 @@
1
+ # Конвейер не отдаёт чужому коду свои права и секреты
2
+
3
+ **Намерение.** Конвейер выполняется с правами репозитория и с доступом к его секретам. Три
4
+ способа отдать их постороннему живут не в коде, а в двадцати строках yaml:
5
+
6
+ - **подвижная метка вместо SHA.** `uses: some/action@v1` — это указатель, который владелец
7
+ действия может перевести на другой код в любой момент, и он же перевёдется у всех, кто на
8
+ метку сослался;
9
+ - **`pull_request_target` вместе с выкачиванием ветки автора PR.** Триггер даёт секреты
10
+ основного репозитория, а код берётся у постороннего;
11
+ - **`permissions: write-all`** на весь рабочий поток вместо нужного права нужному заданию.
12
+
13
+ Ни одну из трёх обычная проверка кода не увидит: это не код, это настройка.
14
+
15
+ **Какой отказ это поймало.** Собственный. На момент заведения записи у комплекта было **13
16
+ находок высокой строгости и 5 средней**, среди них десять действий, закреплённых меткой вместо
17
+ SHA, `id-token: write` на уровне всего потока и пять вызовов `checkout`, оставляющих учётные
18
+ данные в `.git/config`.
19
+
20
+ **Почему порог именно такой.** Замер 2026-09-08 по шести настоящим репозиториям:
21
+
22
+ | проект | High | Medium |
23
+ |---|---|---|
24
+ | `express` | 0 | 0 |
25
+ | `flask` | 0 | 0 |
26
+ | `uv` | 0 | 0 |
27
+ | `httpx` | 4 | 4 |
28
+ | `gin` | 18 | 0 |
29
+ | `ripgrep` | 19 | 8 |
30
+
31
+ Половина держит ноль. Порог взят с тех, кто его держит, а не выдуман: он достижим, и это
32
+ доказано чужой практикой, а не нашим мнением.
33
+
34
+ **Готовый аналог есть, и мы его зовём.** [`zizmor`](https://github.com/zizmorcore/zizmor) (MIT,
35
+ 6459 звёзд, статический разбор GitHub Actions). Его же гоняет `flask` отдельным заданием
36
+ конвейера. Своего разбора yaml мы не писали. Обёртка отвечает за порог (`--min-severity medium`)
37
+ и за то, чтобы «не проверено» не выдавалось за «чисто»: `--no-exit-codes` разводит «нашлись
38
+ находки» и «инструмент не отработал», а отсутствие итоговой строки в выводе даёт код 2.
39
+
40
+ **Образцы.** `red/` — рабочий поток с `pull_request_target`, выкачиванием ветки автора PR,
41
+ `permissions: write-all` и незакреплённым действием. `green/` — тот же поток на `pull_request`,
42
+ с правами только на чтение, действием по SHA и `persist-credentials: false`.
43
+
44
+ **Чего НЕ ловит.**
45
+
46
+ - **Только GitHub Actions.** GitLab CI, Jenkins, Buildkite не проверяются: zizmor их не читает.
47
+ - **Не выполняет конвейер.** Разбор статический: он видит опасный уклад, но не видит, что делает
48
+ скрипт внутри `run:`. Скачивание и выполнение чужого кода строкой `curl … | sh` внутри шага —
49
+ предмет другой проверки, и её у нас нет.
50
+ - **Не сторожит SHA после закрепления.** Закреплённое действие может оказаться заброшенным или
51
+ уязвимым; «закреплено» и «безопасно» — разные утверждения. Обновление закреплённых SHA — работа
52
+ для dependabot, а не для этой записи.
53
+ - **Точечное гашение с причиной остаётся возможным.** `# zizmor: ignore[правило] причина` снимает
54
+ находку, и это осознанный уклад: наш собственный `gate-not-weakened` требует, чтобы подавление
55
+ было точечным и с названной причиной, а не порогом на весь файл. Мы сами пользуемся этим дважды
56
+ и обе причины написали вслух.