agent-quality-kit 0.12.0 → 0.14.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 (74) hide show
  1. package/README.md +43 -7
  2. package/README.ru.md +44 -7
  3. package/kit/gates/api-contract-has-arbiter/README.md +16 -1
  4. package/kit/gates/api-contract-has-arbiter/check.sh +66 -23
  5. package/kit/gates/complexity-limit/gate.yml +4 -0
  6. package/kit/gates/dead-code/gate.yml +4 -0
  7. package/kit/gates/entry-commands-exist/README.md +64 -0
  8. package/kit/gates/entry-commands-exist/check.sh +110 -0
  9. package/kit/gates/entry-commands-exist/gate.yml +19 -0
  10. package/kit/gates/entry-commands-exist/green/AGENTS.md +13 -0
  11. package/kit/gates/entry-commands-exist/green/Makefile +6 -0
  12. package/kit/gates/entry-commands-exist/green/justfile +2 -0
  13. package/kit/gates/entry-commands-exist/green/package.json +10 -0
  14. package/kit/gates/entry-commands-exist/red/AGENTS.md +9 -0
  15. package/kit/gates/entry-commands-exist/red/Makefile +2 -0
  16. package/kit/gates/entry-commands-exist/red/package.json +9 -0
  17. package/kit/gates/env-secrets-not-committed/README.md +73 -0
  18. package/kit/gates/env-secrets-not-committed/check.sh +139 -0
  19. package/kit/gates/env-secrets-not-committed/gate.yml +21 -0
  20. package/kit/gates/env-secrets-not-committed/green/.aqk-tracked +10 -0
  21. package/kit/gates/env-secrets-not-committed/green/.env +10 -0
  22. package/kit/gates/env-secrets-not-committed/green/.env.production +5 -0
  23. package/kit/gates/env-secrets-not-committed/green/.env.test +2 -0
  24. package/kit/gates/env-secrets-not-committed/red/.aqk-tracked +5 -0
  25. package/kit/gates/env-secrets-not-committed/red/.env +7 -0
  26. package/kit/gates/no-print-in-prod/gate.yml +4 -0
  27. package/kit/gates/swallowed-error/gate.yml +4 -0
  28. package/kit/gates/todo-without-task/gate.yml +4 -0
  29. package/llms.txt +8 -2
  30. package/package.json +1 -1
  31. package/tool/commands/badge.mjs +1 -1
  32. package/tool/commands/context.mjs +81 -9
  33. package/tool/commands/doctor-catalog.mjs +222 -0
  34. package/tool/commands/doctor.mjs +81 -209
  35. package/tool/commands/learn.mjs +119 -19
  36. package/tool/commands/probe.mjs +50 -68
  37. package/tool/commands/project.mjs +6 -1
  38. package/tool/commands/prompt.mjs +69 -0
  39. package/tool/commands/report.mjs +1 -1
  40. package/tool/commands/vitals.mjs +15 -11
  41. package/tool/i18n/en-docs.mjs +22 -1
  42. package/tool/i18n/en-gates.mjs +6 -1
  43. package/tool/i18n/en.mjs +78 -4
  44. package/tool/i18n/index.mjs +42 -3
  45. package/tool/i18n/ru-docs.mjs +24 -1
  46. package/tool/i18n/ru-gates.mjs +6 -1
  47. package/tool/i18n/ru.mjs +87 -4
  48. package/tool/lib/adopt.mjs +15 -1
  49. package/tool/lib/advice.mjs +115 -0
  50. package/tool/lib/annotate.mjs +66 -0
  51. package/tool/lib/brief.mjs +3 -1
  52. package/tool/lib/cadence.mjs +40 -1
  53. package/tool/lib/core.mjs +40 -1
  54. package/tool/lib/gate-worker.mjs +18 -0
  55. package/tool/lib/history.mjs +34 -5
  56. package/tool/lib/manifest.mjs +65 -21
  57. package/tool/lib/repo.mjs +47 -35
  58. package/tool/lib/run.mjs +98 -9
  59. package/tool/program.mjs +4 -0
  60. package/tool/selfcheck/smoke/_fixture.mjs +8 -3
  61. package/tool/selfcheck/smoke/api-contract.test.mjs +37 -0
  62. package/tool/selfcheck/smoke/corpus.test.mjs +151 -0
  63. package/tool/selfcheck/smoke/first-run.test.mjs +144 -0
  64. package/tool/selfcheck/smoke/verdict.test.mjs +91 -3
  65. package/tool/selfcheck/smoke.sh +10 -2
  66. package/tool/selfcheck/units-annotate.mjs +67 -0
  67. package/tool/selfcheck/units-cadence.mjs +40 -1
  68. package/tool/selfcheck/units-context.mjs +64 -1
  69. package/tool/selfcheck/units-learn.mjs +32 -0
  70. package/tool/selfcheck/units-level.mjs +97 -3
  71. package/tool/selfcheck/units-probe.mjs +2 -1
  72. package/tool/selfcheck/units-prompt.mjs +106 -0
  73. package/tool/selfcheck/units-repo.mjs +44 -1
  74. package/tool/selfcheck/units-verdict.mjs +76 -0
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "sample",
3
+ "scripts": {
4
+ "build": "tsc"
5
+ },
6
+ "dependencies": {
7
+ "debug": "4.3.4"
8
+ }
9
+ }
@@ -0,0 +1,73 @@
1
+ # Пароли из .env не лежат в git
2
+
3
+ **Намерение.** Файл окружения с настоящими значениями, попав в git, раздаёт пароли и ключи
4
+ каждому, кто склонировал репозиторий, — и из истории их уже не вычистить: ключ считается
5
+ скомпрометированным, его остаётся только сменить.
6
+
7
+ **Почему не «любой `.env` в git — брак».** Так было бы проще, и так было бы неверно. Сверено по
8
+ первоисточникам 2026-09-11:
9
+
10
+ - dotenv: «Should I commit my `.env` file? No. Unless you encrypt it with dotenvx. Then we
11
+ recommend you do» — зашифрованный файл коммитят намеренно;
12
+ - Next.js: «`.env.test` file should be included in your repository, but `.env.test.local`
13
+ shouldn't, as `.env*.local` are intended to be ignored through `.gitignore`»;
14
+ - Vite: «`.env.*.local` files are local-only and can contain sensitive variables. You should add
15
+ `*.local` to your `.gitignore`» — про обычный `.env` не сказано ничего, и во многих проектах
16
+ его коммитят с публичными настройками.
17
+
18
+ Поэтому правил два:
19
+
20
+ 1. **`.env.local` и `.env.*.local` в git — находка всегда.** Здесь все сходятся: «local» — только
21
+ для этой машины, что бы в файле ни лежало.
22
+ 2. **Остальные `.env*` в git — находка, если внутри ключ-секрет с настоящим значением:** имя
23
+ содержит `PASSWORD`, `SECRET`, `TOKEN`, `API_KEY`, `PRIVATE_KEY`, `ACCESS_KEY`, `CREDENTIAL`,
24
+ либо значение — адрес с паролем (`схема://пользователь:пароль@хост`).
25
+
26
+ **Значение секрета проверка не печатает.** Вывод гейта попадает в лог конвейера; напечатать
27
+ пароль значило бы завести вторую утечку ради сообщения о первой. Называются файл, строка, ключ.
28
+
29
+ **Готовый аналог — есть частично, и мы его замерили.** Закоммиченный `.env` с тремя секретами и
30
+ `.env.local` с токеном (2026-09-11):
31
+
32
+ | | пароль, придуманный человеком | случайная строка | ключ с форматом поставщика | `.env.local` |
33
+ |---|---|---|---|---|
34
+ | наш `secrets-not-in-code` | не видит | не видит | видит | не видит |
35
+ | `gitleaks` 8.30.1 | не видит | видит (по энтропии) | видит | не видит |
36
+ | [`detect-secrets`](https://github.com/Yelp/detect-secrets) 1.5.0 | видит | видит | видит | не видит |
37
+ | эта запись | видит | видит | видит, если имя ключа говорящее | видит |
38
+
39
+ `detect-secrets` сильнее нас во втором правиле: он читает имена ключей во всех файлах, а не
40
+ только в `.env`. Если он у вас уже стоит в pre-commit с файлом-базой — второе правило закрыто
41
+ им, первое остаётся за этой записью. Родным рецептом он не стал: в конвейере ему нужна база
42
+ (`.secrets.baseline`), а запись ставится одной командой.
43
+
44
+ **Чего НЕ ловит.**
45
+
46
+ - **Секрет под неговорящим именем.** `STRIPE=…`, `DB=…`, `X_CONF=…` — ключ не называет себя
47
+ секретом. Такое ловит только сканер по формату значения (`secrets-not-in-code`, `gitleaks`).
48
+ - **Короткий пароль.** Значения короче восьми знаков пропускаются: `dev`, `x`, `1234` в `.env`
49
+ для локального стенда встречаются куда чаще настоящих паролей такой длины.
50
+ - **Пароль, совпавший со значением по умолчанию.** `postgres`, `root`, `admin`, `guest`,
51
+ `minioadmin` — стоят в каждом docker-compose для разработки, и считать их утечкой значит
52
+ краснеть на каждом втором проекте. Если такой пароль стоит на проде, это другая беда.
53
+ - **Публичные переменные сборки.** `NEXT_PUBLIC_`, `VITE_`, `REACT_APP_`, `EXPO_PUBLIC_`,
54
+ `NUXT_PUBLIC_`, `GATSBY_` уезжают в браузер по устройству — секретом они быть не могут.
55
+ - **Шаблоны и тестовые данные.** `.env.example`, `.env.sample`, `.env.template`, `.env.dist`,
56
+ `.env.test`, а также каталоги `templates/`, `examples/`, `fixtures/`, `testdata/`.
57
+ - **`.env.local` в каталоге тестов и демо.** `tests/`, `test/`, `__tests__/`, `spec/`,
58
+ `playgrounds/`, `demos/` — там это фикстура: библиотека показывает или проверяет, что умеет
59
+ читать такой файл. Замер: оба ложных срабатывания были ровно такими. Пароль внутри тестового
60
+ `.env` при этом ловится — утечка в тестах остаётся утечкой.
61
+ - **Историю.** Проверяется то, что лежит в git сейчас. Файл, удалённый из индекса, из истории не
62
+ исчез — это лечится сменой ключа, а не проверкой.
63
+ - **Файл, который лежит на диске и в `.gitignore`.** Это норма, а не находка: проверка спрашивает
64
+ git, а не диск. (Ошибка соседа, agentlint, ровно в этом: он проверяет диск и кричит
65
+ «закоммичен» про файл из `.gitignore`.)
66
+
67
+ **Образцы.** Настоящего git внутри каталога комплекта взять неоткуда, поэтому образцы кладут
68
+ список отслеживаемых файлов в `.aqk-tracked`; в живом проекте его не бывает и спрашивается git.
69
+ `red/` — `.env` с человеческими паролями (без формата и энтропии — ровно то, что сканеры
70
+ пропускают) и `.env.local` в индексе. `green/` — `.env` с публичными переменными сборки,
71
+ заглушками, ссылками `${…}`, паролями по умолчанию для локального стенда; `.env.production`,
72
+ зашифрованный `dotenvx`; `.env.test` с тестовым паролем; `.env.local` в `tests/`; `.env` в
73
+ `tests/fixtures/` и `templates/`. На всём этом арбитр обязан молчать.
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env sh
2
+ # Файл окружения с настоящими значениями лежит в git: пароль, токен или ключ разошёлся со всеми
3
+ # клонами — и из истории его уже не вычистить.
4
+ #
5
+ # ЗАЧЕМ СВОЯ ПРОВЕРКА, если есть сканеры секретов. Замер 2026-09-11 на закоммиченном `.env`
6
+ # (research/competitors/agentlint.md): наш `secrets-not-in-code` узнаёт секрет только по формату
7
+ # поставщика (`sk_live_…`); `gitleaks` 8.30.1 — по формату и по ЭНТРОПИИ: случайную строку
8
+ # (`wN7rQ2vLx9Tp`, энтропия 3,6) он ловит, а пароль, придуманный человеком (`Sup3rS3cret2024`,
9
+ # `hunter2hunter2`), пропускает. У такого пароля нет ни формата, ни энтропии — его узнают
10
+ # только по ИМЕНИ ключа. `detect-secrets` 1.5.0 имя ключа читает и оба ловит, но `.env.local`
11
+ # не видит вовсе: такой файл личный по определению, что бы в нём ни лежало.
12
+ #
13
+ # ДВА ПРАВИЛА, и второе не шире, чем нужно.
14
+ # 1. `.env.local` и `.env.*.local` в git — находка всегда. Документация Vite: «.env.*.local
15
+ # files are local-only and can contain sensitive variables. You should add `*.local` to your
16
+ # `.gitignore`»; Next.js: «.env*.local are intended to be ignored through .gitignore».
17
+ # 2. Остальные `.env*` в git — находка, только если внутри ключ-секрет с НАСТОЯЩИМ значением.
18
+ # Не любой `.env`: Next.js велит коммитить `.env.test`, во многих проектах на Vite
19
+ # коммитят `.env` с публичными настройками, а зашифрованный `dotenvx` файл коммитить
20
+ # рекомендует сам dotenv («Should I commit my .env file? No. Unless you encrypt it with
21
+ # dotenvx»). Проверка «файл есть в git» без чтения значений врала бы на всех троих.
22
+ #
23
+ # ЗНАЧЕНИЕ НЕ ПЕЧАТАЕТСЯ. Вывод гейта попадает в лог конвейера; напечатать пароль значило бы
24
+ # завести вторую утечку ради сообщения о первой. Называются файл, строка и имя ключа.
25
+ DIR="${1:-.}"
26
+ # Существование файла проверяется ДО `.`: под dash неудачный `.` завершает скрипт немедленно, и
27
+ # запасная ветка не выполняется никогда (замерено 2026-09-08 на соседней записи).
28
+ SKIP_LIB="$(dirname "$0")/../_skip.sh"
29
+ if [ ! -f "$SKIP_LIB" ]; then
30
+ echo "рядом с проверкой нет _skip.sh — обход не собран, проверка не состоялась"
31
+ echo " почини: скопируй гейт вместе с файлом kit/gates/_skip.sh, он общий на весь каталог"
32
+ exit 2
33
+ fi
34
+ . "$SKIP_LIB"
35
+ if [ "${AQK_SKIP_READY:-}" != 1 ]; then
36
+ echo "_skip.sh есть, но обход не собрался — проверка НЕ СОСТОЯЛАСЬ, а не прошла"
37
+ echo " почини: замени kit/gates/_skip.sh целым файлом из каталога"
38
+ exit 2
39
+ fi
40
+
41
+ # Образцы (gates.sh) несут список отслеживаемых файлов в `.aqk-tracked`: вложенный .git внутри
42
+ # каталога комплекта создал бы embedded-репозиторий. В настоящем проекте спрашиваем git.
43
+ if [ -f "$DIR/.aqk-tracked" ]; then
44
+ LIST=$(cat "$DIR/.aqk-tracked")
45
+ else
46
+ if ! (cd "$DIR" 2>/dev/null && git rev-parse --git-dir >/dev/null 2>&1); then
47
+ echo "не git-репозиторий — закоммитить сюда нечего"
48
+ exit 0
49
+ fi
50
+ # core.quotePath=false: иначе путь с не-ASCII приходит в кавычках, и якорь «$» не совпадает.
51
+ LIST=$(cd "$DIR" && git -c core.quotePath=false ls-files 2>/dev/null)
52
+ fi
53
+ LIST=$(printf '%s\n' "$LIST" | tr -d '\r')
54
+
55
+ # Кандидаты — файлы окружения по имени. Шаблоны (`.example`, `.sample`, `.template`, `.dist`) и
56
+ # каталоги заготовок и тестовых данных — не утечка: их кладут намеренно, чтобы скопировать.
57
+ # Замер на соседней записи: больше половины совпадений по имени были такими.
58
+ ENVS=$(printf '%s\n' "$LIST" \
59
+ | grep -E '(^|/)\.env(\.[A-Za-z0-9_.-]+)?$' \
60
+ | grep -vE '\.(example|sample|template|dist|schema)$' \
61
+ | grep -vE '(^|/)(templates?|examples?|samples?|fixtures?|__fixtures__|testdata|test_data|dist)/' \
62
+ | grep -vE '(^|/)kit/gates/[^/]+/(red|green)(/|$)' \
63
+ || true)
64
+ if command -v own_samples_filter >/dev/null 2>&1 || type own_samples_filter >/dev/null 2>&1; then
65
+ ENVS=$(printf '%s\n' "$ENVS" | own_samples_filter "$DIR" | grep -v '^$' || true)
66
+ fi
67
+ [ -z "$ENVS" ] && exit 0
68
+
69
+ # Секрет узнаётся по имени ключа и по значению. Ложные — это то, что в git класть МОЖНО:
70
+ # · публичные по устройству переменные сборки (`NEXT_PUBLIC_`, `VITE_`, `REACT_APP_`, …) —
71
+ # они и так уезжают в браузер;
72
+ # · пустое значение, ссылка `${VAR}`/`$VAR`, заглушка (`changeme`, `<…>`, `your_…`);
73
+ # · значения по умолчанию для локального стенда (`postgres`, `root`, `admin`, `guest`…) —
74
+ # тот же пароль стоит в каждом docker-compose для разработки;
75
+ # · зашифрованное `dotenvx` (`encrypted:…`); числа и логические (`TOKEN_TTL=86400`);
76
+ # · короче восьми знаков — настоящий пароль короче почти не бывает, а «dev» и «x» бывают.
77
+ # Интервалы `{n}` в регулярках НЕ использовать: mawk из node:22-slim их не понимает, и две
78
+ # записи каталога однажды молчали на своих красных образцах ровно из-за этого.
79
+ # Построчно, а не `for F in $ENVS`: путь с пробелом развалился бы на части. Цикл в конвейере
80
+ # идёт в подоболочке, поэтому находки собираются в вывод, а не в переменную-флаг.
81
+ OUT=$(printf '%s\n' "$ENVS" | while IFS= read -r F; do
82
+ BASE=${F##*/}
83
+ case "$BASE" in
84
+ .env.local|.env.*.local)
85
+ # В каталоге тестов и демо-приложений `.env.local` — фикстура: библиотека показывает или
86
+ # проверяет, что умеет его читать. Замер 2026-09-11: `tests/.env.local` у motdotla/dotenv
87
+ # и `playgrounds/*/.env.local` у инструментов для Next.js — обе находки ложные. Правило 2
88
+ # (настоящий пароль внутри) там действует: утечка в тестах остаётся утечкой.
89
+ if printf '%s\n' "$F" | grep -qE '(^|/)(tests?|__tests__|specs?|playgrounds?|demos?)/'; then :; else
90
+ echo "$F: личный файл окружения отслеживается git — «.local» по определению только для этой машины"
91
+ continue
92
+ fi ;;
93
+ # Next.js: «.env.test file should be included in your repository».
94
+ .env.test|.env.testing) continue ;;
95
+ esac
96
+ [ -f "$DIR/$F" ] || continue
97
+ HITS=$(tr -d '\r' < "$DIR/$F" | awk '
98
+ function placeholder(v, l) {
99
+ l = tolower(v)
100
+ if (l == "") return 1
101
+ if (l ~ /^encrypted:/) return 1
102
+ if (l ~ /^\$/) return 1
103
+ if (l ~ /^<.*>$/) return 1
104
+ if (l ~ /^(your|example|dummy|placeholder|replace|change)[-_ a-z0-9]*$/) return 1
105
+ if (l ~ /^(changeme|todo|none|null|secret|password|test|dev|development|local|localhost)$/) return 1
106
+ if (l ~ /^(postgres|mysql|root|admin|guest|redis|minioadmin|user|pass)$/) return 1
107
+ if (l ~ /^x+$/ || l ~ /^\*+$/ || l ~ /^\.+$/) return 1
108
+ if (l ~ /^[0-9]+$/ || l ~ /^(true|false|yes|no|on|off)$/) return 1
109
+ return 0
110
+ }
111
+ /^[ \t]*#/ || /^[ \t]*$/ { next }
112
+ {
113
+ line = $0
114
+ sub(/^[ \t]*export[ \t]+/, "", line)
115
+ eq = index(line, "=")
116
+ if (eq == 0) next
117
+ key = substr(line, 1, eq - 1); val = substr(line, eq + 1)
118
+ gsub(/^[ \t]+|[ \t]+$/, "", key); gsub(/^[ \t]+|[ \t]+$/, "", val)
119
+ sub(/[ \t]+#.*$/, "", val)
120
+ if (val ~ /^".*"$/ || val ~ /^\047.*\047$/) val = substr(val, 2, length(val) - 2)
121
+ k = toupper(key)
122
+ if (k ~ /^(NEXT_PUBLIC_|VITE_|REACT_APP_|PUBLIC_|EXPO_PUBLIC_|NUXT_PUBLIC_|GATSBY_)/) next
123
+ # Пароль внутри адреса: схема://пользователь:пароль@хост.
124
+ if (match(val, /:\/\/[^\/:@ ]+:[^\/@ ]+@/)) {
125
+ cred = substr(val, RSTART + 3, RLENGTH - 4); pw = substr(cred, index(cred, ":") + 1)
126
+ if (!placeholder(pw)) { print NR ": " key " — пароль внутри адреса"; next }
127
+ }
128
+ if (k !~ /(PASSWORD|PASSWD|SECRET|TOKEN|API_?KEY|PRIVATE_?KEY|ACCESS_?KEY|CREDENTIAL)/) next
129
+ if (placeholder(val) || length(val) < 8) next
130
+ print NR ": " key " — похоже на настоящее значение"
131
+ }')
132
+ [ -n "$HITS" ] && printf '%s\n' "$HITS" | sed "s|^|$F:|; s|\$| (само значение не печатаю)|"
133
+ done)
134
+
135
+ [ -z "$OUT" ] && exit 0
136
+ printf '%s\n' "$OUT"
137
+ echo " почини: git rm --cached <файл> и добавь его в .gitignore; в git оставь .env.example с пустыми"
138
+ echo " значениями. И смени эти пароли и ключи: из истории git их уже не вычистить."
139
+ exit 1
@@ -0,0 +1,21 @@
1
+ intent: личные файлы окружения и настоящие пароли из .env не попадают в git
2
+ intent_en: personal environment files and real passwords from .env do not end up in git
3
+
4
+ # Только там, где файлом окружения пользуются: признак считает `.env`, `.env.example`,
5
+ # `.env.sample` в корне. Проекту без них запись показывать незачем.
6
+ trigger:
7
+ has_env: true
8
+
9
+ # Переносимая проверка — единственный рецепт, и это решение, а не пробел. Готовый `detect-secrets`
10
+ # ловит пароли по имени ключа, но не видит `.env.local` (личный по определению, что бы в нём ни
11
+ # лежало) и требует файла-базы для работы в конвейере; `gitleaks` пропускает человеческие пароли.
12
+ # Замер и таблица — в README записи.
13
+ recipes:
14
+ any: bash {gate}/check.sh {dir}
15
+
16
+ tool: https://github.com/Yelp/detect-secrets
17
+
18
+ proof: incidents/README.md, 2026-09-11 «пароль из .env, которого не видит ни один сканер» —
19
+ пять репозиториев с закоммиченным .env.production.local из поиска кода GitHub: гейт краснеет на
20
+ всех пяти, четыре находки настоящие; контрольные — одиннадцать репозиториев, ложных после
21
+ исключения ноль
@@ -0,0 +1,10 @@
1
+ README.md
2
+ .env
3
+ .env.example
4
+ .env.production
5
+ .env.test
6
+ tests/fixtures/.env
7
+ templates/.env
8
+ src/app.py
9
+ tests/.env.local
10
+ playgrounds/app/.env.local
@@ -0,0 +1,10 @@
1
+ # Значения по умолчанию, которые коммитят намеренно: секретов здесь нет
2
+ APP_ENV=development
3
+ VITE_API_KEY=pk_demo_public_1234567
4
+ DB_PASSWORD=
5
+ API_TOKEN=changeme
6
+ SECRET_KEY=${APP_SECRET}
7
+ REDIS_PASSWORD=<set-in-vault>
8
+ TOKEN_TTL_SECONDS=86400000
9
+ DATABASE_URL=postgres://postgres:postgres@localhost:5432/app
10
+ RABBITMQ_URL=amqp://guest:guest@localhost:5672/
@@ -0,0 +1,5 @@
1
+ #/-------------------[DOTENV_PUBLIC_KEY]--------------------/
2
+ #/ public-key encryption for .env files /
3
+ #/----------------------------------------------------------/
4
+ DOTENV_PUBLIC_KEY_PRODUCTION="03f8b0c2d1e4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1"
5
+ DB_PASSWORD="encrypted:BG8M6U+GKJGwpGA42ml2erb9+T2NBX6Z2JkBLynDy21poz0UfF5aPxCgRbIyhnQFdWKd0C9GZ7lM5PeL86xghoMcWvvPpkyQ0yaD2pZ64RzoxFGB1lTZYlEgQOxTDJnWxODHfuQcFY10uA=="
@@ -0,0 +1,2 @@
1
+ # Next.js: .env.test положено держать в репозитории — значения для тестов
2
+ DB_PASSWORD=testonly-9Xk2mQ
@@ -0,0 +1,5 @@
1
+ README.md
2
+ .env
3
+ .env.local
4
+ .env.example
5
+ src/app.py
@@ -0,0 +1,7 @@
1
+ # Настройки прода, закоммиченные по ошибке. Пароли — человеческие, без узнаваемого формата:
2
+ # ровно то, что сканеры по форме и энтропии пропускают.
3
+ APP_ENV=production
4
+ DB_HOST=db.internal
5
+ DB_PASSWORD=Sup3rS3cret2024
6
+ DATABASE_URL=postgres://app:Winter2024Pass@db.internal:5432/app
7
+ export JWT_SECRET="hunter2hunter2"
@@ -27,3 +27,7 @@ samples_for: python
27
27
 
28
28
  proof: incidents/README.md, 2026-09-07 «замер по пяти стекам вынес приговор пяти записям» —
29
29
  434 находки переносимой проверки на пяти чужих репозиториях, настоящих ноль
30
+
31
+ # Правило Biome под эту запись — для сверки `covers:` (рецептов под Biome нет). Имя сверено по
32
+ # схеме конфигурации Biome 2.5.12, а не по памяти.
33
+ biome_rules: suspicious/noConsole
@@ -26,3 +26,7 @@ proof: incidents/README.md, 2026-09-07 «замер по пяти стекам
26
26
  переносимая версия дала 46 находок на пяти чужих репозиториях, настоящими оказались только
27
27
  пустые `catch {}` в zod; весь питоновский класс `except ImportError: pass` и растовый
28
28
  `let _ = write(..)?` были ложными
29
+
30
+ # Правило Biome под эту запись — для сверки `covers:` (рецептов под Biome нет). Имя сверено по
31
+ # схеме конфигурации Biome 2.5.12, а не по памяти.
32
+ biome_rules: suspicious/noEmptyBlockStatements
@@ -16,3 +16,7 @@ recipes:
16
16
  typescript: eslint --no-config-lookup --ignore-pattern 'gates/*/red/**' --ignore-pattern 'gates/*/green/**' --rule '{"no-warning-comments":["error",{"terms":["todo","fixme","hack","xxx"]}]}' {dir}
17
17
 
18
18
  proof: incidents/README.md — «2026-08-25 разбор 1069 коммитов»: 416 коммитов из 1069 оказались стабилизацией уже выкаченного
19
+
20
+ # У Biome правила под TODO нет вовсе — сверено по схеме конфигурации Biome 2.5.12. Заявка
21
+ # «lint (biome) держит эту запись» поэтому заведомо неверна, и doctor так и говорит.
22
+ biome_rules: none
package/llms.txt CHANGED
@@ -40,10 +40,16 @@ Zero runtime dependencies. Node 18+ and an `sh` shell. MIT.
40
40
  enforces, what the ratchets hold. Where it does not know, it says so: a run that never happened
41
41
  is reported as unknown, never as clean. `context --install` writes a `SessionStart` hook into
42
42
  `.claude/settings.json` (Claude Code only; the rest of the kit stays vendor-neutral). Measured:
43
- the block is ~375 tokens, and carries what a file cannot — what changed today. `context --full`
43
+ the block is ~500 tokens, and carries what a file cannot — what changed today, the next three
44
+ steps with ready commands, and when to run which command (before a commit, after changing a
45
+ check, when a check is in the way). `context --full`
44
46
  adds the command map and the rulebook verbatim (~7000 tokens): a deliberate trade, chosen by
45
47
  the owner after the objection about long inputs, on the grounds that an agent reads files
46
48
  poorly and the tokens are the price of it not guessing
49
+ - Hand the agent one task instead of retelling the diagnosis: `npx agent-quality-kit prompt` —
50
+ ground rules on top, at most five items by weight (red gates, defects the probe planted and no
51
+ check caught, the project's own checks to declare, rules Claude Code cannot see, catalog
52
+ entries to propose), and for every item the command that proves it done. Nothing is run
47
53
  - Check that the kit's own wiring is actually connected: `npx agent-quality-kit vitals` — are the
48
54
  tools the declared gates need installed, is the hook present in `.git/hooks` (a line in the
49
55
  config is an intention, not a guard), does the agent receive the state, is the version current.
@@ -70,7 +76,7 @@ Zero runtime dependencies. Node 18+ and an `sh` shell. MIT.
70
76
  the files `init` writes are owned by root, so you cannot edit your own manifest. Debian-based
71
77
  on purpose: the gates are `sh`, `grep`, `awk`, `find` — under alpine's busybox they behave
72
78
  differently, and an image where the gates behave differently is worse than no image
73
- - As a GitHub Action: `uses: arsen-ask-lx/Agent_Quality_Kit@v0.12.0` with `min: 1`
79
+ - As a GitHub Action: `uses: arsen-ask-lx/Agent_Quality_Kit@v0.14.0` with `min: 1`
74
80
  (https://github.com/marketplace/actions/agent-quality-kit-aqk)
75
81
 
76
82
  ## What makes it different
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-quality-kit",
3
- "version": "0.12.0",
3
+ "version": "0.14.0",
4
4
  "description": "Turns the rules an agent is supposed to follow into commands with exit codes, and reports which of them actually run. Zero dependencies.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -45,7 +45,7 @@ async function cmdBadge(args = []) {
45
45
  // Прогон, а не манифест. Значок при красном гейте — это и есть недоказанное утверждение.
46
46
  const gates = declaredGates(man);
47
47
  if (gates.length) {
48
- const run = runGates(man);
48
+ const run = await runGates(man);
49
49
  if (run.failed) {
50
50
  const red = run.results.filter((r) => !r.ok).map((r) => r.name).join(", ");
51
51
  die(`\n ${L.badge.redGates(run.failed, red)}\n`);
@@ -23,8 +23,12 @@
23
23
  import { readFile, writeFile, mkdir } from "node:fs/promises";
24
24
  import { spawnSync } from "node:child_process";
25
25
  import { join } from "node:path";
26
- import { CWD, TARGET_DIR, SELF, c, exists, commandRows } from "../lib/core.mjs";
27
- import { readManifest, assessLevel } from "../lib/manifest.mjs";
26
+ import { CWD, TARGET_DIR, SELF, c, exists, commandRows, preCommitHook } from "../lib/core.mjs";
27
+ import { readManifest, assessLevel, coversOf } from "../lib/manifest.mjs";
28
+ import { detectFacts, readCatalog } from "../lib/repo.mjs";
29
+ import { catalogBuckets, startWith, blindAdvice } from "../lib/advice.mjs";
30
+ import { proposeGates, readAdoptFiles } from "../lib/adopt.mjs";
31
+ import { declaredGates } from "../lib/run.mjs";
28
32
  import { probeStatus } from "./probe.mjs";
29
33
  import { L } from "../i18n/index.mjs";
30
34
 
@@ -33,6 +37,23 @@ import { L } from "../i18n/index.mjs";
33
37
  const MAX_RED = 5;
34
38
  const MAX_RATCHETS = 3;
35
39
 
40
+ // ЧТО ДЕЛАТЬ ДАЛЬШЕ — не больше трёх шагов, по убыванию того, насколько это ФАКТ о проекте:
41
+ // 1. объявить проверки, которые у проекта уже есть: дешевле всего, и это его собственное;
42
+ // 2. классы брака, которые проба подсадила в ЕГО файлы и ЕГО проверки не поймали;
43
+ // 3. «начните с этих трёх» из каталога.
44
+ // Класс, названный пробой, в третьем списке не повторяется: один шаг, а не два. Остаток —
45
+ // числом: молчание о нём прочиталось бы как «больше делать нечего».
46
+ function nextSteps({ init = false, adopt = [], blind = [], start = [] } = {}, max = 3) {
47
+ const all = [];
48
+ // Без манифеста `aqk add` отказывает — остальные шаги без этого невыполнимы.
49
+ if (init) all.push({ kind: "init" });
50
+ if (adopt.length) all.push({ kind: "adopt", gates: adopt });
51
+ for (const b of blind) all.push({ kind: "blind", ...b });
52
+ const seen = new Set(blind.map((b) => b.slug));
53
+ for (const st of start) if (!seen.has(st.slug)) all.push({ kind: "start", ...st });
54
+ return { steps: all.slice(0, max), rest: Math.max(0, all.length - max) };
55
+ }
56
+
36
57
  // Чистая функция: на входе состояние, на выходе строки. Отделена от чтения диска намеренно —
37
58
  // это единственное место комплекта, чей текст читает машина, и проверять его надо не прогоном,
38
59
  // а перебором случаев, включая те, которых на нашем репозитории не бывает.
@@ -86,6 +107,22 @@ function contextBlock(state, T = L.context) {
86
107
  else out.push(T.probeClean(pr.state === "stale" ? pr.behind : 0));
87
108
  }
88
109
 
110
+ // ДАЛЬШЕ — то, что `doctor` знает, а агенту не говорилось: блок отвечал только «как дела».
111
+ // Шаги вычислены, а не пожелания: у каждого команда, которую можно выполнить сейчас.
112
+ const nx = state.next;
113
+ if (nx && nx.steps && nx.steps.length) {
114
+ out.push("", T.nextTitle);
115
+ nx.steps.forEach((st, i) => out.push(`${i + 1}. ${T.nextStep[st.kind](st)}`));
116
+ if (nx.rest) out.push(T.nextMore(nx.rest));
117
+ }
118
+
119
+ // КОГДА ЧТО — правила вида «ситуация → команда». Общий совет («тестируй изменения») агент
120
+ // пролистывает; проверяемый («перед коммитом — вот эта команда») выполняет. Первое правило
121
+ // зависит от факта: стоит хук — сказать не обходить его; нет — дать команду, само не случится.
122
+ if (state.when) {
123
+ out.push("", T.whenTitle, `- ${T.whenCommit(state.when.hook)}`, ...T.whenRules.map((r) => `- ${r}`));
124
+ }
125
+
89
126
  // ПОЛНЫЙ БЛОК — решение владельца от 2026-09-08, принятое ПОСЛЕ возражения и вопреки ему.
90
127
  // Возражение было такое: вход, растущий в длину, роняет качество у всех проверенных моделей,
91
128
  // и свод, влитый целиком, даёт правило, которое в контексте есть и не выполняется. Ответ
@@ -223,6 +260,37 @@ async function installHook(full = false) {
223
260
  console.log(c.dim(` ${T.hookWhat}`));
224
261
  }
225
262
 
263
+ // Прошлый прогон — из отчёта, который кладёт `doctor --run`. Отдельной функцией: его читают и
264
+ // `context`, и `prompt`, и два разбора одного файла разошлись бы.
265
+ async function readRun() {
266
+ const lastRun = join(CWD, TARGET_DIR, "last-run.md");
267
+ if (!(await exists(lastRun))) return null;
268
+ const run = parseLastRun(await readFile(lastRun, "utf8"));
269
+ if (run) run.stale = runIsStale(run.when);
270
+ return run;
271
+ }
272
+
273
+ // Что советовать — теми же функциями, что у `doctor`: корзины каталога, «начните с трёх», совет
274
+ // под язык, чужие проверки проекта. Одно место на `context` и `prompt`: второй расчёт того же
275
+ // самого разошёлся бы с первым. Класс из пробы, чей гейт уже стоит, в совет не идёт — ставить
276
+ // его второй раз бессмысленно.
277
+ async function readAdvice(man, probe) {
278
+ const facts = await detectFacts(man);
279
+ const catalog = await readCatalog();
280
+ const { todo } = catalogBuckets(catalog, facts, coversOf(man).covered);
281
+ const adopt = declaredGates(man).length ? [] : proposeGates(await readAdoptFiles(CWD));
282
+ const blind = (probe?.classes || [])
283
+ .filter((b) => !facts.gateKeys.includes(b.slug))
284
+ .map((b) => ({ ...b, command: blindAdvice(catalog.find((r) => r.slug === b.slug), facts, {}).command }));
285
+ const start = startWith(todo, facts, 3)
286
+ .map((rec) => ({ slug: rec.slug, intent: rec.intent || "", command: blindAdvice(rec, facts, {}).command }));
287
+ // Гейт стоит, проба его ГОНЯЛА — и брак он пропустил. Самое ценное, что проба знает: не
288
+ // «поставь», а «твоя проверка здесь слепа». Гейт, поставленный после пробы, сюда не идёт —
289
+ // поймает ли, покажет следующая.
290
+ const missed = (probe?.classes || []).filter((b) => facts.gateKeys.includes(b.slug) && probe?.ran?.has(b.slug));
291
+ return { adopt, blind, start, missed };
292
+ }
293
+
226
294
  async function cmdContext(args = []) {
227
295
  const full = args.includes("--full");
228
296
  if (args.includes("--install")) return installHook(full);
@@ -249,12 +317,7 @@ async function cmdContext(args = []) {
249
317
  rules = countArbiters(await readFile(join(CWD, entry), "utf8"), ["человек", "human", "nobody"]);
250
318
  }
251
319
 
252
- let run = null;
253
- const lastRun = join(CWD, TARGET_DIR, "last-run.md");
254
- if (await exists(lastRun)) {
255
- run = parseLastRun(await readFile(lastRun, "utf8"));
256
- if (run) run.stale = runIsStale(run.when);
257
- }
320
+ const run = await readRun();
258
321
 
259
322
  // Проба: сколько классов не ловит никто и насколько отметка отстала. Читается из файла,
260
323
  // ничего не запускает — блок обязан укладываться в секунду.
@@ -293,9 +356,18 @@ async function cmdContext(args = []) {
293
356
  fullPart = { entry, rows, text };
294
357
  }
295
358
 
359
+ // ДАЛЬШЕ — теми же функциями, что у `doctor`: корзины каталога, «начните с трёх», совет под
360
+ // язык, чужие проверки проекта. Второй расчёт того же самого разошёлся бы с первым.
361
+ let next = null;
362
+ try {
363
+ const { adopt, blind, start } = await readAdvice(man, probe);
364
+ next = nextSteps({ init: !man, adopt, blind, start });
365
+ } catch { /* не посчитали — блок скажет остальное; выдумывать шаги нельзя */ }
366
+
296
367
  console.log(contextBlock({
297
368
  entry, entryExists: rules !== null, level, rules, run, ratchets, probe, full: fullPart,
369
+ next, when: { hook: await preCommitHook(CWD) },
298
370
  }).join("\n"));
299
371
  }
300
372
 
301
- export { cmdContext, contextBlock, parseLastRun, countArbiters, withHook, hasOurHook, portableSelf };
373
+ export { cmdContext, contextBlock, nextSteps, parseLastRun, countArbiters, withHook, hasOurHook, portableSelf, readRun, readAdvice };