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.
- package/README.md +88 -18
- package/README.ru.md +91 -19
- 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 +39 -41
- package/tool/commands/doctor-catalog.mjs +35 -10
- package/tool/commands/doctor.mjs +18 -26
- package/tool/commands/feedback.mjs +231 -0
- package/tool/commands/gates.mjs +12 -6
- package/tool/commands/project.mjs +11 -13
- package/tool/commands/prompt.mjs +2 -1
- package/tool/commands/report.mjs +19 -3
- package/tool/commands/vitals.mjs +9 -3
- package/tool/i18n/en-docs.mjs +19 -2
- package/tool/i18n/en-gates.mjs +35 -0
- package/tool/i18n/en.mjs +29 -2
- package/tool/i18n/ru-docs.mjs +18 -2
- package/tool/i18n/ru-gates.mjs +36 -0
- package/tool/i18n/ru.mjs +27 -2
- package/tool/lib/adopt.mjs +58 -4
- package/tool/lib/ask.mjs +118 -0
- package/tool/lib/brief.mjs +17 -38
- package/tool/lib/core.mjs +49 -12
- package/tool/lib/execution.mjs +32 -1
- package/tool/lib/gate-worker.mjs +4 -1
- package/tool/lib/manifest.mjs +39 -13
- package/tool/lib/prove.mjs +3 -3
- package/tool/lib/run.mjs +142 -10
- package/tool/program.mjs +6 -0
- package/tool/selfcheck/smoke/_fixture.mjs +13 -1
- package/tool/selfcheck/smoke/fail-closed.test.mjs +96 -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-ask.mjs +85 -0
- package/tool/selfcheck/units-brief.mjs +3 -13
- package/tool/selfcheck/units-context.mjs +2 -1
- package/tool/selfcheck/units-execution.mjs +37 -1
- package/tool/selfcheck/units-feedback.mjs +137 -0
- 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
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# Эксплуатационные гейты из живого AI-проекта: что переносить, а что не притворять универсальным
|
|
2
|
+
|
|
3
|
+
> Проверено по коду `audit_project` 15.09.2026, ссылки на первоисточники сверены 16.09.2026. Это актуальная карта практики, а не ручной
|
|
4
|
+
> снимок production-метрик. Живые значения остаются в Prometheus, логах и отчётах прогонов.
|
|
5
|
+
>
|
|
6
|
+
> Главная граница: здесь описаны **намерения и устройство арбитров**. Реализация конкретного
|
|
7
|
+
> Django/Celery/Compose-проекта остаётся в его репозитории и объявляется в `.aqk.yml` командой,
|
|
8
|
+
> а не копируется в AQK.
|
|
9
|
+
|
|
10
|
+
## Один вывод
|
|
11
|
+
|
|
12
|
+
Зелёная фича ещё не означает зелёную систему. Query budget одной ручки, memory budget одной
|
|
13
|
+
задачи и тест одного worker не отвечают на вопрос, выдержит ли одновременно работающий флот
|
|
14
|
+
общую RAM, PostgreSQL, Redis, диск и очередь.
|
|
15
|
+
|
|
16
|
+
Поэтому бюджетов всегда два:
|
|
17
|
+
|
|
18
|
+
1. **локальный** — SQL, время, память и корректность одного endpoint/task;
|
|
19
|
+
2. **системный** — p95, error rate, суммарный working set, очереди и OOM под смешанной нагрузкой.
|
|
20
|
+
|
|
21
|
+
## Карта по факту
|
|
22
|
+
|
|
23
|
+
| Область | Что действительно держит машина | Честная граница |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| N+1 | 21 тестовый файл и 63 вызова `assertNumQueries` / `django_assert_max_num_queries`; полный pytest блокирует рост известных бюджетов | нет требования, что **каждая новая** list/paginated-ручка получила budget-тест |
|
|
26
|
+
| DirectPG N+1 | отдельный тест проверяет не только число запросов, но и отсутствие линейного роста на увеличенном наборе | это проектный арбитр, обычный Django-счётчик его не заменяет |
|
|
27
|
+
| OOM после факта | `ContainerOOM` на росте `container_oom_events_total`; выражение имеет firing и quiet сценарии `promtool` | cAdvisor в основном описывает работающие контейнеры; история exited/restart требует отдельного источника |
|
|
28
|
+
| Лимиты observability | тест разбирает Compose и держит сумму лимитов tier под явным капом | это кап одного tier, не бюджет всего production-флота и не резерв RAM хоста |
|
|
29
|
+
| Celery topology | объявленные/используемые очереди сравниваются с `worker -Q` в production Compose | имена и структура конфигурации проектные |
|
|
30
|
+
| Celery redelivery | `visibility_timeout` сравнивается с максимальным hard time limit и запасом | длинный timeout ухудшает скорость возврата работы после принудительной смерти worker |
|
|
31
|
+
| Публичные метрики | publisher обязан попасть в реестр; быстрый pre-commit ловит забытое имя, pytest проверяет глубокую проводку | наличие имени не доказывает, что production реально его собирает |
|
|
32
|
+
| Prometheus rules | синтаксис и выбранная семантика правил проверяются `promtool`; после deploy сверяется набор реально загруженных правил | не каждое правило уже имеет отдельные firing/quiet samples |
|
|
33
|
+
| Скрипты-гейты | `check-gate-wiring.py` требует автоматический запуск либо честную запись в реестре ручных инструментов | «запускается» не означает «полезен»; это другой вопрос |
|
|
34
|
+
| Ошибки и trace | GlitchTip, Sentry SDK, Loki/Alloy и `request_id` связывают исключение с окружением запроса | доступность самой цепочки также нуждается в heartbeat/alert |
|
|
35
|
+
| Backup | расписание, ротация, метрика успеха, `BackupStale` и тесты скрипта | копия на том же сервере не переживёт потерю сервера; restore надо репетировать отдельно |
|
|
36
|
+
|
|
37
|
+
## Что ещё только видно, но не закрыто
|
|
38
|
+
|
|
39
|
+
Наличие графика или Telegram-alert не равно предотвращению.
|
|
40
|
+
|
|
41
|
+
### Память
|
|
42
|
+
|
|
43
|
+
- нет ранней полосы «контейнер долго держится выше доли своего лимита»;
|
|
44
|
+
- нет отношения `sum(container working_set) / host RAM` для всего флота;
|
|
45
|
+
- нет host-level RAM/swap/PSI прибора уровня node exporter;
|
|
46
|
+
- нет долговечной истории OOM/restart для уже остановившихся контейнеров;
|
|
47
|
+
- нет memory budgets для всех тяжёлых pytest-сценариев;
|
|
48
|
+
- сумма отдельных потолков не является резервированием общей памяти.
|
|
49
|
+
|
|
50
|
+
Последний пункт особенно важен. Docker ограничивает отдельный сервис, но несколько сервисов
|
|
51
|
+
могут одновременно приблизиться к своим пределам и исчерпать хост. Поэтому static Compose gate
|
|
52
|
+
и runtime fleet gate решают разные задачи; один нельзя выдавать за другой.
|
|
53
|
+
|
|
54
|
+
### PostgreSQL и Redis
|
|
55
|
+
|
|
56
|
+
Экспортёры есть, но нужны отдельные решения по сигналам:
|
|
57
|
+
|
|
58
|
+
- насыщение соединений и ожидание PgBouncer;
|
|
59
|
+
- длинные транзакции, deadlocks, temp bytes и блокировки;
|
|
60
|
+
- память Redis broker при `noeviction`;
|
|
61
|
+
- rejected writes, а не только evictions;
|
|
62
|
+
- возраст старейшей работы и ожидаемое время разбора очереди, а не одна глубина.
|
|
63
|
+
|
|
64
|
+
### Нагрузка
|
|
65
|
+
|
|
66
|
+
- Python load harness измеряет p50/p95/p99 и RPS, но плохие числа сами не дают красный код;
|
|
67
|
+
- k6 thresholds существуют лишь у отдельных ручных сценариев;
|
|
68
|
+
- нет обязательного смешанного прогона HTTP + Celery + отчёты + документы + интеграции;
|
|
69
|
+
- нет nightly/pre-release запуска;
|
|
70
|
+
- нет регулярного soak для утечек, high-watermark процессов и накопления соединений.
|
|
71
|
+
|
|
72
|
+
## Как строится честный OOM-контур
|
|
73
|
+
|
|
74
|
+
Одного правила `any OOM` мало. Нужны пять независимых слоёв.
|
|
75
|
+
|
|
76
|
+
### 1. Статические лимиты сервисов
|
|
77
|
+
|
|
78
|
+
Production Compose задаёт `deploy.resources.limits.memory` или эквивалент. Проверка разбирает
|
|
79
|
+
эффективную конфигурацию, а не ищет слово `memory` grep-ом. Для каждого обязательного сервиса
|
|
80
|
+
должно быть ясно, почему лимит такой и каким замером он получен.
|
|
81
|
+
|
|
82
|
+
### 2. Конфигурационный бюджет
|
|
83
|
+
|
|
84
|
+
Сумма лимитов выбранного tier не растёт молча. Это храповик конфигурации, а не обещание, что RAM
|
|
85
|
+
зарезервирована. Поднять кап можно только вместе с измерением хоста и записанной причиной.
|
|
86
|
+
|
|
87
|
+
### 3. Runtime budget флота
|
|
88
|
+
|
|
89
|
+
Под смешанной нагрузкой считается:
|
|
90
|
+
|
|
91
|
+
```text
|
|
92
|
+
max_over_time((sum(container_memory_working_set_bytes))[15m:1m]) / host_memory_bytes
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Точный PromQL зависит от labels и источника host memory. Порог нельзя копировать вслепую; для
|
|
96
|
+
малого сервера разумная стартовая красная полоса должна оставлять запас ядру, Docker, page cache
|
|
97
|
+
и пикам PostgreSQL. В исследованном проекте рабочей гипотезой остаётся 70%, но она ещё не стала
|
|
98
|
+
доказанным гейтом.
|
|
99
|
+
|
|
100
|
+
### 4. Раннее предупреждение контейнера
|
|
101
|
+
|
|
102
|
+
Устойчивое превышение доли лимита предупреждает до OOM. Рабочая гипотеза проекта — около 85%,
|
|
103
|
+
но её надо откалибровать по пикам и шуму. Мгновенный spike и десять минут давления — разные
|
|
104
|
+
события.
|
|
105
|
+
|
|
106
|
+
### 5. OOM и restart после факта
|
|
107
|
+
|
|
108
|
+
Рост `container_oom_events_total` — critical. Но исчезновение ряда остановленного контейнера не
|
|
109
|
+
должно превратить аварию в тишину: Docker events/inspect или другой collector хранит exit reason,
|
|
110
|
+
restart count и timestamp независимо от текущей жизни контейнера.
|
|
111
|
+
|
|
112
|
+
### Доказательство правила
|
|
113
|
+
|
|
114
|
+
Для alert нужны минимум два `promtool`-сценария:
|
|
115
|
+
|
|
116
|
+
- counter вырос внутри окна → alert firing с ожидаемыми labels/annotations;
|
|
117
|
+
- counter не растёт → alert отсутствует.
|
|
118
|
+
|
|
119
|
+
Синтаксически корректное выражение `vector(0)` обязано сделать первый сценарий красным. Если вся
|
|
120
|
+
батарея остаётся зелёной, семантика OOM не проверяется.
|
|
121
|
+
|
|
122
|
+
## Как строится N+1-гейт
|
|
123
|
+
|
|
124
|
+
Один статический поиск `select_related` ничего не доказывает. Арбитр должен наблюдать полный
|
|
125
|
+
операционный путь.
|
|
126
|
+
|
|
127
|
+
1. Создать больше одного связанного объекта; для list endpoint обычно нужны десятки строк.
|
|
128
|
+
2. Выполнить настоящий HTTP-запрос или функцию задачи.
|
|
129
|
+
3. Зафиксировать постоянный максимум SQL, а не `текущее число + 1`.
|
|
130
|
+
4. Увеличить объём fixture и проверить, что число запросов не растёт линейно.
|
|
131
|
+
5. Запускать тест в общей блокирующей батарее.
|
|
132
|
+
|
|
133
|
+
Django официально предоставляет `assertNumQueries`. Для pytest допустима обёртка с верхним
|
|
134
|
+
пределом, если она печатает выполненные запросы при превышении. Direct SQL, Trino и внешние базы
|
|
135
|
+
нуждаются в своём счётчике — ORM-инструмент не видит их по построению.
|
|
136
|
+
|
|
137
|
+
Честная граница: наличие двадцати одного budget-теста не доказывает покрытие двадцать второй ручки.
|
|
138
|
+
Отдельный coverage-gate возможен только тогда, когда проект умеет машинно определить множество
|
|
139
|
+
ручек, для которых budget обязателен. Проверять слово в имени теста недостаточно.
|
|
140
|
+
|
|
141
|
+
## Два инварианта Celery
|
|
142
|
+
|
|
143
|
+
### Очередь имеет потребителя
|
|
144
|
+
|
|
145
|
+
Сопоставляются три факта:
|
|
146
|
+
|
|
147
|
+
- очереди, объявленные в настройках;
|
|
148
|
+
- очереди, используемые routes, task decorators и `apply_async`;
|
|
149
|
+
- очереди, которые реально слушают production workers.
|
|
150
|
+
|
|
151
|
+
Новая очередь без consumer красит CI. Документ или диаграмма не являются источником истины.
|
|
152
|
+
|
|
153
|
+
### Доставка не опережает hard timeout
|
|
154
|
+
|
|
155
|
+
Redis visibility timeout определяет, когда неacknowledged message возвращается в очередь. Если
|
|
156
|
+
задача законно выполняется дольше, второй worker может получить её повторно. Проектный гейт
|
|
157
|
+
сравнивает отношения настроек и fail-closed обрабатывает неразрешимые значения.
|
|
158
|
+
|
|
159
|
+
Это не универсальная формула для всех Celery-систем: ETA/retry и принудительное завершение
|
|
160
|
+
создают обратную цену слишком большого timeout. Инвариант должен учитывать модель задач проекта.
|
|
161
|
+
|
|
162
|
+
## Метрика и алерт — это цепочка, а не файл
|
|
163
|
+
|
|
164
|
+
Полный путь выглядит так:
|
|
165
|
+
|
|
166
|
+
```text
|
|
167
|
+
код публикует имя
|
|
168
|
+
→ exporter его принимает
|
|
169
|
+
→ Prometheus scrape видит ряд
|
|
170
|
+
→ правило вычисляется правильно
|
|
171
|
+
→ Alertmanager маршрутизирует
|
|
172
|
+
→ человек получает сообщение
|
|
173
|
+
→ runbook говорит, что делать
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Для каждого шва нужен свой арбитр. Unit-test publisher не доказывает scrape. `promtool check`
|
|
177
|
+
не доказывает смысл выражения. Тест выражения не доказывает, что production загрузил новый файл.
|
|
178
|
+
После deploy полезна read-only сверка repo rule names с Prometheus API.
|
|
179
|
+
|
|
180
|
+
## Нагрузочный гейт и soak
|
|
181
|
+
|
|
182
|
+
Короткий mixed scenario должен иметь код возврата и одновременно держать:
|
|
183
|
+
|
|
184
|
+
- error rate;
|
|
185
|
+
- p95 ключевых пользовательских путей;
|
|
186
|
+
- throughput или время разбора очередей;
|
|
187
|
+
- суммарный working set флота;
|
|
188
|
+
- отсутствие OOM/restart;
|
|
189
|
+
- при необходимости соединения PostgreSQL и rejected writes Redis.
|
|
190
|
+
|
|
191
|
+
Короткий прогон не ловит медленную утечку. Soak запускается отдельно и реже, с теми же
|
|
192
|
+
инвариантами, но с проверкой тренда памяти, соединений и очередей во времени. Смешивать soak с
|
|
193
|
+
каждым pre-push нельзя: дорогой гейт начнут обходить.
|
|
194
|
+
|
|
195
|
+
## Где здесь агент
|
|
196
|
+
|
|
197
|
+
Агент не должен решать, случилась ли аномалия. Сначала детерминированный detector:
|
|
198
|
+
|
|
199
|
+
```text
|
|
200
|
+
Prometheus rule или baseline script
|
|
201
|
+
→ фильтрация и группировка события
|
|
202
|
+
→ краткоживущий read-only агент
|
|
203
|
+
→ гипотеза + evidence + confidence + связанный deploy
|
|
204
|
+
→ issue / intent / merge request
|
|
205
|
+
→ решение человека
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
По расписанию работает baseline script. Модель вызывается только по нарушению полосы. На первом
|
|
209
|
+
этапе ей достаточно read-only доступа к Prometheus, Loki, error tracker и истории deploy; права
|
|
210
|
+
на production DB, Docker socket, merge и произвольный runbook не нужны.
|
|
211
|
+
|
|
212
|
+
## Как подключать это через AQK без дублей
|
|
213
|
+
|
|
214
|
+
AQK-манифест объявляет **существующую команду проекта**:
|
|
215
|
+
|
|
216
|
+
```yaml
|
|
217
|
+
gates:
|
|
218
|
+
project-verify: "bash scripts/verify.sh"
|
|
219
|
+
prometheus-rules: "docker run --rm ... promtool test rules alerts_test.yml"
|
|
220
|
+
groups:
|
|
221
|
+
code: [project-verify]
|
|
222
|
+
observability: [prometheus-rules]
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Внутренние шаги `project-verify` не перечисляются второй раз в манифесте. При падении агрегатор
|
|
226
|
+
обязан назвать конкретный шаг. Отдельным gate становится только команда, которую агрегатор
|
|
227
|
+
честно не запускает.
|
|
228
|
+
|
|
229
|
+
Один gate AQK получает не больше пяти минут. Если полный verifier проекта дольше, в манифесте
|
|
230
|
+
нужен его быстрый блокирующий режим, а `--full` — отдельный прямой или CI-прогон. Иначе AQK будет
|
|
231
|
+
честно возвращать «не смог проверить: timeout», но интеграция не станет рабочим локальным gate.
|
|
232
|
+
|
|
233
|
+
Не надо:
|
|
234
|
+
|
|
235
|
+
- копировать project-specific Python checks в `kit/gates`;
|
|
236
|
+
- объявлять grep по словам `memory` или `assertNumQueries` доказательством;
|
|
237
|
+
- указывать похожий каталог как `samples`, если структура AQK red/green там отсутствует;
|
|
238
|
+
- повышать уровень AQK заглушками;
|
|
239
|
+
- запускать `aqk init --force` поверх зрелого корпуса правил без review.
|
|
240
|
+
|
|
241
|
+
Новая запись каталога AQK появляется после трёх доказательств: устойчиво опознаваемая конструкция,
|
|
242
|
+
red/green samples и переносимый рецепт с понятной границей ложных срабатываний. До этого это
|
|
243
|
+
методика или собственный gate проекта — и это честное состояние.
|
|
244
|
+
|
|
245
|
+
## Порядок для нового проекта
|
|
246
|
+
|
|
247
|
+
1. До кода записать измеримые AC: SQL, p95, error rate, memory, очередь, идемпотентность.
|
|
248
|
+
2. Быструю детерминированную статику поставить в pre-commit.
|
|
249
|
+
3. Query и task budgets поставить в обычный CI.
|
|
250
|
+
4. Compose limits и alert rules проверять нативными валидаторами.
|
|
251
|
+
5. Mixed load запускать nightly и перед релизом; soak — отдельно, например еженедельно.
|
|
252
|
+
6. Любой OOM сделать красным исходом и сохранить причину после смерти контейнера.
|
|
253
|
+
7. Только после стабилизации сигналов подключать read-only агента к нескольким critical alerts.
|
|
254
|
+
|
|
255
|
+
## Официальные источники
|
|
256
|
+
|
|
257
|
+
- [Django 5.2: `assertNumQueries`](https://docs.djangoproject.com/en/5.2/topics/testing/tools/#django.test.TransactionTestCase.assertNumQueries)
|
|
258
|
+
- [Prometheus: unit testing rules через `promtool`](https://prometheus.io/docs/prometheus/latest/configuration/unit_testing_rules/)
|
|
259
|
+
- [cAdvisor: Prometheus metrics, включая `container_oom_events_total`](https://github.com/google/cadvisor/blob/master/docs/storage/prometheus.md)
|
|
260
|
+
- [Docker Compose: resource limits](https://docs.docker.com/reference/compose-file/deploy/#resources)
|
|
261
|
+
- Docker про то, что сумма отдельных limits не гарантирует запас хоста — дословно:
|
|
262
|
+
«Using `--reserve-memory` and `--limit-memory` does not guarantee that Docker will not use more
|
|
263
|
+
memory on your host than you want», страница
|
|
264
|
+
[`docker service create`](https://docs.docker.com/reference/cli/docker/service/create/).
|
|
265
|
+
Якорь раздела не указан намеренно: страница собирается на стороне браузера, и проверить
|
|
266
|
+
существование якоря запросом нельзя — а ссылка на несуществующий якорь молча ведёт наверх.
|
|
267
|
+
- [Celery Redis: visibility timeout и redelivery](https://docs.celeryq.dev/en/main/getting-started/backends-and-brokers/redis.html#visibility-timeout)
|
|
268
|
+
|
|
269
|
+
## Финальный принцип
|
|
270
|
+
|
|
271
|
+
> Сначала детерминированный сигнал и ограничение. Потом агент. Никогда наоборот.
|
|
272
|
+
|
|
273
|
+
Повторившаяся ошибка должна оставлять после себя не ещё один абзац, а арбитр с кодом возврата.
|
|
274
|
+
Если переносимого арбитра пока нет, граница называется вслух — текст не получает фальшивое имя
|
|
275
|
+
«гейт».
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Общий шов для проверок, судящих ПУТЬ, взятый из текста.
|
|
2
|
+
#
|
|
3
|
+
# ЗАЧЕМ ЭТОТ ФАЙЛ. Замеры 14-15 сентября 2026 по 290 чужим репозиториям нашли у нас двенадцать
|
|
4
|
+
# ложных срабатываний. Разбор показал, что это не двенадцать ошибок, а ОДНА, повторённая
|
|
5
|
+
# двенадцатью способами: мы судили строку как путь на диске, не зная, чем она разрешается.
|
|
6
|
+
# Разрешало её то, чего у нас нет, — адрес на github.com, переменная окружения, генератор сайта,
|
|
7
|
+
# соседний репозиторий вики, проза вокруг.
|
|
8
|
+
#
|
|
9
|
+
# Чинить такое по одному значит растить в каждом гейте свой список исключений; через три правки
|
|
10
|
+
# списки разойдутся, и дважды пойманная беда вернётся через тот гейт, куда её не дописали.
|
|
11
|
+
# Поэтому решение одно и здесь.
|
|
12
|
+
#
|
|
13
|
+
# ТРИ ИСХОДА, А НЕ ДВА — тот же договор, что у всего комплекта:
|
|
14
|
+
# disk путь ведёт на файл в этом каталоге, судить можно
|
|
15
|
+
# unresolved разрешается чем-то, чего у нас нет: МОЛЧАТЬ НЕЛЬЗЯ, но и обвинять нельзя
|
|
16
|
+
# skip не путь вовсе (сеть, почта, якорь) — говорить не о чем
|
|
17
|
+
|
|
18
|
+
# Якорь и строка запроса — не часть имени файла. `assets/x.gif?raw=1` лежит на диске как
|
|
19
|
+
# `assets/x.gif`, `tutorial/#install` — это адрес страницы. Чистится ЗДЕСЬ, а не у зовущего:
|
|
20
|
+
# иначе каждый гейт будет чистить по-своему, и один забудет.
|
|
21
|
+
clean_target() {
|
|
22
|
+
T="${1%%#*}"
|
|
23
|
+
printf '%s' "${T%%\?*}"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
# Возвращает слово исхода, а для unresolved — ещё и причину через двоеточие.
|
|
27
|
+
classify_target() {
|
|
28
|
+
T="$1"
|
|
29
|
+
case "$T" in
|
|
30
|
+
""|\#*) echo "skip"; return ;;
|
|
31
|
+
# ЛЮБАЯ СХЕМА, В ЛЮБОМ РЕГИСТРЕ. Замер 2026-09-15: `file://Users/...` и
|
|
32
|
+
# `Https://conventionalcommits.org` с заглавной H объявлены битыми файлами. Раньше узнавались
|
|
33
|
+
# только `http://` и `https://` строчными — и это была догадка о том, как люди пишут.
|
|
34
|
+
*://*|mailto:*|MAILTO:*) echo "skip"; return ;;
|
|
35
|
+
# Адрес почты целью ссылки без «mailto:». Замер: три таких объявлены битыми файлами.
|
|
36
|
+
*@*.*) echo "skip"; return ;;
|
|
37
|
+
esac
|
|
38
|
+
case "$T" in
|
|
39
|
+
# Путь, уходящий выше корня: так сам GitHub предлагает ссылаться на выпуски и задачи —
|
|
40
|
+
# `../../releases` считается от адреса файла в вебе. Проверено: страница отдаёт 200.
|
|
41
|
+
../*) echo "unresolved:адрес на github.com" ;;
|
|
42
|
+
# Путь от корня САЙТА, а не репозитория.
|
|
43
|
+
/*) echo "unresolved:адрес на github.com" ;;
|
|
44
|
+
# Страница вики живёт в отдельном репозитории `<репо>.wiki`, которого мы не скачиваем.
|
|
45
|
+
wiki/*) echo "unresolved:вики в отдельном репозитории" ;;
|
|
46
|
+
# Путь с косой чертой на конце и страница `.html` — это адрес опубликованного сайта:
|
|
47
|
+
# mkdocs, docusaurus, pkgdown собирают их при публикации. Проверено: 200 на их сайтах.
|
|
48
|
+
*/|*.html|*.htm) echo "unresolved:страницу собирает генератор сайта" ;;
|
|
49
|
+
# Нераскрытая переменная: значение задаётся снаружи.
|
|
50
|
+
*'$'*) echo "unresolved:путь зависит от переменной" ;;
|
|
51
|
+
*) echo "disk" ;;
|
|
52
|
+
esac
|
|
53
|
+
}
|
|
@@ -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
|
|