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.
- package/README.md +61 -2
- package/README.ru.md +61 -2
- package/kit/docs/ai/agent-harness-playbook.md +1 -1
- package/kit/docs/ready-made-rules.md +29 -4
- package/kit/gates/README.md +40 -0
- package/kit/gates/_skip.sh +61 -1
- package/kit/gates/ci-actually-fails/README.md +12 -0
- package/kit/gates/ci-actually-fails/check.sh +26 -3
- package/kit/gates/ci-actually-fails/green/.github/workflows/ci.yml +16 -0
- package/kit/gates/ci-actually-fails/red/.github/workflows/soft.yml +15 -0
- package/kit/gates/ci-not-hijackable/README.md +56 -0
- package/kit/gates/ci-not-hijackable/check.sh +73 -0
- package/kit/gates/ci-not-hijackable/gate.yml +19 -0
- package/kit/gates/ci-not-hijackable/green/.github/workflows/triage.yml +19 -0
- package/kit/gates/ci-not-hijackable/red/.github/workflows/triage.yml +18 -0
- package/kit/gates/color-from-token/check.sh +19 -3
- package/kit/gates/color-from-token/green/Button.tsx +2 -0
- package/kit/gates/commit-explains-itself/README.md +13 -3
- package/kit/gates/commit-explains-itself/check.sh +8 -4
- package/kit/gates/complexity-limit/README.md +5 -0
- package/kit/gates/complexity-limit/check.sh +27 -9
- package/kit/gates/complexity-limit/green/test_fixtures.py +14 -0
- package/kit/gates/deps-are-pinned/README.md +14 -1
- package/kit/gates/deps-are-pinned/check.sh +6 -1
- package/kit/gates/deps-are-pinned/green/pyproject-with-requirements/pyproject.toml +12 -0
- package/kit/gates/deps-are-pinned/green/pyproject-with-requirements/requirements.txt +3 -0
- package/kit/gates/deps-are-pinned/red/pyproject-loose/pyproject.toml +12 -0
- package/kit/gates/deps-are-pinned/red/pyproject-loose/requirements.txt +3 -0
- package/kit/gates/duplicate-code/README.md +11 -2
- package/kit/gates/duplicate-code/check.sh +36 -5
- package/kit/gates/duplicate-code/gate.yml +8 -0
- package/kit/gates/duplicate-code/green/imports_a.go +20 -0
- package/kit/gates/duplicate-code/green/imports_b.go +19 -0
- package/kit/gates/entry-links-exist/README.md +5 -0
- package/kit/gates/entry-links-exist/check.sh +10 -1
- package/kit/gates/entry-links-exist/green/AGENTS.md +5 -0
- package/kit/gates/file-size-limit/README.md +9 -2
- package/kit/gates/file-size-limit/check.sh +14 -2
- package/kit/gates/gate-not-weakened/check.sh +13 -1
- package/kit/gates/hook-actually-fires/README.md +74 -0
- package/kit/gates/hook-actually-fires/check.sh +183 -0
- package/kit/gates/hook-actually-fires/gate.yml +15 -0
- package/kit/gates/hook-actually-fires/green/.claude/hooks/hooks.json +3 -0
- package/kit/gates/hook-actually-fires/green/.claude/settings.json +74 -0
- package/kit/gates/hook-actually-fires/green/.claude/settings.local.json +74 -0
- package/kit/gates/hook-actually-fires/red/.claude/hooks/hooks.json +4 -0
- package/kit/gates/hook-actually-fires/red/.claude/settings.json +53 -0
- package/kit/gates/no-phantom-package/README.md +84 -0
- package/kit/gates/no-phantom-package/check.sh +161 -0
- package/kit/gates/no-phantom-package/gate.yml +20 -0
- package/kit/gates/no-phantom-package/green/AGENTS.md +15 -0
- package/kit/gates/no-phantom-package/red/AGENTS.md +15 -0
- package/kit/gates/no-print-in-prod/README.md +33 -39
- package/kit/gates/no-print-in-prod/gate.yml +14 -6
- package/kit/gates/personal-config-not-shared/README.md +66 -0
- package/kit/gates/personal-config-not-shared/check.sh +103 -0
- package/kit/gates/personal-config-not-shared/gate.yml +16 -0
- package/kit/gates/personal-config-not-shared/green/.aqk-tracked +9 -0
- package/kit/gates/personal-config-not-shared/red/.aqk-tracked +6 -0
- package/kit/gates/secrets-not-in-code/check.sh +29 -4
- package/kit/gates/secrets-not-in-code/green/testdata/certificate/key.pem +3 -0
- package/kit/gates/swallowed-error/README.md +36 -18
- package/kit/gates/swallowed-error/gate.yml +13 -3
- package/kit/gates/test-has-assertion/check.sh +13 -1
- package/kit/gates/test-not-adjusted/README.md +79 -0
- package/kit/gates/test-not-adjusted/check.sh +136 -0
- package/kit/gates/test-not-adjusted/gate.yml +19 -0
- package/kit/gates/test-not-adjusted/green/after/calc.py +6 -0
- package/kit/gates/test-not-adjusted/green/after/tests/test_calc.py +9 -0
- package/kit/gates/test-not-adjusted/green/before/calc.py +2 -0
- package/kit/gates/test-not-adjusted/green/before/tests/test_calc.py +5 -0
- package/kit/gates/test-not-adjusted/red/after/calc.py +2 -0
- package/kit/gates/test-not-adjusted/red/after/tests/test_calc.py +5 -0
- package/kit/gates/test-not-adjusted/red/before/calc.py +2 -0
- package/kit/gates/test-not-adjusted/red/before/tests/test_calc.py +7 -0
- package/kit/gates/todo-without-task/README.md +6 -0
- package/kit/gates/todo-without-task/check.sh +14 -2
- package/kit/gates/todo-without-task/green/app.py +1 -0
- package/kit/ratchet/ratchet.sh +70 -2
- package/kit/rules/general.md +9 -0
- package/kit/rules-en/general.md +82 -0
- package/kit/rules-en/security.md +33 -0
- package/kit/rules-en/testing.md +48 -0
- package/llms.txt +22 -1
- package/package.json +6 -2
- package/tool/commands/badge.mjs +7 -1
- package/tool/commands/context.mjs +260 -0
- package/tool/commands/doctor.mjs +72 -25
- package/tool/commands/gates.mjs +10 -4
- package/tool/commands/learn.mjs +159 -0
- package/tool/commands/project.mjs +9 -1
- package/tool/commands/prove.mjs +67 -0
- package/tool/commands/report.mjs +37 -2
- package/tool/i18n/en-docs.mjs +154 -0
- package/tool/i18n/en.mjs +70 -90
- package/tool/i18n/ru-docs.mjs +156 -0
- package/tool/i18n/ru.mjs +69 -90
- package/tool/i18n/templates-en.mjs +1 -1
- package/tool/i18n/templates-ru.mjs +1 -1
- package/tool/lib/core.mjs +37 -2
- package/tool/lib/evidence.mjs +124 -0
- package/tool/lib/manifest.mjs +63 -5
- package/tool/lib/prove.mjs +172 -0
- package/tool/lib/repo.mjs +31 -2
- package/tool/lib/scope.mjs +46 -2
- package/tool/lib/templates.mjs +3 -0
- package/tool/program.mjs +23 -22
- package/tool/selfcheck/gates.sh +66 -0
- package/tool/selfcheck/mutation.sh +21 -1
- package/tool/selfcheck/smoke.sh +519 -39
- package/tool/selfcheck/units-context.mjs +186 -0
- package/tool/selfcheck/units-evidence.mjs +83 -0
- package/tool/selfcheck/units-learn.mjs +88 -0
- package/tool/selfcheck/units-level.mjs +122 -0
- package/tool/selfcheck/units.mjs +114 -2
- package/kit/gates/no-print-in-prod/check.sh +0 -38
- package/kit/gates/no-print-in-prod/green/docs.ts +0 -15
- package/kit/gates/no-print-in-prod/green/main.go +0 -8
- package/kit/gates/no-print-in-prod/green/main.rs +0 -4
- package/kit/gates/no-print-in-prod/red/main.go +0 -8
- package/kit/gates/no-print-in-prod/red/main.rs +0 -4
- package/kit/gates/swallowed-error/check.sh +0 -54
- package/kit/gates/swallowed-error/green/run.js +0 -8
- package/kit/gates/swallowed-error/red/run.js +0 -3
- /package/kit/gates/commit-explains-itself/green/{COMMIT_MSG → .aqk-commit-msg} +0 -0
- /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.
|
|
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
|
[](https://github.com/marketplace/actions/agent-quality-kit-aqk)
|
|
218
261
|
|
|
219
262
|
```yaml
|
|
220
|
-
- uses: arsen-ask-lx/Agent_Quality_Kit@v0.
|
|
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.
|
|
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
|
[](https://github.com/marketplace/actions/agent-quality-kit-aqk)
|
|
218
261
|
|
|
219
262
|
```yaml
|
|
220
|
-
- uses: arsen-ask-lx/Agent_Quality_Kit@v0.
|
|
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
|
-
| Инструмент | Что проверяет | Состояние
|
|
177
|
+
| Инструмент | Что проверяет | Состояние — дата замера в самой ячейке |
|
|
178
178
|
|---|---|---|
|
|
179
|
-
| [`agnix`](https://github.com/agent-sh/agnix) | 455 правил: структура `CLAUDE.md`/`AGENTS.md`/`SKILL.md`, синтаксис конфигов MCP и хуков, соглашения об именах, **мёртвые ссылки на файлы**. Есть автопочинка и LSP | 404 ⭐, Rust,
|
|
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` пишется, **что именно проверено**: какой инструмент
|
package/kit/gates/README.md
CHANGED
|
@@ -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` — «показывать тем, у кого этого нет». Условие, которого
|
package/kit/gates/_skip.sh
CHANGED
|
@@ -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
|
|
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
|
+
и обе причины написали вслух.
|