agent-quality-kit 0.7.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 +45 -2
- package/README.ru.md +45 -2
- package/kit/gates/_skip.sh +61 -1
- 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 +6 -2
- package/kit/gates/color-from-token/green/Button.tsx +2 -0
- package/kit/gates/complexity-limit/check.sh +6 -7
- package/kit/gates/duplicate-code/check.sh +5 -1
- package/kit/gates/entry-links-exist/check.sh +4 -1
- package/kit/gates/entry-links-exist/green/AGENTS.md +2 -0
- package/kit/gates/file-size-limit/check.sh +1 -1
- package/kit/gates/secrets-not-in-code/check.sh +16 -3
- package/kit/gates/secrets-not-in-code/green/testdata/certificate/key.pem +3 -0
- package/kit/gates/todo-without-task/check.sh +1 -1
- package/kit/gates/todo-without-task/green/app.py +1 -0
- package/llms.txt +22 -1
- package/package.json +4 -1
- package/tool/commands/context.mjs +260 -0
- package/tool/commands/doctor.mjs +25 -18
- package/tool/commands/learn.mjs +159 -0
- package/tool/commands/project.mjs +1 -0
- package/tool/commands/report.mjs +33 -1
- package/tool/i18n/en-docs.mjs +85 -1
- package/tool/i18n/en.mjs +21 -36
- package/tool/i18n/ru-docs.mjs +87 -1
- package/tool/i18n/ru.mjs +21 -36
- package/tool/lib/core.mjs +30 -1
- package/tool/lib/evidence.mjs +124 -0
- package/tool/lib/manifest.mjs +29 -2
- package/tool/lib/prove.mjs +13 -1
- package/tool/lib/scope.mjs +10 -1
- package/tool/lib/templates.mjs +1 -0
- package/tool/program.mjs +19 -23
- package/tool/selfcheck/smoke.sh +242 -2
- 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 +65 -3
- package/tool/selfcheck/units.mjs +1 -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
|
```
|
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
|
```
|
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:
|
|
@@ -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
|
+
и обе причины написали вслух.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
# Конфигурация конвейера не отдаёт чужому коду права и секреты.
|
|
3
|
+
#
|
|
4
|
+
# ЗАЧЕМ. Конвейер выполняется с правами репозитория и с доступом к его секретам. Три способа
|
|
5
|
+
# отдать их постороннему живут не в коде, а в двадцати строках yaml: триггер `pull_request_target`
|
|
6
|
+
# вместе с выкачиванием ветки автора PR; действие, закреплённое подвижной МЕТКОЙ, которую владелец
|
|
7
|
+
# действия может перевести на другой код; права `write-all`, выданные всему рабочему потоку.
|
|
8
|
+
# Ни одну из трёх обычная проверка кода не увидит: это не код, это настройка.
|
|
9
|
+
#
|
|
10
|
+
# Работу делает zizmor (MIT, 6459 звёзд, статический разбор GitHub Actions). Мы отвечаем за
|
|
11
|
+
# порог и за то, чтобы «не проверено» не выдавалось за «чисто».
|
|
12
|
+
#
|
|
13
|
+
# ПОЧЕМУ ПОРОГ ИМЕННО ТАКОЙ. Замер 2026-09-08 по шести настоящим репозиториям: express, flask и
|
|
14
|
+
# uv держат НОЛЬ находок высокой и средней строгости; gin (18), ripgrep (19) и httpx (4) — нет.
|
|
15
|
+
# Порог взят с тех, кто его держит, а не выдуман: он достижим, и это доказано чужой практикой.
|
|
16
|
+
# Мы сами на момент заведения записи были на неправильной стороне — 13 высоких и 5 средних.
|
|
17
|
+
DIR="${1:-.}"
|
|
18
|
+
WF="$DIR/.github/workflows"
|
|
19
|
+
|
|
20
|
+
# Нет конвейера — нечего проверять. Это не успех и не провал, это отсутствие предмета.
|
|
21
|
+
[ -d "$WF" ] || exit 0
|
|
22
|
+
|
|
23
|
+
if ! command -v zizmor >/dev/null 2>&1; then
|
|
24
|
+
echo "не найден zizmor — эта проверка делегирована ему"
|
|
25
|
+
echo " почини: pipx install zizmor (или uv tool install zizmor)"
|
|
26
|
+
exit 2
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
# --no-exit-codes разводит два разных события, которые иначе слиплись бы в один ненулевой код:
|
|
30
|
+
# «нашлись находки» и «инструмент не отработал». Первое читается из вывода, второе — из кода.
|
|
31
|
+
# NO_COLOR и снятие управляющих последовательностей — вместе, а не по отдельности. Внутри
|
|
32
|
+
# GitHub Actions zizmor КРАСИТ вывод (там цвет поддержан), и итоговая строка начинается с
|
|
33
|
+
# escape-последовательности: правило «^[0-9]+ findings» её не видит, и гейт объявлял, что формат
|
|
34
|
+
# сменился. Локально этого не воспроизвести — вне конвейера цвет выключается сам. Поймано
|
|
35
|
+
# прогоном в конвейере 2026-09-08; тот же класс уже записан у нас в scope.mjs: «цвет снимается ДО
|
|
36
|
+
# поиска». NO_COLOR — соглашение, его может не знать следующая версия; sed — страховка, которая
|
|
37
|
+
# в отличие от NO_COLOR ни от кого не зависит.
|
|
38
|
+
ESC=$(printf '\033')
|
|
39
|
+
OUT=$(NO_COLOR=1 zizmor --no-online-audits --no-exit-codes --min-severity medium --format plain "$WF" 2>&1 \
|
|
40
|
+
| sed "s/${ESC}\[[0-9;]*[a-zA-Z]//g")
|
|
41
|
+
CODE=$?
|
|
42
|
+
if [ "$CODE" -ne 0 ]; then
|
|
43
|
+
echo "zizmor не отработал (код $CODE) — проверка не состоялась, это не вердикт «чисто»"
|
|
44
|
+
printf '%s\n' "$OUT" | grep -iE "error:|panic|not found" | head -3 | sed 's/^/ /'
|
|
45
|
+
echo " почини: прогони «zizmor .github/workflows» руками и посмотри, на чём он споткнулся"
|
|
46
|
+
exit 2
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# Итоговая строка — единственное место, где сказано, сколько чего нашлось. Её отсутствие значит,
|
|
50
|
+
# что формат сменился; молчать об этом нельзя, иначе смена формата станет вечным зелёным.
|
|
51
|
+
SUM=$(printf '%s\n' "$OUT" | grep -E "^(No findings to report|[0-9]+ findings)" | tail -1)
|
|
52
|
+
if [ -z "$SUM" ]; then
|
|
53
|
+
echo "ответ zizmor не разобран: итоговой строки в нём нет"
|
|
54
|
+
# Печатаем то, что пришло на самом деле. Без этого причина видна только тому, у кого есть та
|
|
55
|
+
# же машина: первый отказ этой ветки случился в конвейере, а локально не воспроизводился, и
|
|
56
|
+
# разбирать пришлось догадками. Сообщение, не показывающее свой ввод, лечится вторым прогоном.
|
|
57
|
+
echo " вот последнее, что он напечатал:"
|
|
58
|
+
printf '%s\n' "$OUT" | tail -3 | sed 's/^/ /'
|
|
59
|
+
echo " почини: сверь версию zizmor с той, что названа в gate.yml"
|
|
60
|
+
exit 2
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
case "$SUM" in
|
|
64
|
+
"No findings to report"*) exit 0 ;;
|
|
65
|
+
esac
|
|
66
|
+
|
|
67
|
+
printf '%s\n' "$OUT" | grep -E "^(error|warning)\[" | head -20
|
|
68
|
+
N=$(printf '%s\n' "$OUT" | grep -cE "^(error|warning)\[")
|
|
69
|
+
[ "$N" -gt 20 ] && echo " … и ещё $((N - 20))"
|
|
70
|
+
echo " почини: закрепи действия по SHA вместо метки, сузь permissions до нужного задания,"
|
|
71
|
+
echo " замени pull_request_target на pull_request там, где выполняется код автора PR."
|
|
72
|
+
echo " подробности по каждой находке: https://docs.zizmor.sh/audits/"
|
|
73
|
+
exit 1
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
intent: конвейер не отдаёт чужому коду свои права и секреты
|
|
2
|
+
intent_en: the pipeline does not hand its permissions and secrets to somebody else's code
|
|
3
|
+
|
|
4
|
+
# Там, где конвейер есть. Без него отдавать нечего.
|
|
5
|
+
trigger:
|
|
6
|
+
has_ci: true
|
|
7
|
+
|
|
8
|
+
recipes:
|
|
9
|
+
any: bash {gate}/check.sh {dir}
|
|
10
|
+
|
|
11
|
+
# Программа, без которой запись не работает. По первому слову команды этого не видно: обёртка
|
|
12
|
+
# начинается с `bash`. Версия названа: обёртка читает итоговую строку вывода zizmor, и смена
|
|
13
|
+
# формата обязана быть видимой, а не тихой.
|
|
14
|
+
requires: zizmor
|
|
15
|
+
|
|
16
|
+
proof: incidents/README.md, 2026-09-08 «конвейер отдавал права по подвижной метке» — замер по
|
|
17
|
+
шести настоящим репозиториям показал, что express, flask и uv держат ноль находок высокой и
|
|
18
|
+
средней строгости, а gin (18), ripgrep (19) и httpx (4) нет; сам комплект на момент заведения
|
|
19
|
+
записи имел 13 высоких и 5 средних, включая десять действий, закреплённых меткой вместо SHA
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
name: разбор входящих
|
|
2
|
+
|
|
3
|
+
# pull_request вместо pull_request_target: код автора PR выполняется без доступа к секретам
|
|
4
|
+
# основного репозитория.
|
|
5
|
+
on:
|
|
6
|
+
pull_request:
|
|
7
|
+
types: [opened]
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
triage:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
|
17
|
+
with:
|
|
18
|
+
persist-credentials: false
|
|
19
|
+
- run: echo "разбор без прав на запись"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
name: разбор входящих
|
|
2
|
+
|
|
3
|
+
# pull_request_target выполняется в контексте ОСНОВНОЙ ветки и с доступом к секретам,
|
|
4
|
+
# а код берётся из ветки автора PR. Классическая дыра.
|
|
5
|
+
on:
|
|
6
|
+
pull_request_target:
|
|
7
|
+
types: [opened]
|
|
8
|
+
|
|
9
|
+
permissions: write-all
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
triage:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
with:
|
|
17
|
+
ref: ${{ github.event.pull_request.head.sha }}
|
|
18
|
+
- run: npm install && npm run build
|
|
@@ -40,7 +40,7 @@ EXT_RE="$(printf '%s' "$COLOR_EXT" | tr ' ' '|')"
|
|
|
40
40
|
HITS=$(find "$DIR" $(skip_find "$DIR") -type f -print 2>/dev/null \
|
|
41
41
|
| grep -E "\.($EXT_RE)$" \
|
|
42
42
|
| grep -viE "(^|/)[^/]*($SOURCE_RE)[^/]*\.($EXT_RE)$" \
|
|
43
|
-
|
|
|
43
|
+
| drop_generated \
|
|
44
44
|
| LC_ALL=C sort \
|
|
45
45
|
| xargs -r awk '
|
|
46
46
|
# Блочные комментарии вырезаются ПО СОСТОЯНИЮ, а не построчно. Однострочный фильтр
|
|
@@ -63,7 +63,11 @@ HITS=$(find "$DIR" $(skip_find "$DIR") -type f -print 2>/dev/null \
|
|
|
63
63
|
if (e == 0) { line = substr(line, 1, s - 1); inblock = 1; break }
|
|
64
64
|
line = substr(line, 1, s - 1) substr(rest, e + 2)
|
|
65
65
|
}
|
|
66
|
-
|
|
66
|
+
# Хвост — «не буква, не цифра, не дефис», а не просто «не шестнадцатеричный символ».
|
|
67
|
+
# Якорь ссылки «#defining-entry-points» начинается с «#def», за которым идёт «i»: по
|
|
68
|
+
# прежнему правилу это был цвет. Замер 2026-09-08 по шести чужим репозиториям: в uv
|
|
69
|
+
# ложным оказалось именно это, в docs/js/extra.js.
|
|
70
|
+
if (line ~ /#[0-9a-fA-F]{8}([^0-9a-zA-Z_-]|$)|#[0-9a-fA-F]{6}([^0-9a-zA-Z_-]|$)|#[0-9a-fA-F]{3}([^0-9a-zA-Z_-]|$)/)
|
|
67
71
|
print FILENAME ":" FNR ":" $0
|
|
68
72
|
}
|
|
69
73
|
' 2>/dev/null \
|
|
@@ -2,3 +2,5 @@ export function Button({ children }: { children: React.ReactNode }) {
|
|
|
2
2
|
// Цвет приходит из темы: перекрашивается вместе с ней.
|
|
3
3
|
return <button className="bg-[var(--color-accent)] text-[var(--color-on-accent)]">{children}</button>;
|
|
4
4
|
}
|
|
5
|
+
|
|
6
|
+
const DOC_ANCHOR = "concepts/projects/#defining-entry-points";
|
|
@@ -30,7 +30,7 @@ TESTS="-name test -prune -o -name tests -prune -o -name spec -prune -o -name __t
|
|
|
30
30
|
find "$DIR" $(skip_find "$DIR") $TESTS -type f \
|
|
31
31
|
! -name 'test_*' ! -name '*_test.*' ! -name '*.test.*' ! -name '*.spec.*' \
|
|
32
32
|
-print 2>/dev/null | only_code | own_samples_filter "$DIR" \
|
|
33
|
-
|
|
|
33
|
+
| drop_generated \
|
|
34
34
|
| xargs -r awk -v MAX="$MAX" '
|
|
35
35
|
# Один обход на все файлы: процесс на каждый файл дал 19 секунд на 4000 файлов.
|
|
36
36
|
# Разметка вложена по природе: пять уровней тегов — это не сложная логика, а обычная
|
|
@@ -40,12 +40,11 @@ find "$DIR" $(skip_find "$DIR") $TESTS -type f \
|
|
|
40
40
|
FNR == 1 { flush(); worst = 0; wl = 0; wf = FILENAME }
|
|
41
41
|
/^[[:space:]]*$/ { next }
|
|
42
42
|
{
|
|
43
|
-
# ширина отступа: табуляция считается за четыре
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
43
|
+
# ширина отступа: табуляция считается за четыре пробела. Отступ берётся ОДНИМ
|
|
44
|
+
# match, а не посимвольным циклом с substr: цикл выделял новую строку на каждый
|
|
45
|
+
# символ отступа и стоил больше, чем весь остальной разбор. Замер 2026-09-08 на uv.
|
|
46
|
+
match($0, /^[ \t]*/); ind = substr($0, 1, RLENGTH)
|
|
47
|
+
n = gsub(/\t/, "", ind) * 4 + length(ind)
|
|
49
48
|
depth = int(n / 4)
|
|
50
49
|
if (depth > worst) { worst = depth; wl = FNR }
|
|
51
50
|
}
|
|
@@ -30,7 +30,7 @@ TESTS="-name test -prune -o -name tests -prune -o -name spec -prune -o -name __t
|
|
|
30
30
|
find "$DIR" $(skip_find "$DIR") $TESTS -type f \
|
|
31
31
|
! -name 'test_*' ! -name '*_test.*' ! -name '*.test.*' ! -name '*.spec.*' \
|
|
32
32
|
-print 2>/dev/null | only_code | own_samples_filter "$DIR" \
|
|
33
|
-
|
|
|
33
|
+
| drop_generated \
|
|
34
34
|
| LC_ALL=C sort \
|
|
35
35
|
| xargs -r env LC_ALL=C awk -v WIN="$WIN" '
|
|
36
36
|
# Строка-объявление ввоза: `import`, `from … import`, `use …;`, путь в кавычках внутри
|
|
@@ -54,6 +54,10 @@ find "$DIR" $(skip_find "$DIR") $TESTS -type f \
|
|
|
54
54
|
buf[++n] = line
|
|
55
55
|
imp[n] = isimport(line)
|
|
56
56
|
if (n >= WIN) {
|
|
57
|
+
# Ключ и признак «есть ли код» собираются ОДНИМ проходом по окну. Разнести их на
|
|
58
|
+
# два цикла выглядело ускорением — окно из одного ввоза отбрасывалось бы до сборки
|
|
59
|
+
# ключа. Замерено 2026-09-08 на uv: стало 2791 мс против 1770. Второй обход окна
|
|
60
|
+
# дороже, чем сборка ключа, которую он экономит; правка откачена по замеру.
|
|
57
61
|
key = ""; code = 0
|
|
58
62
|
for (i = n - WIN + 1; i <= n; i++) { key = key buf[i] "\x1e"; if (!imp[i]) code = 1 }
|
|
59
63
|
if (!code) next # окно целиком из ввоза — не дубль
|
|
@@ -7,7 +7,10 @@ MISS=0
|
|
|
7
7
|
for MD in "$DIR"/*.md; do
|
|
8
8
|
[ -f "$MD" ] || continue
|
|
9
9
|
# вытащить цели ссылок вида [текст](путь)
|
|
10
|
-
|
|
10
|
+
# Строки со вставками в обратных кавычках чистятся ДО разбора: «`[name](url)`» — это пример
|
|
11
|
+
# оформления ссылки, а не ссылка. Замер 2026-09-08: в uv/STYLE.md именно так и было, и гейт
|
|
12
|
+
# требовал создать файл с именем «url».
|
|
13
|
+
TARGETS=$(sed 's/`[^`]*`//g' "$MD" | sed -n 's/.*](\([^)]*\)).*/\1/p')
|
|
11
14
|
for T in $TARGETS; do
|
|
12
15
|
# автоссылки бывают обёрнуты как <(https://...)> — искать http где угодно внутри,
|
|
13
16
|
# не только в начале строки.
|
|
@@ -6,3 +6,5 @@
|
|
|
6
6
|
Ссылка на страницу сайта документации: [руководство](tutorial/#install) — путь опубликованного
|
|
7
7
|
сайта, а не файл на диске. Найдено замером по fastapi: `[installation guide](tutorial/#install-fastapi)`
|
|
8
8
|
в README читалась как битая, хотя на сайте работает. Так ссылаются mkdocs, docusaurus и jekyll.
|
|
9
|
+
|
|
10
|
+
Пример оформления ссылки: `[name](url)` — это пример, а не ссылка.
|
|
@@ -23,7 +23,7 @@ fi
|
|
|
23
23
|
# не укладывался в две минуты.
|
|
24
24
|
# shellcheck disable=SC2046
|
|
25
25
|
find "$DIR" $(skip_find "$DIR") -type f -print 2>/dev/null | only_code | own_samples_filter "$DIR" \
|
|
26
|
-
|
|
|
26
|
+
| drop_generated \
|
|
27
27
|
| xargs -r wc -l 2>/dev/null \
|
|
28
28
|
| awk '
|
|
29
29
|
$2 == "total" { next }
|
|
@@ -18,9 +18,22 @@ fi
|
|
|
18
18
|
# Красный образец — намеренно сломанный код в репозитории. Сканирующий гейт обязан его
|
|
19
19
|
# пропускать, иначе будет вечно краснеть на том, что сам же и положил. Исключение снимается,
|
|
20
20
|
# когда проверяют сам образец: тогда каталог red и есть цель проверки.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
# Два прохода, а не один, и разница между ними измерена. Живой облачный ключ — находка везде,
|
|
22
|
+
# включая тестовые каталоги: он открывает настоящий счёт. Приватный КЛЮЧ в тестовых данных —
|
|
23
|
+
# норма: любой проект с проверками TLS кладёт туда сертификат, и Go так делает по соглашению.
|
|
24
|
+
# Замер 2026-09-08 по шести чужим репозиториям: gin краснел на testdata/certificate/key.pem,
|
|
25
|
+
# положенном туда намеренно. Гейт, краснеющий на нормальном укладе, выключают в первый день —
|
|
26
|
+
# и тогда он не ловит уже НИЧЕГО, включая настоящий ключ.
|
|
27
|
+
TOKENS='(sk|pk)_(live|test)_[A-Za-z0-9]{16,}|AKIA[0-9A-Z]{16}|ghp_[A-Za-z0-9]{30,}|xox[baprs]-[A-Za-z0-9-]{10,}'
|
|
28
|
+
PRIVKEY='-----BEGIN [A-Z ]*PRIVATE KEY-----'
|
|
29
|
+
# Каталоги образцов: только те, чьё имя не оставляет сомнений. `data` или `assets` сюда не
|
|
30
|
+
# входят — там ключ вполне может оказаться настоящим.
|
|
31
|
+
FIXTURES='(^|/)(testdata|fixtures|__fixtures__|test|tests|spec|__tests__)/'
|
|
32
|
+
|
|
33
|
+
HITS=$(
|
|
34
|
+
{ grep -rInE $(skip_grep "$DIR") -e "$TOKENS" "$DIR" 2>/dev/null
|
|
35
|
+
grep -rInE $(skip_grep "$DIR") -e "$PRIVKEY" "$DIR" 2>/dev/null | grep -vE "$FIXTURES"
|
|
36
|
+
} | grep -v '/\.git/' | own_samples_filter "$DIR" | sort -u)
|
|
24
37
|
if [ -n "$HITS" ]; then
|
|
25
38
|
echo "$HITS" | cut -c1-160
|
|
26
39
|
echo " почини: убери значение из файла, положи его в переменную окружения и отзови старый ключ."
|
|
@@ -21,7 +21,7 @@ fi
|
|
|
21
21
|
# названием и на собственном шаблоне поиска — на том, что дефектом не является.
|
|
22
22
|
# Сами эти слова здесь не пишем: гейт нашёл бы себя. Проверено — находил.
|
|
23
23
|
HITS=$(grep -rnE $(skip_grep "$DIR") $(include_code) \
|
|
24
|
-
'(#|//|/\*|--|<!--)[^"'"'"']*(^|[^A-Za-
|
|
24
|
+
'(#|//|/\*|--|<!--)[^"'"'"']*(^|[^A-Za-z0-9_])(TODO|FIXME|HACK|XXX)([^A-Za-z0-9_]|$)' "$DIR" 2>/dev/null | own_samples_filter "$DIR")
|
|
25
25
|
if [ -n "$HITS" ]; then
|
|
26
26
|
echo "$HITS"
|
|
27
27
|
echo " почини: заведи задачу в очереди работ, маркер убери."
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
UV_TEST_XXX = "имя переменной, а не маркер долга"
|
package/llms.txt
CHANGED
|
@@ -23,11 +23,32 @@ Zero runtime dependencies. Node 18+ and an `sh` shell. MIT.
|
|
|
23
23
|
the other 36 are named as a number, not hidden)
|
|
24
24
|
- Fail a pipeline below a level or on a failed gate: `npx agent-quality-kit doctor --run --min 1`
|
|
25
25
|
- Show only what a diff introduced, so a legacy repo is usable from day one: `doctor --run --since main`
|
|
26
|
+
- Prove the gates actually catch defects: `npx agent-quality-kit prove` — every *provable* gate is
|
|
27
|
+
run against its own red and green sample and must go red on the first and stay quiet on the
|
|
28
|
+
second. A gate with no samples, with samples written for another recipe, or whose command takes
|
|
29
|
+
no directory is reported as unprovable and named; the verdict is "nothing proven is broken, and
|
|
30
|
+
at least one gate is proven". Level AQK-2 and the badge depend on this, not on the presence of
|
|
31
|
+
files
|
|
32
|
+
- See what proves a diff, file by file: `npx agent-quality-kit report --since main` — each changed
|
|
33
|
+
code file is named by a check, walked past in silence, or touched by nothing at all, plus a
|
|
34
|
+
fingerprint over the base, the commands and the file contents
|
|
35
|
+
- Put the repository state into the agent's context instead of hoping it reads the files:
|
|
36
|
+
`npx agent-quality-kit context` — level, what is red right now, how many rules no machine
|
|
37
|
+
enforces, what the ratchets hold. Where it does not know, it says so: a run that never happened
|
|
38
|
+
is reported as unknown, never as clean. `context --install` writes a `SessionStart` hook into
|
|
39
|
+
`.claude/settings.json` (Claude Code only; the rest of the kit stays vendor-neutral). Measured:
|
|
40
|
+
the block is ~375 tokens, and carries what a file cannot — what changed today. `context --full`
|
|
41
|
+
adds the command map and the rulebook verbatim (~7000 tokens): a deliberate trade, chosen by
|
|
42
|
+
the owner after the objection about long inputs, on the grounds that an agent reads files
|
|
43
|
+
poorly and the tokens are the price of it not guessing
|
|
44
|
+
- See what you told the agent and never wrote down: `npx agent-quality-kit learn` — reads Claude Code
|
|
45
|
+
transcripts for this project on this machine and prints rule candidates missing from the entry
|
|
46
|
+
point. Current project only, terminal only, writes nothing, always exits 0
|
|
26
47
|
- Exit codes: 0 pass, 1 below the level or a gate failed
|
|
27
48
|
- As a pre-commit hook: `repo: https://github.com/arsen-ask-lx/Agent_Quality_Kit` with
|
|
28
49
|
`id: aqk` (blocking), `aqk-doctor` (read-only) or `aqk-baseline`. pre-commit installs the
|
|
29
50
|
package itself; there are no dependencies to pull in.
|
|
30
|
-
- As a GitHub Action: `uses: arsen-ask-lx/Agent_Quality_Kit@v0.
|
|
51
|
+
- As a GitHub Action: `uses: arsen-ask-lx/Agent_Quality_Kit@v0.8.0` with `min: 1`
|
|
31
52
|
(https://github.com/marketplace/actions/agent-quality-kit-aqk)
|
|
32
53
|
|
|
33
54
|
## What makes it different
|