agent-quality-kit 0.15.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.
- package/README.md +61 -15
- package/README.ru.md +62 -15
- package/kit/docs/ai/index.md +1 -0
- package/kit/docs/ai/operational-gates.md +275 -0
- package/kit/gates/_target.sh +53 -0
- package/kit/gates/ci-actually-fails/check.sh +18 -3
- package/kit/gates/ci-actually-fails/green/.github/workflows/ci.yml +10 -0
- package/kit/gates/entry-commands-exist/check.sh +88 -12
- package/kit/gates/hook-actually-fires/README.md +12 -0
- package/kit/gates/hook-actually-fires/check.sh +66 -6
- package/kit/gates/hook-actually-fires/gate.yml +2 -2
- package/kit/gates/hook-actually-fires/green/.claude/hooks/auto-format.sh +3 -0
- package/kit/gates/hook-actually-fires/green/.claude/hooks/block-dangerous.sh +3 -0
- package/kit/gates/hook-actually-fires/green/.claude/hooks/done.sh +3 -0
- package/kit/gates/hook-actually-fires/green/.claude/hooks/idle.sh +3 -0
- package/kit/gates/hook-actually-fires/green/.claude/hooks/prompt.sh +3 -0
- package/kit/gates/hook-actually-fires/green/.claude/hooks/session.mjs +1 -0
- package/kit/gates/hook-actually-fires/green/.claude/hooks/stop-gate.sh +3 -0
- package/kit/gates/hook-actually-fires/green/.claude/settings.json +12 -0
- package/kit/gates/test-not-adjusted/README.md +31 -0
- package/llms.txt +26 -7
- package/package.json +1 -1
- package/tool/commands/context.mjs +3 -1
- package/tool/commands/doctor-catalog.mjs +35 -10
- package/tool/commands/feedback.mjs +75 -1
- package/tool/commands/gates.mjs +12 -6
- package/tool/commands/report.mjs +19 -3
- package/tool/commands/vitals.mjs +9 -3
- package/tool/i18n/en-docs.mjs +18 -2
- package/tool/i18n/en-gates.mjs +9 -1
- package/tool/i18n/en.mjs +24 -2
- package/tool/i18n/ru-docs.mjs +17 -2
- package/tool/i18n/ru-gates.mjs +9 -1
- package/tool/i18n/ru.mjs +20 -2
- package/tool/lib/adopt.mjs +58 -4
- package/tool/lib/core.mjs +42 -6
- package/tool/lib/execution.mjs +32 -1
- package/tool/lib/manifest.mjs +39 -13
- package/tool/lib/prove.mjs +3 -3
- package/tool/lib/run.mjs +25 -6
- package/tool/selfcheck/smoke/_fixture.mjs +13 -1
- package/tool/selfcheck/smoke/feedback-send.test.mjs +87 -0
- package/tool/selfcheck/smoke/first-run.test.mjs +67 -3
- package/tool/selfcheck/smoke/preflight.test.mjs +83 -0
- package/tool/selfcheck/smoke/verdict.test.mjs +50 -4
- package/tool/selfcheck/smoke/version-sync.test.mjs +140 -0
- package/tool/selfcheck/smoke.sh +106 -4
- package/tool/selfcheck/units-execution.mjs +37 -1
- package/tool/selfcheck/units-level.mjs +41 -1
- package/tool/selfcheck/units-repo.mjs +75 -0
- package/tool/selfcheck/units-vitals.mjs +27 -0
- package/kit/gates/entry-links-exist/README.md +0 -27
- package/kit/gates/entry-links-exist/check.sh +0 -33
- package/kit/gates/entry-links-exist/gate.yml +0 -17
- package/kit/gates/entry-links-exist/green/AGENTS.md +0 -10
- package/kit/gates/entry-links-exist/green/rules/general.md +0 -3
- package/kit/gates/entry-links-exist/red/AGENTS.md +0 -3
- package/kit/gates/no-phantom-package/README.md +0 -84
- package/kit/gates/no-phantom-package/check.sh +0 -168
- package/kit/gates/no-phantom-package/gate.yml +0 -20
- package/kit/gates/no-phantom-package/green/AGENTS.md +0 -15
- package/kit/gates/no-phantom-package/red/AGENTS.md +0 -15
|
@@ -90,6 +90,12 @@ for F in $CI; do
|
|
|
90
90
|
for (j = 1; j <= nr; j++) if (R[j] != "" && R[j] == id) return 1
|
|
91
91
|
return 0
|
|
92
92
|
}
|
|
93
|
+
# Погашенная команда, о которой ещё не решено. Печатается, когда стало видно, что после неё
|
|
94
|
+
# в блоке ничего нет: тогда исход шага действительно погашен.
|
|
95
|
+
function reportMask( ) {
|
|
96
|
+
if (maskLine) printf "%s:%d: провал погашен прямо в команде: %s\n", file, maskLine, substr(maskText, 1, 90)
|
|
97
|
+
maskLine = 0
|
|
98
|
+
}
|
|
93
99
|
function flush( ) {
|
|
94
100
|
if (blockStart && blockCheck && blockMask && !isRedeemed(blockId))
|
|
95
101
|
printf "%s:%d: проверка не может провалиться — шаг под %s\n", file, blockCheckLine, blockMaskText
|
|
@@ -100,8 +106,17 @@ for F in $CI; do
|
|
|
100
106
|
# вспомогательной команде внутри скрипта (`docker network create … || true`) — это
|
|
101
107
|
# идемпотентность, а не выключенная проверка.
|
|
102
108
|
if (!isComment($0) && $0 ~ runners && $0 ~ /\|\|[[:space:]]*(true|:|exit[[:space:]]+0)/) {
|
|
103
|
-
|
|
104
|
-
|
|
109
|
+
# ОБВИНЯЕМ, ТОЛЬКО ЕСЛИ ПОГАШЕННАЯ КОМАНДА — ПОСЛЕДНЯЯ В БЛОКЕ. Замер 2026-09-14 по
|
|
110
|
+
# семидесяти чужим конвейерам дал одно-единственное срабатывание, и оно было ЛОЖНЫМ:
|
|
111
|
+
# `pnpm eslint src > out.txt || true` строкой ниже сверяется `diff` с эталоном —
|
|
112
|
+
# инструмент ОБЯЗАН выйти ненулевым, а вердикт выносит следующая команда. Шаг
|
|
113
|
+
# проваливается прекрасно. Инструмент, который обвиняет напрасно, выключают целиком,
|
|
114
|
+
# поэтому здесь молчание честнее догадки.
|
|
115
|
+
maskLine = NR; maskText = $0; sub(/^[[:space:]]+/, "", maskText)
|
|
116
|
+
} else if (maskLine && !isComment($0) && $0 !~ /^[[:space:]]*$/) {
|
|
117
|
+
# Печать вердикта не выносит: `echo` после гашения ничего не меняет.
|
|
118
|
+
if ($0 ~ /^[[:space:]]*(-[[:space:]]+)?[A-Za-z0-9_.-]+[[:space:]]*:/) reportMask()
|
|
119
|
+
else if ($0 !~ /^[[:space:]]*(echo|printf|cat|ls)[[:space:]]/) maskLine = 0
|
|
105
120
|
}
|
|
106
121
|
if (isBoundary($0)) flush()
|
|
107
122
|
if (!blockStart) blockStart = NR
|
|
@@ -111,7 +126,7 @@ for F in $CI; do
|
|
|
111
126
|
blockId = $0; sub(/^[^:]*:[[:space:]]*/, "", blockId); gsub(/[[:space:]"'"'"']/, "", blockId)
|
|
112
127
|
}
|
|
113
128
|
}
|
|
114
|
-
END { flush() }' 2>/dev/null)
|
|
129
|
+
END { reportMask(); flush() }' 2>/dev/null)
|
|
115
130
|
[ -z "$RES" ] || BAD="$BAD$RES
|
|
116
131
|
"
|
|
117
132
|
done
|
|
@@ -28,3 +28,13 @@ jobs:
|
|
|
28
28
|
- name: провалить сборку, если линтер был красным
|
|
29
29
|
if: steps.lint.outcome == 'failure'
|
|
30
30
|
run: exit 1
|
|
31
|
+
|
|
32
|
+
# ЗАКОННОЕ ГАШЕНИЕ: инструмент ОБЯЗАН выйти ненулевым, а вердикт выносит следующая
|
|
33
|
+
# команда того же блока. Найдено замером 2026-09-14 по семидесяти чужим конвейерам:
|
|
34
|
+
# это было ЕДИНСТВЕННОЕ срабатывание гейта на всём замере — и оно оказалось ложным
|
|
35
|
+
# (HorusGoul/eslint-plugin-react-render-types, .github/workflows/ci.yml:51). Снимок
|
|
36
|
+
# вывода линтера сверяется с эталоном, шаг проваливается на `diff`.
|
|
37
|
+
- name: линтер как снимок
|
|
38
|
+
run: |
|
|
39
|
+
pnpm eslint src > /tmp/lint-output.txt || true
|
|
40
|
+
diff lint-snapshot.txt /tmp/lint-output.txt
|
|
@@ -41,12 +41,28 @@ ENTRIES=$( { find "$DIR" -maxdepth 1 -type f \( -iname 'agents.md' -o -iname 'cl
|
|
|
41
41
|
# --- что в проекте есть ---------------------------------------------------------
|
|
42
42
|
# Скрипты — только из блока "scripts" каждого package.json (рабочие пространства тоже: команда
|
|
43
43
|
# из свода часто живёт в пакете, а не в корне). Файл склеивается в строку: блок бывает и в одну.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
# ИМЕНА СКРИПТОВ ЧИТАЕТ РАЗБОРЩИК JSON, А НЕ РЕГУЛЯРКА. Найдено 2026-09-15 на
|
|
45
|
+
# `jantimon/web-performance-debugger`: их скрипт `prebuild` содержит
|
|
46
|
+
# `rmSync('dist',{recursive:true,force:true})` — фигурная скобка ВНУТРИ значения. Прежний разбор
|
|
47
|
+
# `[^}]*` обрывался на ней, и шесть существующих скриптов объявлялись несуществующими. Письмо
|
|
48
|
+
# ушло бы живому человеку с неправдой.
|
|
49
|
+
#
|
|
50
|
+
# Структуру разбирает тот, кто умеет её разбирать. `package.json` означает проект на Node, и node
|
|
51
|
+
# там почти наверняка есть; но «почти» нам не годится — без него мы НЕ ПРОВЕРЯЕМ скрипты и
|
|
52
|
+
# говорим об этом, а не додумываем регуляркой.
|
|
53
|
+
NPM_BLIND=""
|
|
54
|
+
if command -v node >/dev/null 2>&1; then
|
|
55
|
+
SCRIPTS=$(find "$DIR" $(skip_find) -type f -name package.json -print 2>/dev/null | own_samples_filter "$DIR" | grep -v '^$' \
|
|
56
|
+
| while IFS= read -r P; do
|
|
57
|
+
node -e 'try{const s=require("fs").readFileSync(process.argv[1],"utf8");const j=JSON.parse(s);for(const k of Object.keys(j.scripts||{}))console.log(k)}catch(e){}' "$P"
|
|
58
|
+
done)
|
|
59
|
+
else
|
|
60
|
+
SCRIPTS=""
|
|
61
|
+
NPM_BLIND=1
|
|
62
|
+
fi
|
|
49
63
|
HAS_PKG=$(find "$DIR" $(skip_find) -type f -name package.json -print 2>/dev/null | own_samples_filter "$DIR" | grep -v '^$' | head -1)
|
|
64
|
+
HAS_JUST=$(find "$DIR" $(skip_find) -type f \( -name justfile -o -name Justfile -o -name .justfile \) -print 2>/dev/null | own_samples_filter "$DIR" | grep -v '^$' | head -1)
|
|
65
|
+
HAS_MK=$(find "$DIR" $(skip_find) -type f \( -name Makefile -o -name makefile -o -name GNUmakefile -o -name '*.mk' \) -print 2>/dev/null | own_samples_filter "$DIR" | grep -v '^$' | head -1)
|
|
50
66
|
|
|
51
67
|
# Цели make: строка вне рецепта, слова до двоеточия; «VAR := x» — присваивание, не цель.
|
|
52
68
|
TARGETS=$(find "$DIR" $(skip_find) -type f \( -name Makefile -o -name makefile -o -name GNUmakefile -o -name '*.mk' \) -print 2>/dev/null \
|
|
@@ -54,11 +70,35 @@ TARGETS=$(find "$DIR" $(skip_find) -type f \( -name Makefile -o -name makefile -
|
|
|
54
70
|
awk '/^[^\t#][^=]*:([^=]|$)/ { sub(/:.*/, ""); n = split($0, a, /[ \t]+/); for (i = 1; i <= n; i++) if (a[i] != "") print a[i] }' "$M"
|
|
55
71
|
done)
|
|
56
72
|
|
|
73
|
+
# ПОДКЛЮЧЁННЫЙ ФАЙЛ, КОТОРОГО У НАС НЕТ, ДЕЛАЕТ ПЕРЕЧЕНЬ ЦЕЛЕЙ НЕПОЛНЫМ. Найдено 2026-09-15 на
|
|
74
|
+
# `exoscale/cli`: их Makefile начинается с `include go.mk/init.mk`, а `go.mk` — подмодуль git,
|
|
75
|
+
# которого в клоне нет. Цели `build` и `test-verbose` объявлены там, и наше «такой цели нет» было
|
|
76
|
+
# бы письмом с неправдой в живую компанию.
|
|
77
|
+
#
|
|
78
|
+
# Молчим ОБО ВСЕХ целях make, а не о конкретной: какая именно объявлена в неподключённом файле,
|
|
79
|
+
# мы знать не можем. Это третий исход — «не смогли проверить», и он назван вслух, а не спрятан.
|
|
80
|
+
MK_BLIND=""
|
|
81
|
+
for M in $(find "$DIR" $(skip_find) -type f \( -name Makefile -o -name makefile -o -name GNUmakefile -o -name '*.mk' \) -print 2>/dev/null | own_samples_filter "$DIR" | grep -v '^$'); do
|
|
82
|
+
for INC in $(sed -n 's/^[[:space:]]*-\{0,1\}include[[:space:]]\{1,\}//p' "$M" 2>/dev/null | tr ' ' '\n' | grep -v '^$'); do
|
|
83
|
+
case "$INC" in
|
|
84
|
+
*'$'*) continue ;;
|
|
85
|
+
esac
|
|
86
|
+
[ -e "$(dirname "$M")/$INC" ] || MK_BLIND="$MK_BLIND$INC
|
|
87
|
+
"
|
|
88
|
+
done
|
|
89
|
+
done
|
|
90
|
+
|
|
57
91
|
# Рецепты just: «имя:», «@имя арг:», «alias имя := …».
|
|
58
92
|
RECIPES=$(find "$DIR" $(skip_find) -type f \( -name justfile -o -name Justfile -o -name .justfile \) -print 2>/dev/null \
|
|
59
93
|
| own_samples_filter "$DIR" | grep -v '^$' | while IFS= read -r J; do
|
|
94
|
+
# ПАРАМЕТР РЕЦЕПТА СОДЕРЖИТ ЗНАК РАВЕНСТВА. Найдено 2026-09-15 на `2mawi2/para`: их
|
|
95
|
+
# `release BUMP="patch":` — обычный рецепт со значением по умолчанию. Прежнее правило
|
|
96
|
+
# требовало `[^:=]*` до двоеточия и такой рецепт не видело: существующая команда
|
|
97
|
+
# объявлялась несуществующей. Присваивание (`version := "1.0"`) отсекается отдельно —
|
|
98
|
+
# у него двоеточие СРАЗУ перед равенством, а у рецепта равенство стоит до двоеточия.
|
|
60
99
|
awk '/^alias[ \t]+/ { print $2; next }
|
|
61
|
-
/^@?[A-Za-z_][A-Za-z0-9_-]*
|
|
100
|
+
/^@?[A-Za-z_][A-Za-z0-9_-]*[ \t]*:=/ { next }
|
|
101
|
+
/^@?[A-Za-z_][A-Za-z0-9_-]*([ \t][^:]*)?:/ { sub(/^@/, ""); sub(/[ \t:].*/, ""); print }' "$J"
|
|
62
102
|
done)
|
|
63
103
|
|
|
64
104
|
has() { printf '%s\n' "$2" | grep -qxF -- "$1"; }
|
|
@@ -82,20 +122,41 @@ while IFS= read -r E; do
|
|
|
82
122
|
CODE=$(awk '/^[ \t]*(```|~~~)/ { f = !f; next }
|
|
83
123
|
f { print; next }
|
|
84
124
|
{ while (match($0, /`[^`]+`/)) { print substr($0, RSTART + 1, RLENGTH - 2); $0 = substr($0, RSTART + RLENGTH) } }' "$E")
|
|
85
|
-
|
|
86
|
-
|
|
125
|
+
# «BUN RUN <ФАЙЛ>» — ЗАПУСК ФАЙЛА, А НЕ СКРИПТА. Найдено 2026-09-15 на `Gerstep/HumanCompiler`:
|
|
126
|
+
# `bun run scripts/generate-plugin.ts <profile>` — путь прочитался как имя скрипта «scripts».
|
|
127
|
+
# Отсекаем по признаку пути: косая черта или расширение исполняемого файла.
|
|
128
|
+
grep -oE '(npm|pnpm|yarn|bun)[[:space:]]+run[[:space:]]+[A-Za-z][A-Za-z0-9_:./-]*[*<{]?' "$E" \
|
|
129
|
+
| grep -v '[*<{:]$' | grep -vE '[[:space:]][A-Za-z0-9_.-]*/' \
|
|
130
|
+
| grep -vE '\.(ts|js|mjs|cjs|tsx|jsx|py|sh)$' \
|
|
131
|
+
| sed 's/\.$//; s/[[:space:]][[:space:]]*/ /g' | sort -u > "$TMP"
|
|
87
132
|
while IFS= read -r CMD; do
|
|
133
|
+
# Без разборщика JSON перечень скриптов неполон, и «такого скрипта нет» — догадка.
|
|
134
|
+
[ -n "$NPM_BLIND" ] && continue
|
|
88
135
|
N=${CMD##* }
|
|
89
136
|
has "$N" "$SCRIPTS" && continue
|
|
90
|
-
|
|
91
|
-
|
|
137
|
+
# СОБСТВЕННОЙ СБОРКИ У РЕПОЗИТОРИЯ НЕТ — значит свод, скорее всего, описывает ДРУГОЙ проект.
|
|
138
|
+
# Найдено 2026-09-15 на `Aurealibe/claude-config` и `Weaverse/.agents`: это сборники правил,
|
|
139
|
+
# которые ставят в проект-получатель, и своего package.json у них нет и не должно быть.
|
|
140
|
+
# Про какой проект написано «npm run build», снаружи не видно — обвинять нельзя.
|
|
141
|
+
[ -n "$HAS_PKG" ] || { NO_BUILD=1; continue; }
|
|
142
|
+
report "$E" "$CMD" "такого скрипта нет ни в одном package.json"
|
|
92
143
|
done < "$TMP"
|
|
93
|
-
|
|
144
|
+
# «MAKE» — ОБЫЧНЫЙ АНГЛИЙСКИЙ ГЛАГОЛ, как и «just». Найдено 2026-09-15 на
|
|
145
|
+
# `andyhartzler/my-bluebubbles-web`: «make it go away», «make one of these go away» дали цели
|
|
146
|
+
# «it» и «one». У них выше по файлу незакрытый блок кода, и весь текст после него читается как
|
|
147
|
+
# команды — но чинить надо не разбор блоков, а предмет обвинения: Makefile в репозитории нет
|
|
148
|
+
# вовсе, сравнивать не с чем.
|
|
149
|
+
[ -n "$HAS_MK" ] && for N in $(printf '%s\n' "$CODE" | grep -E '(^|[^A-Za-z0-9_-])make[[:space:]]' | grep -vE -- '-C|--directory|-f[[:space:]]|--file' \
|
|
94
150
|
| grep -oE '(^|[^A-Za-z0-9_-])make([[:space:]]+-[A-Za-z0-9]+)*[[:space:]]+[A-Za-z][A-Za-z0-9_.-]*=?' \
|
|
95
151
|
| grep -v '=$' | sed 's/.*[[:space:]]//' | sort -u); do
|
|
152
|
+
[ -n "$MK_BLIND" ] && continue
|
|
96
153
|
has "$N" "$TARGETS" || report "$E" "make $N" "такой цели нет ни в одном Makefile"
|
|
97
154
|
done
|
|
98
|
-
|
|
155
|
+
# «JUST» — ОБЫЧНОЕ АНГЛИЙСКОЕ СЛОВО, и в прозе оно стоит чаще, чем в роли запускалки. Найдено
|
|
156
|
+
# 2026-09-15: «I just uploaded a new video» и «fuzzy output (`just over`)» дали находки
|
|
157
|
+
# «рецепт uploaded» и «рецепт over» в репозиториях, где justfile нет вовсе. Без justfile
|
|
158
|
+
# сравнивать не с чем — обвинение без предмета.
|
|
159
|
+
[ -n "$HAS_JUST" ] && for N in $(printf '%s\n' "$CODE" | grep -oE '(^|[^A-Za-z0-9_-])just[[:space:]]+[A-Za-z][A-Za-z0-9_-]*' \
|
|
99
160
|
| sed 's/.*[[:space:]]//' | sort -u); do
|
|
100
161
|
has "$N" "$RECIPES" || report "$E" "just $N" "такого рецепта нет в justfile"
|
|
101
162
|
done
|
|
@@ -107,4 +168,19 @@ if [ "$MISS" = 1 ]; then
|
|
|
107
168
|
echo " почини: верни команду в проект или исправь свод. Агент берёт команды из свода дословно:"
|
|
108
169
|
echo " несуществующая команда — это «Missing script» у агента и «проверил» про проверку, которой нет."
|
|
109
170
|
fi
|
|
171
|
+
# ЧТО МЫ НЕ СМОГЛИ ПОСМОТРЕТЬ — говорится вслух и при находках, и без них. Молчание здесь
|
|
172
|
+
# означало бы «цели make проверены», а они не проверены вовсе.
|
|
173
|
+
if [ -n "$NO_BUILD" ]; then
|
|
174
|
+
echo " не проверено: команды npm. Своего package.json у репозитория нет — похоже, свод описывает"
|
|
175
|
+
echo " другой проект, в который его ставят. Про какой именно, снаружи не видно."
|
|
176
|
+
fi
|
|
177
|
+
if [ -n "$NPM_BLIND" ]; then
|
|
178
|
+
echo " не проверено: скрипты npm. Разобрать package.json нечем — на этой машине нет node."
|
|
179
|
+
echo " почини: поставь node либо прогони проверку там, где он есть."
|
|
180
|
+
fi
|
|
181
|
+
if [ -n "$MK_BLIND" ]; then
|
|
182
|
+
echo " не проверено: цели make. Makefile подключает файлы, которых нет в этом каталоге:"
|
|
183
|
+
printf '%s' "$MK_BLIND" | sort -u | sed 's/^/ /'
|
|
184
|
+
echo " обычно это подмодуль git — тогда проверять надо там, где он выгружен."
|
|
185
|
+
fi
|
|
110
186
|
exit "$MISS"
|
|
@@ -5,6 +5,18 @@
|
|
|
5
5
|
линтером. Ошибку в имени события Claude Code **не показывает** — хук просто никогда не
|
|
6
6
|
вызывается. Настройка выглядит как защита и защитой не является.
|
|
7
7
|
|
|
8
|
+
**Третий класс, добавленный 2026-09-14 — и нашёл его чужой инструмент.** `agnix`, прогнанный по
|
|
9
|
+
нашему же репозиторию, сказал «Script file not found» о **шести** хуках в нашем **зелёном**
|
|
10
|
+
образце: гейт с именем «хук правда срабатывает» говорил «чисто» о настройке, где ни один хук
|
|
11
|
+
сработать не мог, потому что ни одного из шести файлов не существовало. Теперь команда хука
|
|
12
|
+
сверяется с диском.
|
|
13
|
+
|
|
14
|
+
Граница узкая: красится только **относительный путь со слэшем** (`.claude/hooks/x.sh`,
|
|
15
|
+
`scripts/guard.sh`). Программа из PATH (`npx`, `prettier`, `bash -c …`) не судится — у неё нет
|
|
16
|
+
пути, и «не нашли» было бы обвинением по догадке. Абсолютный путь пропускается: он про чужую
|
|
17
|
+
машину, а не про этот репозиторий. `$CLAUDE_PROJECT_DIR` снимается как приставка — это и есть
|
|
18
|
+
корень проекта.
|
|
19
|
+
|
|
8
20
|
**Какой отказ это поймало.** Замер по 48 чужим настройкам `.claude/settings.json`, снятым с
|
|
9
21
|
GitHub 2026-09-07. Шесть настоящих находок в четырёх репозиториях:
|
|
10
22
|
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env sh
|
|
2
|
-
# Хук, который не сработает никогда: имя события с опечаткой,
|
|
3
|
-
#
|
|
2
|
+
# Хук, который не сработает никогда: имя события с опечаткой, `matcher` на событии, которое его
|
|
3
|
+
# не поддерживает, либо команда, указывающая на файл, которого нет.
|
|
4
|
+
#
|
|
5
|
+
# ТРЕТИЙ КЛАСС ДОБАВЛЕН 2026-09-14, И НАШЁЛ ЕГО ЧУЖОЙ ИНСТРУМЕНТ. `agnix`, прогнанный по нашему
|
|
6
|
+
# же репозиторию, сказал «Script file not found» о ШЕСТИ хуках в нашем ЗЕЛЁНОМ образце: гейт с
|
|
7
|
+
# именем `hook-actually-fires` говорил «чисто» о настройке, где ни один хук сработать не мог.
|
|
8
|
+
# Это второй раз за неделю, когда соседский инструмент находит у нас то, чего не видят наши же
|
|
9
|
+
# проверки, — и ровно тот класс, ради которого написан весь комплект.
|
|
4
10
|
#
|
|
5
11
|
# ЗАЧЕМ. Человек заводит хук, чтобы машина держала то, что он держать не может: не дать
|
|
6
12
|
# сделать force-push, отформатировать после правки, не отпустить работу с красным линтером.
|
|
@@ -117,6 +123,10 @@ for F in "$DIR/.claude/settings.json" "$DIR/.claude/settings.local.json" \
|
|
|
117
123
|
# означают «всё» — ровно то, что и происходит на событии без поддержки matcher, то
|
|
118
124
|
# есть автор не обманут. Замер по 39 чужим настройкам: без этого сужения гейт краснел
|
|
119
125
|
# на четырёх, и все четыре были «matcher»: "" либо "*", то есть шум.
|
|
126
|
+
if (awaitCmd) {
|
|
127
|
+
awaitCmd = 0
|
|
128
|
+
printf "@CMD@%d@%s\n", cmdLine, pending
|
|
129
|
+
}
|
|
120
130
|
if (awaitMatcher) {
|
|
121
131
|
awaitMatcher = 0
|
|
122
132
|
if (pending != "" && pending != "*" && pending != ".*")
|
|
@@ -130,6 +140,9 @@ for F in "$DIR/.claude/settings.json" "$DIR/.claude/settings.local.json" \
|
|
|
130
140
|
if (c == ":") {
|
|
131
141
|
key = pending; keyLine = pendingLine
|
|
132
142
|
if (ev != "" && key == "matcher" && (ev in NOMATCHER)) { awaitMatcher = 1; matcherLine = keyLine; matcherEv = ev }
|
|
143
|
+
# Команда хука. Существование файла проверяет ОБОЛОЧКА, а не awk: у awk нет способа
|
|
144
|
+
# спросить файловую систему, не вызывая внешний процесс на каждую строку.
|
|
145
|
+
if (key == "command") { awaitCmd = 1; cmdLine = keyLine }
|
|
133
146
|
continue
|
|
134
147
|
}
|
|
135
148
|
# ЛЮБОЙ структурный символ снимает ожидание значения matcher. Без этого нестроковое
|
|
@@ -137,7 +150,7 @@ for F in "$DIR/.claude/settings.json" "$DIR/.claude/settings.local.json" \
|
|
|
137
150
|
# следующая строка документа — обычно ключ «hooks» — печаталась как значение matcher.
|
|
138
151
|
# Найдено код-ревью 2026-09-07.
|
|
139
152
|
if (c == "{" || c == "[") {
|
|
140
|
-
awaitMatcher = 0
|
|
153
|
+
awaitMatcher = 0; awaitCmd = 0
|
|
141
154
|
depth++
|
|
142
155
|
if (c == "{" && rootIsHooks == 1 && depth == 1 && hooksDepth == -1) hooksDepth = 1
|
|
143
156
|
else if (c == "{" && key == "hooks" && hooksDepth == -1) hooksDepth = depth
|
|
@@ -145,12 +158,12 @@ for F in "$DIR/.claude/settings.json" "$DIR/.claude/settings.local.json" \
|
|
|
145
158
|
key = ""; continue
|
|
146
159
|
}
|
|
147
160
|
if (c == "}" || c == "]") {
|
|
148
|
-
awaitMatcher = 0
|
|
161
|
+
awaitMatcher = 0; awaitCmd = 0
|
|
149
162
|
if (depth == evDepth) { ev = ""; evDepth = -1 }
|
|
150
163
|
if (depth == hooksDepth) hooksDepth = -1
|
|
151
164
|
depth--; key = ""; continue
|
|
152
165
|
}
|
|
153
|
-
if (c == ",") { awaitMatcher = 0; key = ""; continue }
|
|
166
|
+
if (c == ",") { awaitMatcher = 0; awaitCmd = 0; key = ""; continue }
|
|
154
167
|
}
|
|
155
168
|
}
|
|
156
169
|
' "$F" 2>/tmp/.hookerr.$$)
|
|
@@ -160,6 +173,52 @@ for F in "$DIR/.claude/settings.json" "$DIR/.claude/settings.local.json" \
|
|
|
160
173
|
# Разделяем их кодом возврата: «не смогли разобрать» — это 2, а не молчаливый ноль.
|
|
161
174
|
if [ "$CODE" -ne 0 ] || [ -n "$E" ]; then
|
|
162
175
|
ERR="$ERR$F: разобрать не удалось${E:+ — }$E
|
|
176
|
+
"
|
|
177
|
+
fi
|
|
178
|
+
# КОМАНДЫ ХУКОВ — отдельной дорожкой: awk отдал их помеченными строками, файловую систему
|
|
179
|
+
# спрашивает оболочка.
|
|
180
|
+
#
|
|
181
|
+
# ГРАНИЦА НАМЕРЕННО УЗКАЯ. Красим только ОТНОСИТЕЛЬНЫЙ путь со слэшем: `.claude/hooks/x.sh`,
|
|
182
|
+
# `scripts/guard.sh`. Программа из PATH (`npx`, `prettier`, `bash -c …`) нам не видна — у неё
|
|
183
|
+
# нет пути, и «не нашли» означало бы обвинение по догадке. Абсолютный путь пропускаем: он
|
|
184
|
+
# относится к чужой машине, а не к репозиторию. Из двух ошибок здесь выбирается молчание:
|
|
185
|
+
# ложное обвинение выключает гейт целиком.
|
|
186
|
+
CMDS=$(printf '%s\n' "$RES" | grep '^@CMD@' || true)
|
|
187
|
+
RES=$(printf '%s\n' "$RES" | grep -v '^@CMD@' || true)
|
|
188
|
+
if [ -n "$CMDS" ]; then
|
|
189
|
+
MISS=$(printf '%s\n' "$CMDS" | while IFS= read -r L; do
|
|
190
|
+
[ -z "$L" ] && continue
|
|
191
|
+
LN=$(printf '%s' "$L" | cut -d@ -f3)
|
|
192
|
+
CMD=$(printf '%s' "$L" | cut -d@ -f4-)
|
|
193
|
+
# Переменная окружения, которой Claude Code называет корень проекта, — это и есть DIR.
|
|
194
|
+
CMD=$(printf '%s' "$CMD" | sed 's|\${CLAUDE_PROJECT_DIR}/*||g; s|\$CLAUDE_PROJECT_DIR/*||g')
|
|
195
|
+
# КАВЫЧКИ СНИМАЮТСЯ ПОСЛЕ ПЕРЕМЕННОЙ И ДО РАЗБОРА НА СЛОВА. Найдено замером 2026-09-15 по
|
|
196
|
+
# `kupzed/catatz`: у них `node "$CLAUDE_PROJECT_DIR/.claude/hooks/adapter.mjs"` — путь
|
|
197
|
+
# лежит ВНУТРИ кавычек вместе с переменной. Переменную мы снимали, кавычки оставались, и
|
|
198
|
+
# файл искался по имени с кавычками. Пять «пропавших» хуков, и все пять на месте (200).
|
|
199
|
+
# Письмо по такой находке было бы неправдой — а писать мы собирались именно по ним.
|
|
200
|
+
CMD=$(printf '%s' "$CMD" | tr -d '"'"'"'"')
|
|
201
|
+
# Первое слово — программа. Если это запускалка, файл стоит вторым.
|
|
202
|
+
P=$(printf '%s' "$CMD" | awk "{print \$1}")
|
|
203
|
+
case "$P" in
|
|
204
|
+
bash|sh|node|python|python3|ruby|perl|deno|bun) P=$(printf '%s' "$CMD" | awk "{print \$2}") ;;
|
|
205
|
+
esac
|
|
206
|
+
case "$P" in
|
|
207
|
+
# ПЕРЕМЕННАЯ, КОТОРУЮ МЫ НЕ РАСКРЫВАЕМ, — повод молчать. Замер 2026-09-15 по
|
|
208
|
+
# `Aurealibe/claude-config`: `${CLAUDE_PLUGIN_ROOT:-.}/.claude/hooks/session-start`.
|
|
209
|
+
# Значение задаётся снаружи, проверить путь на диске нельзя, и объявить его пропавшим
|
|
210
|
+
# значит обвинить по догадке. CLAUDE_PROJECT_DIR — исключение: он и есть корень, и
|
|
211
|
+
# снимается выше.
|
|
212
|
+
*'$'*) continue ;;
|
|
213
|
+
# ДОМАШНИЙ КАТАЛОГ — НЕ РЕПОЗИТОРИЙ. Найдено 2026-09-15 на `Dynokostya/just-works`:
|
|
214
|
+
# `bash ~/.claude/statusline-command.sh` — хук пользовательского уровня, он живёт у
|
|
215
|
+
# человека, а не в проекте. Проверить его на диске проекта нельзя.
|
|
216
|
+
'~'*) continue ;;
|
|
217
|
+
""|-*|/*) continue ;;
|
|
218
|
+
*/*) [ -e "$DIR/$P" ] || printf "%s:%s: команда хука указывает на «%s» — такого файла в проекте нет, хук не сработает никогда\n" "$F" "$LN" "$P" ;;
|
|
219
|
+
esac
|
|
220
|
+
done)
|
|
221
|
+
[ -n "$MISS" ] && OUT="$OUT$MISS
|
|
163
222
|
"
|
|
164
223
|
fi
|
|
165
224
|
[ -n "$RES" ] && OUT="$OUT$RES
|
|
@@ -178,6 +237,7 @@ fi
|
|
|
178
237
|
ALL=$(printf '%s' "$OUT" | grep -v '^$')
|
|
179
238
|
[ -z "$ALL" ] && exit 0
|
|
180
239
|
printf '%s\n' "$ALL"
|
|
181
|
-
echo " почини: сверь имя события с https://code.claude.com/docs/en/hooks
|
|
240
|
+
echo " почини: сверь имя события с https://code.claude.com/docs/en/hooks, убери matcher там, где его нет,"
|
|
241
|
+
echo " и верни на место файл, на который указывает команда, — либо убери сам хук."
|
|
182
242
|
echo " хук с неверным именем не вызывается и об этом не сообщается — защита существует только на бумаге."
|
|
183
243
|
exit 1
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
intent: хук агента правда срабатывает — имя события известно, matcher не игнорируется
|
|
2
|
-
intent_en: an agent hook actually fires — the event name is real
|
|
1
|
+
intent: хук агента правда срабатывает — имя события известно, matcher не игнорируется молча, файл команды существует
|
|
2
|
+
intent_en: an agent hook actually fires — the event name is real, the matcher is not silently ignored, and the command's file exists
|
|
3
3
|
|
|
4
4
|
# Только там, где агента настраивали. В проекте без `.claude/settings.json` проверять нечего,
|
|
5
5
|
# а запись, показанная не тому, стоит доверия всему каталогу.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// Образец: путь лежит внутри кавычек вместе с переменной, и файл существует.
|
|
@@ -77,3 +77,34 @@ deletes or disables the checkwash job disarms it in the same diff»* — это
|
|
|
77
77
|
коммитами. `red/` — код изменён на неверный, три утверждения заменены на `is not None`.
|
|
78
78
|
`green/` — код и тест выросли вместе: добавлена функция и тест к ней, ни одно утверждение не
|
|
79
79
|
убрано и не ослаблено.
|
|
80
|
+
|
|
81
|
+
## Известное ложное срабатывание: литерал `"\n"` читается как имя теста
|
|
82
|
+
|
|
83
|
+
**Цена записи, названная вслух.** `checkwash` принимает строковый литерал `"\n"` за ИМЯ
|
|
84
|
+
тестового юнита. Файл, где таких литералов два и больше, при ЛЮБОМ сдвиге строк получает
|
|
85
|
+
`TEST_DISABLED — test unit disappeared` уровня **high** — даже если тронут один комментарий и
|
|
86
|
+
ни одно утверждение не изменено.
|
|
87
|
+
|
|
88
|
+
Воспроизведение, шесть строк (проверено 2026-09-16 на 0.2.13 и 0.3.4 — в обеих одинаково):
|
|
89
|
+
|
|
90
|
+
```js
|
|
91
|
+
import test from "node:test";
|
|
92
|
+
test("единственный", (t) => {
|
|
93
|
+
const a = "x\ny".split("\n").slice(-1).join("\n");
|
|
94
|
+
const b = "p\nq".split("\n").slice(-1).join("\n");
|
|
95
|
+
t.assert.ok(a && b);
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Коммит «before» с этим файлом, коммит «after» с добавленной строкой комментария — и
|
|
100
|
+
`checkwash check HEAD~1..HEAD` даёт `verdict: block`, `unit: "\n"`.
|
|
101
|
+
|
|
102
|
+
`split("\n")` — обычнейшая конструкция в тестах, работающих с выводом команды. Мы наступили на
|
|
103
|
+
это на собственном `tool/selfcheck/smoke/verdict.test.mjs`: гейт покраснел на добавленном
|
|
104
|
+
комментарии. У себя обошли устранением настоящего повтора (хвост вывода вынесен в функцию
|
|
105
|
+
фикстуры), но **это обход, а не лечение**: чинить может только автор инструмента.
|
|
106
|
+
|
|
107
|
+
**Что это значит для того, кто ставит запись.** Приговор `TEST_DISABLED` на файле, где
|
|
108
|
+
утверждения не менялись, стоит перепроверить глазами: посмотрите, есть ли в нём литералы
|
|
109
|
+
`"\n"` и совпадает ли имя «исчезнувшего юнита» с настоящим именем теста. Имя вида `\n` — признак
|
|
110
|
+
этого дефекта, а не подгонки теста. Остальные двадцать детекторов инструмента он не затрагивает.
|
package/llms.txt
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
# AQK — Agent Quality Kit
|
|
2
2
|
|
|
3
|
-
> A
|
|
4
|
-
>
|
|
5
|
-
>
|
|
6
|
-
>
|
|
7
|
-
>
|
|
8
|
-
>
|
|
3
|
+
> A CLI that answers one question: can the checks this repository declares actually fail? A check
|
|
4
|
+
> that cannot fail looks exactly like a check that passes — `|| true`, `continue-on-error`, a
|
|
5
|
+
> linter aimed at an empty directory, a test with no assertion, a hook nobody installed. AQK plants
|
|
6
|
+
> a known defect into a COPY of the project and reports which declared guard noticed and which
|
|
7
|
+
> stayed silent. It distinguishes three outcomes and never collapses them into two: clean ·
|
|
8
|
+
> finding · COULD NOT CHECK. The third means the check itself failed; treating it as either of the
|
|
9
|
+
> others is how a repository ends up protected by checks that cannot go red. Every rule the project
|
|
10
|
+
> promises becomes a command with an exit code, so a machine holds the promise instead of somebody's
|
|
11
|
+
> attention. Levels AQK-0..3 measure equipment, not quality, and are computed by a run — never by a
|
|
12
|
+
> questionnaire and never by a model's opinion.
|
|
9
13
|
|
|
10
14
|
Vendor-neutral: works with any coding agent (Claude Code, Codex, Cursor, Gemini CLI, GitHub
|
|
11
15
|
Copilot, Windsurf, Aider, OpenCode) and with no AI at all. It calls no vendor API and needs no
|
|
@@ -50,6 +54,18 @@ Zero runtime dependencies. Node 18+ and an `sh` shell. MIT.
|
|
|
50
54
|
ground rules on top, at most five items by weight (red gates, defects the probe planted and no
|
|
51
55
|
check caught, the project's own checks to declare, rules Claude Code cannot see, catalog
|
|
52
56
|
entries to propose), and for every item the command that proves it done. Nothing is run
|
|
57
|
+
- Send the author one answer — the only payment the kit asks: `npx agent-quality-kit feedback`
|
|
58
|
+
prints a short report built from the last run and the probe (version, level, stack, what went
|
|
59
|
+
red, what it COULD NOT CHECK, which defect classes nothing here catches) with **no paths, no
|
|
60
|
+
code and no repository name**, plus a prefilled link. `feedback --send "your words"` posts it in
|
|
61
|
+
one command using the human's own `gh` account, as a comment in an open discussion. **Without
|
|
62
|
+
the flag nothing ever leaves the machine**, and a machine holds that claim, not this sentence.
|
|
63
|
+
An agent must show the text and ask before adding the flag
|
|
64
|
+
- Install it into Claude Code as a plugin, from the kit's own marketplace, with nobody's approval
|
|
65
|
+
to wait for: `/plugin marketplace add arsen-ask-lx/Agent_Quality_Kit` then
|
|
66
|
+
`/plugin install aqk@agent-quality-kit`. The session-start hook puts the repository state into
|
|
67
|
+
the agent's context before its first action; two skills cover "can these checks fail" and "what
|
|
68
|
+
to fix first". A thin wrapper over the same CLI — no logic of its own
|
|
53
69
|
- Check that the kit's own wiring is actually connected: `npx agent-quality-kit vitals` — are the
|
|
54
70
|
tools the declared gates need installed, is the hook present in `.git/hooks` (a line in the
|
|
55
71
|
config is an intention, not a guard), does the agent receive the state, is the version current.
|
|
@@ -76,7 +92,7 @@ Zero runtime dependencies. Node 18+ and an `sh` shell. MIT.
|
|
|
76
92
|
the files `init` writes are owned by root, so you cannot edit your own manifest. Debian-based
|
|
77
93
|
on purpose: the gates are `sh`, `grep`, `awk`, `find` — under alpine's busybox they behave
|
|
78
94
|
differently, and an image where the gates behave differently is worse than no image
|
|
79
|
-
- As a GitHub Action: `uses: arsen-ask-lx/Agent_Quality_Kit@v0.
|
|
95
|
+
- As a GitHub Action: `uses: arsen-ask-lx/Agent_Quality_Kit@v0.16.0` with `min: 1`
|
|
80
96
|
(https://github.com/marketplace/actions/agent-quality-kit-aqk)
|
|
81
97
|
|
|
82
98
|
## What makes it different
|
|
@@ -95,6 +111,9 @@ Zero runtime dependencies. Node 18+ and an `sh` shell. MIT.
|
|
|
95
111
|
- `.aqk.yml` — the manifest: entry, rules, docs, lang, gates as commands, covers (what a
|
|
96
112
|
declared gate already holds, so it is not reported as debt), samples, ratchets, lessons
|
|
97
113
|
- `.aqkignore` — paths the scanning checks must not read (brought-in code, vendored, generated)
|
|
114
|
+
- `.aqk/last-run.md` — the short report of the last run, with three marks and not two: `✔` clean,
|
|
115
|
+
`✘` a finding, `?` COULD NOT CHECK. Readers (the context block, the task list, the feedback
|
|
116
|
+
report) must never collapse the third into either of the others
|
|
98
117
|
|
|
99
118
|
## Documentation
|
|
100
119
|
|
package/package.json
CHANGED
|
@@ -257,7 +257,9 @@ async function readAdvice(man, probe) {
|
|
|
257
257
|
const facts = await detectFacts(man);
|
|
258
258
|
const catalog = await readCatalog();
|
|
259
259
|
const { todo } = catalogBuckets(catalog, facts, coversOf(man).covered);
|
|
260
|
-
|
|
260
|
+
// Чужие проверки — и тогда, когда гейты уже объявлены: отсев идёт по КОМАНДЕ внутри
|
|
261
|
+
// `proposeGates`, а не по факту «манифест не пуст» (замер 2026-09-16, см. doctor-catalog).
|
|
262
|
+
const adopt = proposeGates(await readAdoptFiles(CWD), declaredGates(man).map(([, cmd]) => cmd));
|
|
261
263
|
const blind = (probe?.classes || [])
|
|
262
264
|
.filter((b) => !facts.gateKeys.includes(b.slug))
|
|
263
265
|
.map((b) => ({ ...b, command: blindAdvice(catalog.find((r) => r.slug === b.slug), facts, {}).command }));
|
|
@@ -18,6 +18,34 @@ import { L } from "../i18n/index.mjs";
|
|
|
18
18
|
|
|
19
19
|
// Обязательный минимум проекта — прогоном, а не по памяти. До сих пор это было единственное
|
|
20
20
|
// место, где комплект просил верить на слово, что человек прочитал методичку и сверился.
|
|
21
|
+
// ЧТО У ПРОЕКТА УЖЕ ЕСТЬ — отдельной функцией, а не ветками внутри вывода: выбор причины из
|
|
22
|
+
// трёх плюс перебор плюс два условия давали вложенность 6 при пределе 5, и наш же гейт это
|
|
23
|
+
// поймал. Печать — единственное, что здесь происходит; решение принято в `adopt.mjs`.
|
|
24
|
+
function weakSay(weak) {
|
|
25
|
+
return weak.kind === "off" ? L.doctor.weakOff(weak.text)
|
|
26
|
+
: weak.kind === "zero" ? L.doctor.weakZero()
|
|
27
|
+
: L.doctor.weakStub(weak.text);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function printAdopted(found) {
|
|
31
|
+
if (!found.length) return;
|
|
32
|
+
console.log(`\n ${c.bold(L.doctor.haveAlready(found.length))}`);
|
|
33
|
+
for (const g of found) {
|
|
34
|
+
// ВЫКЛЮЧЕННАЯ ПРОВЕРКА — ЭТО НАХОДКА, а не «посмотреть не смогли»: мы прочитали тело
|
|
35
|
+
// скрипта и знаем ответ. Поэтому крест, а не вопрос, и причина названа дословно — человек
|
|
36
|
+
// не поверит обвинению без строки, которую может найти у себя глазами.
|
|
37
|
+
console.log(` ${g.weak ? c.red("✘") : c.green("✔")} ${g.name.padEnd(12)} ${c.dim(`${g.cmd} ← ${g.source}`)}`);
|
|
38
|
+
if (g.weak) console.log(` ${c.red(weakSay(g.weak))}`);
|
|
39
|
+
}
|
|
40
|
+
// «Впишите в манифест» — ТОЛЬКО про рабочие. Строкой выше сказано, что слабую объявлять
|
|
41
|
+
// нельзя; предложить её тут же — это совет, противоречащий собственному предостережению,
|
|
42
|
+
// и человек послушает тот, что ближе к строке с командой.
|
|
43
|
+
const weak = found.filter((g) => g.weak).length;
|
|
44
|
+
const ok = found.filter((g) => !g.weak);
|
|
45
|
+
if (weak) console.log(c.dim(` ${L.doctor.weakHow(weak)}`));
|
|
46
|
+
if (ok.length) console.log(c.dim(` ${L.doctor.haveAlreadyHow(ok.map((g) => `${g.name}: "${g.cmd}"`).join(" "))}`));
|
|
47
|
+
}
|
|
48
|
+
|
|
21
49
|
async function reportBaseline(man, facts) {
|
|
22
50
|
const { readdir, readFile } = await import("node:fs/promises");
|
|
23
51
|
let files = [];
|
|
@@ -77,16 +105,13 @@ async function reportCatalog(man, facts, probe = null, verbose = true) {
|
|
|
77
105
|
// только СВОИ записи, а чужие проверки не читали вовсе. С точки зрения владельца это
|
|
78
106
|
// неправда, и первое, что он видел, было обвинением. Предлагаем, а не вписываем: гейт в
|
|
79
107
|
// чужом манифесте без спроса — наше решение в чужом файле.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
console.log(c.dim(` ${L.doctor.haveAlreadyHow(found.map((g) => `${g.name}: "${g.cmd}"`).join(" "))}`));
|
|
88
|
-
}
|
|
89
|
-
}
|
|
108
|
+
//
|
|
109
|
+
// ПОКАЗЫВАЕТСЯ И ТОГДА, КОГДА ГЕЙТЫ ОБЪЯВЛЕНЫ. Прежде здесь стояло `!declaredGates(man).length`:
|
|
110
|
+
// чужие проверки читались только у того, у кого гейтов нет вовсе. Замер 2026-09-16 по двенадцати
|
|
111
|
+
// репозиториям: у шести зрелых блок исчезал целиком, стоило объявить ОДИН гейт, и итог говорил
|
|
112
|
+
// «держит машина 0» проекту с настоящим pre-commit и Makefile. Чем больше человек настроил, тем
|
|
113
|
+
// меньше мы о нём знали. Повтора не будет: уже объявленную команду `proposeGates` отсеивает сам.
|
|
114
|
+
printAdopted(proposeGates(await readAdoptFiles(CWD), declaredGates(man).map(([, cmd]) => cmd)));
|
|
90
115
|
|
|
91
116
|
// ЧТО ВАШИ ПРОВЕРКИ ПРОПУСТИЛИ. Проба знала имена непойманных классов и писала в отметку одно
|
|
92
117
|
// число; человек в `doctor` не видел ничего. Это самое конкретное, что мы знаем о проекте, —
|