agent-quality-kit 0.14.0 → 0.16.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 +88 -18
  2. package/README.ru.md +91 -19
  3. package/kit/docs/ai/index.md +1 -0
  4. package/kit/docs/ai/operational-gates.md +275 -0
  5. package/kit/gates/_target.sh +53 -0
  6. package/kit/gates/ci-actually-fails/check.sh +18 -3
  7. package/kit/gates/ci-actually-fails/green/.github/workflows/ci.yml +10 -0
  8. package/kit/gates/entry-commands-exist/check.sh +88 -12
  9. package/kit/gates/hook-actually-fires/README.md +12 -0
  10. package/kit/gates/hook-actually-fires/check.sh +66 -6
  11. package/kit/gates/hook-actually-fires/gate.yml +2 -2
  12. package/kit/gates/hook-actually-fires/green/.claude/hooks/auto-format.sh +3 -0
  13. package/kit/gates/hook-actually-fires/green/.claude/hooks/block-dangerous.sh +3 -0
  14. package/kit/gates/hook-actually-fires/green/.claude/hooks/done.sh +3 -0
  15. package/kit/gates/hook-actually-fires/green/.claude/hooks/idle.sh +3 -0
  16. package/kit/gates/hook-actually-fires/green/.claude/hooks/prompt.sh +3 -0
  17. package/kit/gates/hook-actually-fires/green/.claude/hooks/session.mjs +1 -0
  18. package/kit/gates/hook-actually-fires/green/.claude/hooks/stop-gate.sh +3 -0
  19. package/kit/gates/hook-actually-fires/green/.claude/settings.json +12 -0
  20. package/kit/gates/test-not-adjusted/README.md +31 -0
  21. package/llms.txt +26 -7
  22. package/package.json +1 -1
  23. package/tool/commands/context.mjs +39 -41
  24. package/tool/commands/doctor-catalog.mjs +35 -10
  25. package/tool/commands/doctor.mjs +18 -26
  26. package/tool/commands/feedback.mjs +231 -0
  27. package/tool/commands/gates.mjs +12 -6
  28. package/tool/commands/project.mjs +11 -13
  29. package/tool/commands/prompt.mjs +2 -1
  30. package/tool/commands/report.mjs +19 -3
  31. package/tool/commands/vitals.mjs +9 -3
  32. package/tool/i18n/en-docs.mjs +19 -2
  33. package/tool/i18n/en-gates.mjs +35 -0
  34. package/tool/i18n/en.mjs +29 -2
  35. package/tool/i18n/ru-docs.mjs +18 -2
  36. package/tool/i18n/ru-gates.mjs +36 -0
  37. package/tool/i18n/ru.mjs +27 -2
  38. package/tool/lib/adopt.mjs +58 -4
  39. package/tool/lib/ask.mjs +118 -0
  40. package/tool/lib/brief.mjs +17 -38
  41. package/tool/lib/core.mjs +49 -12
  42. package/tool/lib/execution.mjs +32 -1
  43. package/tool/lib/gate-worker.mjs +4 -1
  44. package/tool/lib/manifest.mjs +39 -13
  45. package/tool/lib/prove.mjs +3 -3
  46. package/tool/lib/run.mjs +142 -10
  47. package/tool/program.mjs +6 -0
  48. package/tool/selfcheck/smoke/_fixture.mjs +13 -1
  49. package/tool/selfcheck/smoke/fail-closed.test.mjs +96 -1
  50. package/tool/selfcheck/smoke/feedback-send.test.mjs +87 -0
  51. package/tool/selfcheck/smoke/first-run.test.mjs +67 -3
  52. package/tool/selfcheck/smoke/preflight.test.mjs +83 -0
  53. package/tool/selfcheck/smoke/verdict.test.mjs +50 -4
  54. package/tool/selfcheck/smoke/version-sync.test.mjs +140 -0
  55. package/tool/selfcheck/smoke.sh +106 -4
  56. package/tool/selfcheck/units-ask.mjs +85 -0
  57. package/tool/selfcheck/units-brief.mjs +3 -13
  58. package/tool/selfcheck/units-context.mjs +2 -1
  59. package/tool/selfcheck/units-execution.mjs +37 -1
  60. package/tool/selfcheck/units-feedback.mjs +137 -0
  61. package/tool/selfcheck/units-level.mjs +41 -1
  62. package/tool/selfcheck/units-repo.mjs +75 -0
  63. package/tool/selfcheck/units-vitals.mjs +27 -0
  64. package/kit/gates/entry-links-exist/README.md +0 -27
  65. package/kit/gates/entry-links-exist/check.sh +0 -33
  66. package/kit/gates/entry-links-exist/gate.yml +0 -17
  67. package/kit/gates/entry-links-exist/green/AGENTS.md +0 -10
  68. package/kit/gates/entry-links-exist/green/rules/general.md +0 -3
  69. package/kit/gates/entry-links-exist/red/AGENTS.md +0 -3
  70. package/kit/gates/no-phantom-package/README.md +0 -84
  71. package/kit/gates/no-phantom-package/check.sh +0 -168
  72. package/kit/gates/no-phantom-package/gate.yml +0 -20
  73. package/kit/gates/no-phantom-package/green/AGENTS.md +0 -15
  74. package/kit/gates/no-phantom-package/red/AGENTS.md +0 -15
@@ -1,84 +0,0 @@
1
- # Пакета с таким именем нет в реестре
2
-
3
- **Намерение.** Агент, не знающий инструмента, придумывает правдоподобное имя: `reactCodemodHelper`,
4
- `eslint-plugin-async-safe`, `@types/fetch-retry`. Имя попадает в `AGENTS.md`, в `SKILL.md`, в
5
- пример из README. Дальше его читает следующий агент — человек или машина — и выполняет
6
- `npm install`. Если к тому времени имя занято, в проект приезжает чужой код.
7
-
8
- **Какой отказ это поймало.** README самого `slopcheck` открывается историей: модель написала
9
- команду `npx` с именем `react-codeshift` в 47 файлах, пакета не существовало, кто-то его
10
- зарегистрировал, и 237 репозиториев уже на него ссылались. Имя намеренно оторвано здесь от слова
11
- `npx`: иначе эта строка стала бы находкой самой записи, а вердикт на `main` зависел бы от того,
12
- не снимет ли реестр захваченный пакет с публикации. Красный образец выбирается так, чтобы его
13
- нельзя было погасить снаружи, — и текст вокруг него тоже. Мы проверили этот пакет 2026-09-08:
14
-
15
- ```
16
- react-codeshift 1.0.0 создан 2026-01-14 maintainer: debugducky
17
- ```
18
-
19
- Доля: по [USENIX Security 2025](https://arxiv.org/abs/2406.10279) около 20% сгенерированного
20
- кода ссылается на несуществующие пакеты, и 58% выдуманных имён повторяются от запроса к
21
- запросу — то есть предсказуемы для того, кто захочет их занять.
22
-
23
- **Почему машина, а не внимательность.** Отличить `jscodeshift` от `reactCodemodHelper` глазами
24
- нельзя: оба выглядят как настоящие. Единственный арбитр — реестр.
25
-
26
- **Готовый аналог есть, и мы его зовём.**
27
- [`slopcheck`](https://github.com/mattschaller/slopcheck) (npm, MIT, ноль зависимостей) достаёт
28
- имена из команд установки в `.md`, `.mdc`, `.yml`, `.yaml`, `.json`, `.cursorrules` и сверяет с
29
- `registry.npmjs.org`. Своего разбора мы не писали. Обёртка отвечает за три вещи, которых он не
30
- делает: какие файлы ему дать (иначе он находит наши собственные образцы), что считать браком и
31
- что делать, когда реестр не ответил.
32
-
33
- **Почему обёртка, а не прямой вызов.** Без сети `slopcheck` печатает `? имя — validation error`
34
- и выходит **с нулём**. Для гейта это худший из возможных ответов: сборка зелёная, а не проверено
35
- ничего — ровно та тишина, ради запрета которой существует весь стандарт. Обёртка в этом случае
36
- выходит с кодом **2** и говорит «проверка не состоялась», отдельно от вердикта «брак».
37
- Тот же код — если счётчик находок ненулевой, а разобрать их не вышло: значит `slopcheck` сменил
38
- формат вывода, и молчать об этом нельзя. И тот же — если блока со счётчиками в ответе не нашлось
39
- вовсе: так будет, если он начнёт печатать JSON одной строкой. Эту ветку нашли прогоном
40
- подставного вывода **после** того, как написали «формат стабилен»: без неё все счётчики
41
- оставались нулями и гейт выходил с нулём, не проверив ничего.
42
-
43
- **Образцы.** В `red/AGENTS.md` кодмод вызван именем `reactCodemodHelper`. Имя выбрано не наугад: `npm`
44
- запрещает заглавные буквы в именах новых пакетов, и это подтверждает та же библиотека, которой
45
- пользуется реестр:
46
-
47
- ```
48
- $ node -e "console.log(require('validate-npm-package-name')('reactCodemodHelper'))"
49
- validForNewPackages: false errors: [ 'name can no longer contain capital letters' ]
50
- ```
51
-
52
- Значит образец не сможет протухнуть: занять это имя нельзя никому, а выглядит оно как настоящая
53
- галлюцинация — модели постоянно пишут camelCase. `green/AGENTS.md` — тот же файл, где кодмод
54
- назван настоящим именем `jscodeshift`. Второй пакет, `prettier`, стоит в обоих: он показывает,
55
- что зелёным гейт становится не от пустоты.
56
-
57
- **Чего НЕ ловит.**
58
-
59
- - **Пакет, который сквоттер уже зарегистрировал.** Проверка спрашивает у реестра только «есть
60
- ли», и гаснет ровно в тот момент, когда становится опасно: `react-codeshift` сегодня зелёный.
61
- Это предел приёма, а не недоделка — «есть ли пакет» и «чей он» разные вопросы, и второй
62
- решают Socket и Snyk, которых мы не дублируем. Смысл записи в другом: до регистрации проходит
63
- время, и большинство выдуманных имён так и остаются свободными.
64
- - **Только npm.** PyPI, crates.io, Go-модули не проверяются: `slopcheck` их не умеет, а писать
65
- своё — заводить второй разбор команд установки. Питоновский проект, где агент придумал имя
66
- пакета, эта запись не прикроет.
67
- - **Имя из прозы, принятое за пакет.** Разбор берёт слово после `npm i`/`npx`/`yarn add` и
68
- спотыкается о текст, где эти слова стоят не как команда. Проверено на README самого
69
- `slopcheck`: слово `Commands`, стоящее в прозе после имени команды, даёт находку
70
- `Commands — not found on npm`. На
71
- нашем репозитории — 89 файлов, 9 имён — ложных нет, но на чужом такое встретится. Первым,
72
- кого эта запись покрасила, был её собственный README: имена образцов стояли там рядом с
73
- `npx`. Текст переписан, проверка — нет. Ослепить её на README записей каталога значило бы
74
- перестать видеть настоящие команды установки, которых там хватает.
75
- - **Пакет, названный в исходнике, а не в документации.** `import` из несуществующего модуля —
76
- предмет сборки, она об этом скажет сама.
77
- - **Сеть.** Запись — единственная в каталоге, которой нужен интернет. В конвейере без выхода
78
- наружу она честно выйдет с кодом 2, а не соврёт зелёным. Тем же кодом отвечает ответ реестра
79
- «слишком часто» (HTTP 429): снаружи он неотличим от обрыва связи, и сообщение называет обе
80
- причины, а не выбирает одну наугад.
81
- - **Свой предел времени.** `doctor` даёт гейту 300 секунд, а здесь каждое имя — запрос наружу
82
- (до трёх попыток по десять секунд, по десять имён разом). Проект с сотнями команд установки
83
- при медленном реестре упрётся в предел и будет показан как `timeout` — не как «проверка не
84
- состоялась». Эту разницу изнутри проверки не выразить: её съедает тот, кто её обрывает.
@@ -1,168 +0,0 @@
1
- #!/usr/bin/env sh
2
- # Имена пакетов, названные в документации, сверяются с реестром npm.
3
- #
4
- # ЗАЧЕМ. Агент, не знающий инструмента, придумывает правдоподобное имя. Дальше его читает
5
- # другой агент и выполняет `npm install` — и если имя к тому времени кем-то занято, в проект
6
- # приезжает чужой код. Обычные проверки цепочки поставок смотрят на пакеты, которые ЕСТЬ;
7
- # здесь опасен как раз тот, которого пока нет.
8
- #
9
- # Работу делает slopcheck (MIT, ноль зависимостей). Мы отвечаем за три вещи, которых он не
10
- # делает: какие файлы ему дать, что считать браком и что делать, когда ответа не было.
11
- # Каждая ветка «ответа не было» кончается кодом 2 и словами «проверка не состоялась»: у
12
- # инструмента, написанного против молчаливого зелёного, своего молчаливого зелёного быть не может.
13
- DIR="${1:-.}"
14
-
15
- # Существование файла проверяется ДО `.`, а не запасной веткой после. В POSIX-оболочке (здесь
16
- # dash) неудачный `.` завершает скрипт немедленно — привычное «. файл || запасной_вариант» не
17
- # выполняется НИКОГДА, и гейт умирает с кодом 2 без единого слова о причине. Проверено прогоном
18
- # 2026-09-08; та же мёртвая ветка стоит ещё в девяти проверках каталога.
19
- SKIP_LIB="$(dirname "$0")/../_skip.sh"
20
- if [ ! -f "$SKIP_LIB" ]; then
21
- echo "рядом с проверкой нет _skip.sh — обход не собран, проверка не состоялась"
22
- echo " почини: скопируй гейт вместе с файлом kit/gates/_skip.sh, он общий на весь каталог"
23
- exit 2
24
- fi
25
- . "$SKIP_LIB"
26
- # Файл на месте — этого мало: подмена содержимого давала код 0. Метка стоит в КОНЦЕ _skip.sh,
27
- # поэтому проверка ловит и обрыв файла на середине.
28
- if [ "${AQK_SKIP_READY:-}" != 1 ]; then
29
- echo "_skip.sh есть, но обход не собрался — проверка НЕ СОСТОЯЛАСЬ, а не прошла"
30
- echo " почини: замени kit/gates/_skip.sh целым файлом из каталога"
31
- exit 2
32
- fi
33
-
34
- # Даже когда файл на месте, нужных функций в нём может не оказаться: без них конвейер ниже не
35
- # напечатает ни одного пути, список окажется пуст, а пустой список — это `exit 0`. Гейт вышел бы
36
- # зелёным, не посмотрев ни в один файл.
37
- if ! command -v own_samples_filter >/dev/null 2>&1; then
38
- echo "в _skip.sh нет обхода own_samples_filter — проверка не состоялась"
39
- echo " почини: обнови kit/gates/_skip.sh до версии, идущей с этим гейтом"
40
- exit 2
41
- fi
42
-
43
- if ! command -v slopcheck >/dev/null 2>&1; then
44
- echo "не найден slopcheck — эта проверка делегирована ему"
45
- echo " почини: npm i -g slopcheck@0.2.0"
46
- exit 2
47
- fi
48
-
49
- # Свой обход, а не встроенный в slopcheck: только так работает own_samples_filter. Без него
50
- # красный образец этой же записи выдаётся за находку, и гейт краснеет на самом комплекте.
51
- LIST=$(mktemp) || exit 2
52
- # shellcheck disable=SC2046
53
- find "$DIR" $(skip_find "$DIR") -type f \
54
- \( -name '*.md' -o -name '*.mdc' -o -name '*.yml' -o -name '*.yaml' \
55
- -o -name '*.json' -o -name '.cursorrules' \) -print 2>/dev/null \
56
- | own_samples_filter "$DIR" > "$LIST"
57
-
58
- if [ ! -s "$LIST" ]; then rm -f "$LIST"; exit 0; fi
59
-
60
- # Через NUL, а не через $(...): путь с пробелом иначе распадается на два аргумента, slopcheck
61
- # не находит ни одного из них и молча выходит с нулём. Та же ловушка уже стоила нам зелёного
62
- # вердикта в hook-actually-fires.
63
- ERRF=$(mktemp) || { rm -f "$LIST"; exit 2; }
64
- OUT=$(tr '\n' '\0' < "$LIST" | xargs -0 slopcheck --json 2>"$ERRF")
65
- # Код xargs: 0 — все вызовы прошли, 123 — хотя бы один вернул 1..125. Именно 123 приходит и на
66
- # честной находке (slopcheck выходит с 1), поэтому сам по себе он ничего не значит. А вот всё
67
- # остальное — сбой обвязки, и его нельзя принимать за вердикт.
68
- XS=$?
69
- ERR=$(cat "$ERRF" 2>/dev/null)
70
- rm -f "$ERRF" "$LIST"
71
-
72
- # Файлов может оказаться больше, чем влезает в одну команду, и тогда xargs зовёт slopcheck
73
- # несколько раз. Упавший вызов пишет в stderr, а уцелевшие всё равно печатают свой JSON —
74
- # счётчики сходятся, и гейт вышел бы с нулём, не проверив целую партию файлов.
75
- if [ -n "$ERR" ] || { [ "$XS" -ne 0 ] && [ "$XS" -ne 123 ]; }; then
76
- echo "slopcheck не отработал (код $XS) — проверка не состоялась, это не вердикт «чисто»"
77
- [ -n "$ERR" ] && printf '%s\n' "$ERR" | head -5 | sed 's/^/ /'
78
- echo " почини: прогони «slopcheck --json .» руками и посмотри, на чём он споткнулся"
79
- exit 2
80
- fi
81
-
82
- [ -n "$OUT" ] || {
83
- echo "slopcheck ничего не ответил — проверка не состоялась, это не вердикт «чисто»"
84
- echo " почини: прогони «slopcheck --json .» руками и посмотри, на чём он споткнулся"
85
- exit 2
86
- }
87
-
88
- printf '%s\n' "$OUT" | awk '
89
- function val(s) { sub(/^[^:]*:[[:space:]]*/, "", s); sub(/,$/, "", s); gsub(/^"|"$/, "", s); return s }
90
- function label(s) {
91
- if (s == "not_found") return "в реестре такого пакета нет"
92
- if (s == "unpublished") return "пакет был и снят с публикации — имя свободно для захвата"
93
- if (s == "security_hold") return "реестр пометил пакет как вредоносный"
94
- return ""
95
- }
96
- # xargs может позвать slopcheck несколько раз, если путей слишком много для одной команды.
97
- # Тогда JSON-документов на входе будет несколько, и счётчики надо складывать, а не брать
98
- # последний: иначе находки первой партии исчезнут.
99
- /^ "packages"/ { inpkg = 1; sawpkg = 1; next }
100
- inpkg && /^ \}/ { inpkg = 0; next }
101
- inpkg && /"errors"/ { errs += val($0); next }
102
- inpkg && /"notFound"/ { bad += val($0); next }
103
- inpkg && /"unpublished"/ { bad += val($0); next }
104
- inpkg && /"securityHold"/ { bad += val($0); next }
105
- /^ "package"/ { pkg = val($0); next }
106
- /^ "status"/ { st = val($0); next }
107
- /^ "file"/ { f = val($0); next }
108
- /^ "line"/ { ln = val($0); next }
109
- # Печатаем на «command»: он идёт последним в записи о месте, значит к этому моменту известны
110
- # и пакет, и состояние, и файл со строкой.
111
- /^ "command"/ {
112
- cmd = val($0)
113
- if (st == "error") next
114
- # Состояние, которого мы не знаем. Раньше здесь стояло «всё, что не error, — брак», и
115
- # новое состояние в следующей версии slopcheck превратило бы КАЖДУЮ команду установки в
116
- # находку с английским словом вместо объяснения. Незнакомое состояние — повод сказать
117
- # «не разобрали», а не вынести вердикт.
118
- if (label(st) == "") { unknown = st; next }
119
- if (shown < 20) print f ":" ln ": «" pkg "» — " label(st) " · " cmd
120
- shown++
121
- next
122
- }
123
- END {
124
- if (unknown != "") {
125
- print "slopcheck вернул незнакомое состояние «" unknown "» — разобрать ответ не вышло"
126
- print " почини: сверь версию slopcheck с той, что названа в gate.yml"
127
- exit 2
128
- }
129
- if (shown > 20) print " … и ещё " (shown - 20)
130
- if (shown > 0) {
131
- # Находки и «не проверено» показываются вместе. Свернуть второе в первое значило бы
132
- # потерять ровно ту разницу, ради которой написана эта обёртка.
133
- if (errs > 0) print " кроме того, реестр не ответил по именам: " errs " — они НЕ проверены"
134
- print " почини: проверь имя в реестре и впиши настоящее. Если пакета нет, его может"
135
- print " зарегистрировать кто угодно — и следующий агент выполнит установку чужого кода."
136
- exit 1
137
- }
138
- # Реестр не ответил. Молчать здесь нельзя: тишина неотличима от «всё хорошо», а именно
139
- # это наш стандарт и запрещает. Красим отдельным кодом, чтобы вердикт не путали с браком.
140
- # Причина названа обеими: slopcheck помечает так и недоступную сеть, и ответ 429 «слишком
141
- # часто», и любой другой не-200 — снаружи они неразличимы, и врать про сеть мы не будем.
142
- if (errs > 0) {
143
- print "реестр npm не ответил: не проверено имён — " errs
144
- print " проверка не состоялась. Это не вердикт «чисто»."
145
- print " почини: если сети нет — прогони гейт там, где она есть. Если сеть есть, реестр"
146
- print " мог ограничить частоту запросов: повтори через минуту."
147
- exit 2
148
- }
149
- # Блока со счётчиками не нашлось вовсе. Так будет, если slopcheck начнёт печатать JSON
150
- # одной строкой: ни одно правило выше не сработает, все счётчики останутся нулями — и
151
- # проверка выйдет с нулём, ничего не проверив. Нашли это прогоном подставного вывода уже
152
- # после того, как записали «формат стабилен»: догадка о стабильности стоила бы молчаливого
153
- # зелёного на каждом прогоне.
154
- if (!sawpkg) {
155
- print "ответ slopcheck не разобран: блока «packages» в нём нет"
156
- print " почини: сверь версию slopcheck с той, что названа в gate.yml"
157
- exit 2
158
- }
159
- # Счётчик говорит о браке, а печатать нечего — значит мы разучились читать вывод slopcheck
160
- # (сменился формат). Тоже не вердикт «чисто».
161
- if (bad > 0) {
162
- print "slopcheck насчитал находок: " bad ", но разобрать их не вышло — сменился формат"
163
- print " почини: сверь версию slopcheck с той, что названа в gate.yml"
164
- exit 2
165
- }
166
- exit 0
167
- }
168
- '
@@ -1,20 +0,0 @@
1
- intent: имя пакета, названное в документации, существует в реестре
2
- intent_en: a package name mentioned in the documentation exists in the registry
3
-
4
- # Там, где агента используют: предмет проверки — текст, который агент читает и по которому
5
- # выполняет установку. В проекте без такого текста ставить запись незачем.
6
- trigger:
7
- has_agent_entry: true
8
-
9
- recipes:
10
- any: bash {gate}/check.sh {dir}
11
-
12
- # Программа, без которой запись не работает. По первому слову команды этого не видно: обёртка
13
- # начинается с `bash`, который есть всегда. Версия названа: обёртка разбирает JSON slopcheck,
14
- # и смена формата обязана быть видимой, а не тихой.
15
- requires: slopcheck
16
-
17
- proof: incidents/README.md, 2026-09-08 «пакет-призрак из чужого README оказался
18
- зарегистрирован» — `react-codeshift`, канонический пример slopsquatting из README самого
19
- slopcheck, на 2026-09-08 существует в npm (создан 2026-01-14, maintainer debugducky);
20
- доля выдуманных имён в сгенерированном коде — USENIX Security 2025, arxiv 2406.10279
@@ -1,15 +0,0 @@
1
- # Правила проекта
2
-
3
- ## Как чинить импорты
4
-
5
- Перед коммитом прогони кодмод:
6
-
7
- ```bash
8
- npx jscodeshift --fix src/
9
- ```
10
-
11
- ## Форматирование
12
-
13
- ```bash
14
- npm install prettier
15
- ```
@@ -1,15 +0,0 @@
1
- # Правила проекта
2
-
3
- ## Как чинить импорты
4
-
5
- Перед коммитом прогони кодмод:
6
-
7
- ```bash
8
- npx reactCodemodHelper --fix src/
9
- ```
10
-
11
- ## Форматирование
12
-
13
- ```bash
14
- npm install prettier
15
- ```