agent-quality-kit 0.7.0 → 0.9.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 +135 -9
- package/README.ru.md +167 -23
- package/kit/docs/ai/project-baseline.md +14 -0
- package/kit/docs/ready-made-rules.md +103 -0
- package/kit/gates/_skip.sh +61 -1
- package/kit/gates/ci-not-hijackable/README.md +56 -0
- package/kit/gates/ci-not-hijackable/check.sh +73 -0
- package/kit/gates/ci-not-hijackable/gate.yml +19 -0
- package/kit/gates/ci-not-hijackable/green/.github/workflows/triage.yml +19 -0
- package/kit/gates/ci-not-hijackable/red/.github/workflows/triage.yml +18 -0
- package/kit/gates/color-from-token/check.sh +10 -2
- package/kit/gates/color-from-token/green/Button.tsx +2 -0
- package/kit/gates/complexity-limit/check.sh +6 -7
- package/kit/gates/duplicate-code/check.sh +5 -1
- package/kit/gates/entry-links-exist/check.sh +4 -1
- package/kit/gates/entry-links-exist/green/AGENTS.md +2 -0
- package/kit/gates/file-size-limit/check.sh +1 -1
- package/kit/gates/lesson-has-outcome/check.sh +5 -1
- package/kit/gates/mcp-server-resolves/README.md +62 -0
- package/kit/gates/mcp-server-resolves/check.sh +110 -0
- package/kit/gates/mcp-server-resolves/gate.yml +18 -0
- package/kit/gates/mcp-server-resolves/green/.mcp.json +20 -0
- package/kit/gates/mcp-server-resolves/red/.mcp.json +16 -0
- package/kit/gates/secrets-not-in-code/check.sh +16 -3
- package/kit/gates/secrets-not-in-code/green/testdata/certificate/key.pem +3 -0
- package/kit/gates/todo-without-task/check.sh +1 -1
- package/kit/gates/todo-without-task/green/app.py +1 -0
- package/llms.txt +38 -2
- package/package.json +2 -3
- package/tool/commands/context.mjs +264 -0
- package/tool/commands/doctor.mjs +104 -28
- package/tool/commands/learn.mjs +159 -0
- package/tool/commands/project.mjs +19 -2
- package/tool/commands/prove.mjs +1 -0
- package/tool/commands/report.mjs +33 -1
- package/tool/commands/vitals.mjs +159 -0
- package/tool/i18n/en-docs.mjs +125 -1
- package/tool/i18n/en.mjs +39 -36
- package/tool/i18n/index.mjs +36 -3
- package/tool/i18n/ru-docs.mjs +127 -1
- package/tool/i18n/ru.mjs +39 -36
- package/tool/lib/banner.mjs +59 -0
- package/tool/lib/brief.mjs +192 -0
- package/tool/lib/core.mjs +32 -1
- package/tool/lib/evidence.mjs +124 -0
- package/tool/lib/manifest.mjs +173 -15
- package/tool/lib/prove.mjs +24 -2
- package/tool/lib/repo.mjs +31 -1
- package/tool/lib/scope.mjs +10 -1
- package/tool/lib/templates.mjs +1 -0
- package/tool/program.mjs +45 -23
- package/tool/selfcheck/smoke.sh +592 -3
- package/tool/selfcheck/units-banner.mjs +65 -0
- package/tool/selfcheck/units-brief.mjs +97 -0
- package/tool/selfcheck/units-context.mjs +188 -0
- package/tool/selfcheck/units-evidence.mjs +83 -0
- package/tool/selfcheck/units-learn.mjs +88 -0
- package/tool/selfcheck/units-level.mjs +211 -3
- package/tool/selfcheck/units-repo.mjs +134 -0
- package/tool/selfcheck/units-vitals.mjs +62 -0
- package/tool/selfcheck/units.mjs +4 -75
package/kit/gates/_skip.sh
CHANGED
|
@@ -72,8 +72,68 @@ include_code() {
|
|
|
72
72
|
# Сгенерированный файл не правят руками — предъявлять его размер или сложность человеку
|
|
73
73
|
# бессмысленно и вредно: он выключит проверку целиком.
|
|
74
74
|
# Опознаём по общепринятой шапке в первых пяти строках.
|
|
75
|
+
# Регистр кириллицы `grep -i` сворачивает НЕ ВЕЗДЕ: на linux сворачивает, в Git Bash под
|
|
76
|
+
# Windows — нет. Шапка `// СГЕНЕРИРОВАН` там не находилась никогда, и узнали мы об этом только
|
|
77
|
+
# 2026-09-08, когда нарочный набор впервые прогнали на windows-задании. Это не регрессия правки,
|
|
78
|
+
# а старая дыра, которую нечем было увидеть: на живых шести проектах таких шапок не было.
|
|
79
|
+
# Поэтому кириллица перечисляется явно, а не доверяется флагу: три написания, которые бывают
|
|
80
|
+
# на деле, — строчное в комментарии, с заглавной в начале фразы и капсом в баннере.
|
|
81
|
+
GENERATED_MARKERS='@generated|do not edit|autogenerated|auto-generated|generated by|сгенерирован|Сгенерирован|СГЕНЕРИРОВАН'
|
|
82
|
+
|
|
83
|
+
# Один файл: сгенерирован ли он. Осталось для тех, кто спрашивает про ОДИН файл
|
|
84
|
+
# (gate-not-weakened сверяет строку находки, а не список путей).
|
|
75
85
|
is_generated() {
|
|
76
|
-
head -5 "$1" 2>/dev/null | grep -qiE
|
|
86
|
+
head -5 "$1" 2>/dev/null | grep -qiE "$GENERATED_MARKERS"
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
# СПИСОК файлов на stdin (по пути в строке) → тот же список без сгенерированных.
|
|
90
|
+
#
|
|
91
|
+
# ЗАЧЕМ ОТДЕЛЬНО ОТ is_generated. Цикл `while read; do is_generated; done` запускает ДВА
|
|
92
|
+
# процесса на каждый файл — head и grep. Замерено 2026-09-08 на 748 файлах проекта uv:
|
|
93
|
+
# обход и отбор 12 мс, подсчёт строк одним wc 37 мс, а этот цикл — 3862 мс. Сто крат от
|
|
94
|
+
# всего остального, и ни микросекунды из них не потрачено на сравнение строк: платили за
|
|
95
|
+
# fork+exec. Медленным был не язык — медленным было количество запусков.
|
|
96
|
+
#
|
|
97
|
+
# ПОЧЕМУ ИМЕННО grep, А НЕ awk. Соблазн был собрать всё одним awk с `nextfile`. Отвергнуто:
|
|
98
|
+
# `nextfile` — расширение, а `tolower` на кириллице врёт в mawk и busybox-awk, и «сгенерирован»
|
|
99
|
+
# перестал бы находиться там, где сейчас находится. Здесь ТОТ ЖЕ grep с ТЕМИ ЖЕ флагами, что
|
|
100
|
+
# и в is_generated, — значит совпадение считается ровно так же, как раньше, и меняется только
|
|
101
|
+
# число запусков. Равенство вывода проверено на шести чужих проектах: 0 расхождений.
|
|
102
|
+
#
|
|
103
|
+
# `-m1` останавливает grep на первом совпадении в файле, `/dev/null` заставляет его печатать
|
|
104
|
+
# имя файла даже когда xargs передал ровно один путь.
|
|
105
|
+
drop_generated() {
|
|
106
|
+
LIST=$(mktemp) || { cat; return; }
|
|
107
|
+
GEN=$(mktemp) || { rm -f "$LIST"; cat; return; }
|
|
108
|
+
cat > "$LIST"
|
|
109
|
+
# Пути передаются через НОЛЬ-разделитель: xargs по умолчанию рвёт по пробелам, и файл
|
|
110
|
+
# «src/my component.tsx» уехал бы в grep двумя несуществующими путями. Поймано нарочным
|
|
111
|
+
# набором до выпуска: старый цикл `while read` пробелы держал, и потерять это было нельзя.
|
|
112
|
+
tr '\n' '\0' < "$LIST" \
|
|
113
|
+
| xargs -0 -r grep -niE -m1 -e "$GENERATED_MARKERS" /dev/null 2>/dev/null \
|
|
114
|
+
| awk -v list="$LIST" '
|
|
115
|
+
# Строка вывода grep — «путь:номер:текст», и разобрать её по первому двоеточию нельзя:
|
|
116
|
+
# в пути тоже бывает двоеточие, а в тексте находки — тем более. Поэтому путь не
|
|
117
|
+
# угадывается, а СВЕРЯЕТСЯ со списком, который мы сами и передали: идём по двоеточиям
|
|
118
|
+
# слева направо, пока префикс не совпадёт с известным путём. Двоеточие в имени файла
|
|
119
|
+
# редкость, но «редко» и «никогда» — разные вещи, а тихо оставленный сгенерированный
|
|
120
|
+
# файл даёт находки на чужом коде.
|
|
121
|
+
BEGIN { while ((getline l < list) > 0) known[l] = 1 }
|
|
122
|
+
{
|
|
123
|
+
rest = $0; prefix = ""
|
|
124
|
+
while ((i = index(rest, ":")) > 0) {
|
|
125
|
+
prefix = prefix substr(rest, 1, i - 1)
|
|
126
|
+
rest = substr(rest, i + 1)
|
|
127
|
+
if (prefix in known) {
|
|
128
|
+
if (match(rest, /^[0-9]+:/) && substr(rest, 1, RLENGTH - 1) + 0 <= 5) print prefix
|
|
129
|
+
break
|
|
130
|
+
}
|
|
131
|
+
prefix = prefix ":"
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
' | sort -u > "$GEN"
|
|
135
|
+
if [ -s "$GEN" ]; then grep -vxF -f "$GEN" "$LIST"; else cat "$LIST"; fi
|
|
136
|
+
rm -f "$LIST" "$GEN"
|
|
77
137
|
}
|
|
78
138
|
|
|
79
139
|
# Для grep: --exclude-dir на каждое имя. Образцы гейтов сюда не входят — см. own_samples_filter:
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Конвейер не отдаёт чужому коду свои права и секреты
|
|
2
|
+
|
|
3
|
+
**Намерение.** Конвейер выполняется с правами репозитория и с доступом к его секретам. Три
|
|
4
|
+
способа отдать их постороннему живут не в коде, а в двадцати строках yaml:
|
|
5
|
+
|
|
6
|
+
- **подвижная метка вместо SHA.** `uses: some/action@v1` — это указатель, который владелец
|
|
7
|
+
действия может перевести на другой код в любой момент, и он же перевёдется у всех, кто на
|
|
8
|
+
метку сослался;
|
|
9
|
+
- **`pull_request_target` вместе с выкачиванием ветки автора PR.** Триггер даёт секреты
|
|
10
|
+
основного репозитория, а код берётся у постороннего;
|
|
11
|
+
- **`permissions: write-all`** на весь рабочий поток вместо нужного права нужному заданию.
|
|
12
|
+
|
|
13
|
+
Ни одну из трёх обычная проверка кода не увидит: это не код, это настройка.
|
|
14
|
+
|
|
15
|
+
**Какой отказ это поймало.** Собственный. На момент заведения записи у комплекта было **13
|
|
16
|
+
находок высокой строгости и 5 средней**, среди них десять действий, закреплённых меткой вместо
|
|
17
|
+
SHA, `id-token: write` на уровне всего потока и пять вызовов `checkout`, оставляющих учётные
|
|
18
|
+
данные в `.git/config`.
|
|
19
|
+
|
|
20
|
+
**Почему порог именно такой.** Замер 2026-09-08 по шести настоящим репозиториям:
|
|
21
|
+
|
|
22
|
+
| проект | High | Medium |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| `express` | 0 | 0 |
|
|
25
|
+
| `flask` | 0 | 0 |
|
|
26
|
+
| `uv` | 0 | 0 |
|
|
27
|
+
| `httpx` | 4 | 4 |
|
|
28
|
+
| `gin` | 18 | 0 |
|
|
29
|
+
| `ripgrep` | 19 | 8 |
|
|
30
|
+
|
|
31
|
+
Половина держит ноль. Порог взят с тех, кто его держит, а не выдуман: он достижим, и это
|
|
32
|
+
доказано чужой практикой, а не нашим мнением.
|
|
33
|
+
|
|
34
|
+
**Готовый аналог есть, и мы его зовём.** [`zizmor`](https://github.com/zizmorcore/zizmor) (MIT,
|
|
35
|
+
6459 звёзд, статический разбор GitHub Actions). Его же гоняет `flask` отдельным заданием
|
|
36
|
+
конвейера. Своего разбора yaml мы не писали. Обёртка отвечает за порог (`--min-severity medium`)
|
|
37
|
+
и за то, чтобы «не проверено» не выдавалось за «чисто»: `--no-exit-codes` разводит «нашлись
|
|
38
|
+
находки» и «инструмент не отработал», а отсутствие итоговой строки в выводе даёт код 2.
|
|
39
|
+
|
|
40
|
+
**Образцы.** `red/` — рабочий поток с `pull_request_target`, выкачиванием ветки автора PR,
|
|
41
|
+
`permissions: write-all` и незакреплённым действием. `green/` — тот же поток на `pull_request`,
|
|
42
|
+
с правами только на чтение, действием по SHA и `persist-credentials: false`.
|
|
43
|
+
|
|
44
|
+
**Чего НЕ ловит.**
|
|
45
|
+
|
|
46
|
+
- **Только GitHub Actions.** GitLab CI, Jenkins, Buildkite не проверяются: zizmor их не читает.
|
|
47
|
+
- **Не выполняет конвейер.** Разбор статический: он видит опасный уклад, но не видит, что делает
|
|
48
|
+
скрипт внутри `run:`. Скачивание и выполнение чужого кода строкой `curl … | sh` внутри шага —
|
|
49
|
+
предмет другой проверки, и её у нас нет.
|
|
50
|
+
- **Не сторожит SHA после закрепления.** Закреплённое действие может оказаться заброшенным или
|
|
51
|
+
уязвимым; «закреплено» и «безопасно» — разные утверждения. Обновление закреплённых SHA — работа
|
|
52
|
+
для dependabot, а не для этой записи.
|
|
53
|
+
- **Точечное гашение с причиной остаётся возможным.** `# zizmor: ignore[правило] причина` снимает
|
|
54
|
+
находку, и это осознанный уклад: наш собственный `gate-not-weakened` требует, чтобы подавление
|
|
55
|
+
было точечным и с названной причиной, а не порогом на весь файл. Мы сами пользуемся этим дважды
|
|
56
|
+
и обе причины написали вслух.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
# Конфигурация конвейера не отдаёт чужому коду права и секреты.
|
|
3
|
+
#
|
|
4
|
+
# ЗАЧЕМ. Конвейер выполняется с правами репозитория и с доступом к его секретам. Три способа
|
|
5
|
+
# отдать их постороннему живут не в коде, а в двадцати строках yaml: триггер `pull_request_target`
|
|
6
|
+
# вместе с выкачиванием ветки автора PR; действие, закреплённое подвижной МЕТКОЙ, которую владелец
|
|
7
|
+
# действия может перевести на другой код; права `write-all`, выданные всему рабочему потоку.
|
|
8
|
+
# Ни одну из трёх обычная проверка кода не увидит: это не код, это настройка.
|
|
9
|
+
#
|
|
10
|
+
# Работу делает zizmor (MIT, 6459 звёзд, статический разбор GitHub Actions). Мы отвечаем за
|
|
11
|
+
# порог и за то, чтобы «не проверено» не выдавалось за «чисто».
|
|
12
|
+
#
|
|
13
|
+
# ПОЧЕМУ ПОРОГ ИМЕННО ТАКОЙ. Замер 2026-09-08 по шести настоящим репозиториям: express, flask и
|
|
14
|
+
# uv держат НОЛЬ находок высокой и средней строгости; gin (18), ripgrep (19) и httpx (4) — нет.
|
|
15
|
+
# Порог взят с тех, кто его держит, а не выдуман: он достижим, и это доказано чужой практикой.
|
|
16
|
+
# Мы сами на момент заведения записи были на неправильной стороне — 13 высоких и 5 средних.
|
|
17
|
+
DIR="${1:-.}"
|
|
18
|
+
WF="$DIR/.github/workflows"
|
|
19
|
+
|
|
20
|
+
# Нет конвейера — нечего проверять. Это не успех и не провал, это отсутствие предмета.
|
|
21
|
+
[ -d "$WF" ] || exit 0
|
|
22
|
+
|
|
23
|
+
if ! command -v zizmor >/dev/null 2>&1; then
|
|
24
|
+
echo "не найден zizmor — эта проверка делегирована ему"
|
|
25
|
+
echo " почини: pipx install zizmor (или uv tool install zizmor)"
|
|
26
|
+
exit 2
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
# --no-exit-codes разводит два разных события, которые иначе слиплись бы в один ненулевой код:
|
|
30
|
+
# «нашлись находки» и «инструмент не отработал». Первое читается из вывода, второе — из кода.
|
|
31
|
+
# NO_COLOR и снятие управляющих последовательностей — вместе, а не по отдельности. Внутри
|
|
32
|
+
# GitHub Actions zizmor КРАСИТ вывод (там цвет поддержан), и итоговая строка начинается с
|
|
33
|
+
# escape-последовательности: правило «^[0-9]+ findings» её не видит, и гейт объявлял, что формат
|
|
34
|
+
# сменился. Локально этого не воспроизвести — вне конвейера цвет выключается сам. Поймано
|
|
35
|
+
# прогоном в конвейере 2026-09-08; тот же класс уже записан у нас в scope.mjs: «цвет снимается ДО
|
|
36
|
+
# поиска». NO_COLOR — соглашение, его может не знать следующая версия; sed — страховка, которая
|
|
37
|
+
# в отличие от NO_COLOR ни от кого не зависит.
|
|
38
|
+
ESC=$(printf '\033')
|
|
39
|
+
OUT=$(NO_COLOR=1 zizmor --no-online-audits --no-exit-codes --min-severity medium --format plain "$WF" 2>&1 \
|
|
40
|
+
| sed "s/${ESC}\[[0-9;]*[a-zA-Z]//g")
|
|
41
|
+
CODE=$?
|
|
42
|
+
if [ "$CODE" -ne 0 ]; then
|
|
43
|
+
echo "zizmor не отработал (код $CODE) — проверка не состоялась, это не вердикт «чисто»"
|
|
44
|
+
printf '%s\n' "$OUT" | grep -iE "error:|panic|not found" | head -3 | sed 's/^/ /'
|
|
45
|
+
echo " почини: прогони «zizmor .github/workflows» руками и посмотри, на чём он споткнулся"
|
|
46
|
+
exit 2
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# Итоговая строка — единственное место, где сказано, сколько чего нашлось. Её отсутствие значит,
|
|
50
|
+
# что формат сменился; молчать об этом нельзя, иначе смена формата станет вечным зелёным.
|
|
51
|
+
SUM=$(printf '%s\n' "$OUT" | grep -E "^(No findings to report|[0-9]+ findings)" | tail -1)
|
|
52
|
+
if [ -z "$SUM" ]; then
|
|
53
|
+
echo "ответ zizmor не разобран: итоговой строки в нём нет"
|
|
54
|
+
# Печатаем то, что пришло на самом деле. Без этого причина видна только тому, у кого есть та
|
|
55
|
+
# же машина: первый отказ этой ветки случился в конвейере, а локально не воспроизводился, и
|
|
56
|
+
# разбирать пришлось догадками. Сообщение, не показывающее свой ввод, лечится вторым прогоном.
|
|
57
|
+
echo " вот последнее, что он напечатал:"
|
|
58
|
+
printf '%s\n' "$OUT" | tail -3 | sed 's/^/ /'
|
|
59
|
+
echo " почини: сверь версию zizmor с той, что названа в gate.yml"
|
|
60
|
+
exit 2
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
case "$SUM" in
|
|
64
|
+
"No findings to report"*) exit 0 ;;
|
|
65
|
+
esac
|
|
66
|
+
|
|
67
|
+
printf '%s\n' "$OUT" | grep -E "^(error|warning)\[" | head -20
|
|
68
|
+
N=$(printf '%s\n' "$OUT" | grep -cE "^(error|warning)\[")
|
|
69
|
+
[ "$N" -gt 20 ] && echo " … и ещё $((N - 20))"
|
|
70
|
+
echo " почини: закрепи действия по SHA вместо метки, сузь permissions до нужного задания,"
|
|
71
|
+
echo " замени pull_request_target на pull_request там, где выполняется код автора PR."
|
|
72
|
+
echo " подробности по каждой находке: https://docs.zizmor.sh/audits/"
|
|
73
|
+
exit 1
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
intent: конвейер не отдаёт чужому коду свои права и секреты
|
|
2
|
+
intent_en: the pipeline does not hand its permissions and secrets to somebody else's code
|
|
3
|
+
|
|
4
|
+
# Там, где конвейер есть. Без него отдавать нечего.
|
|
5
|
+
trigger:
|
|
6
|
+
has_ci: true
|
|
7
|
+
|
|
8
|
+
recipes:
|
|
9
|
+
any: bash {gate}/check.sh {dir}
|
|
10
|
+
|
|
11
|
+
# Программа, без которой запись не работает. По первому слову команды этого не видно: обёртка
|
|
12
|
+
# начинается с `bash`. Версия названа: обёртка читает итоговую строку вывода zizmor, и смена
|
|
13
|
+
# формата обязана быть видимой, а не тихой.
|
|
14
|
+
requires: zizmor
|
|
15
|
+
|
|
16
|
+
proof: incidents/README.md, 2026-09-08 «конвейер отдавал права по подвижной метке» — замер по
|
|
17
|
+
шести настоящим репозиториям показал, что express, flask и uv держат ноль находок высокой и
|
|
18
|
+
средней строгости, а gin (18), ripgrep (19) и httpx (4) нет; сам комплект на момент заведения
|
|
19
|
+
записи имел 13 высоких и 5 средних, включая десять действий, закреплённых меткой вместо SHA
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
name: разбор входящих
|
|
2
|
+
|
|
3
|
+
# pull_request вместо pull_request_target: код автора PR выполняется без доступа к секретам
|
|
4
|
+
# основного репозитория.
|
|
5
|
+
on:
|
|
6
|
+
pull_request:
|
|
7
|
+
types: [opened]
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
triage:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
|
17
|
+
with:
|
|
18
|
+
persist-credentials: false
|
|
19
|
+
- run: echo "разбор без прав на запись"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
name: разбор входящих
|
|
2
|
+
|
|
3
|
+
# pull_request_target выполняется в контексте ОСНОВНОЙ ветки и с доступом к секретам,
|
|
4
|
+
# а код берётся из ветки автора PR. Классическая дыра.
|
|
5
|
+
on:
|
|
6
|
+
pull_request_target:
|
|
7
|
+
types: [opened]
|
|
8
|
+
|
|
9
|
+
permissions: write-all
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
triage:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
with:
|
|
17
|
+
ref: ${{ github.event.pull_request.head.sha }}
|
|
18
|
+
- run: npm install && npm run build
|
|
@@ -40,7 +40,7 @@ EXT_RE="$(printf '%s' "$COLOR_EXT" | tr ' ' '|')"
|
|
|
40
40
|
HITS=$(find "$DIR" $(skip_find "$DIR") -type f -print 2>/dev/null \
|
|
41
41
|
| grep -E "\.($EXT_RE)$" \
|
|
42
42
|
| grep -viE "(^|/)[^/]*($SOURCE_RE)[^/]*\.($EXT_RE)$" \
|
|
43
|
-
|
|
|
43
|
+
| drop_generated \
|
|
44
44
|
| LC_ALL=C sort \
|
|
45
45
|
| xargs -r awk '
|
|
46
46
|
# Блочные комментарии вырезаются ПО СОСТОЯНИЮ, а не построчно. Однострочный фильтр
|
|
@@ -63,7 +63,15 @@ HITS=$(find "$DIR" $(skip_find "$DIR") -type f -print 2>/dev/null \
|
|
|
63
63
|
if (e == 0) { line = substr(line, 1, s - 1); inblock = 1; break }
|
|
64
64
|
line = substr(line, 1, s - 1) substr(rest, e + 2)
|
|
65
65
|
}
|
|
66
|
-
|
|
66
|
+
# Хвост — «не буква, не цифра, не дефис», а не просто «не шестнадцатеричный символ».
|
|
67
|
+
# Якорь ссылки «#defining-entry-points» начинается с «#def», за которым идёт «i»: по
|
|
68
|
+
# прежнему правилу это был цвет. Замер 2026-09-08 по шести чужим репозиториям: в uv
|
|
69
|
+
# ложным оказалось именно это, в docs/js/extra.js.
|
|
70
|
+
# Цвет выписан поразрядно: часть сборок mawk повторители не понимает (в node:22-slim
|
|
71
|
+
# это mawk 1.3.4 20200120), а какая сборка у читателя — мы не знаем
|
|
72
|
+
# {3},{6},{8}, и это условие не срабатывало никогда — гейт молчал на красном образце.
|
|
73
|
+
# Поймано сборкой docker-образа 2026-09-08, а не прогоном: на хосте gawk.
|
|
74
|
+
if (line ~ /#[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]([^0-9a-zA-Z_-]|$)|#[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]([^0-9a-zA-Z_-]|$)|#[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]([^0-9a-zA-Z_-]|$)/)
|
|
67
75
|
print FILENAME ":" FNR ":" $0
|
|
68
76
|
}
|
|
69
77
|
' 2>/dev/null \
|
|
@@ -2,3 +2,5 @@ export function Button({ children }: { children: React.ReactNode }) {
|
|
|
2
2
|
// Цвет приходит из темы: перекрашивается вместе с ней.
|
|
3
3
|
return <button className="bg-[var(--color-accent)] text-[var(--color-on-accent)]">{children}</button>;
|
|
4
4
|
}
|
|
5
|
+
|
|
6
|
+
const DOC_ANCHOR = "concepts/projects/#defining-entry-points";
|
|
@@ -30,7 +30,7 @@ TESTS="-name test -prune -o -name tests -prune -o -name spec -prune -o -name __t
|
|
|
30
30
|
find "$DIR" $(skip_find "$DIR") $TESTS -type f \
|
|
31
31
|
! -name 'test_*' ! -name '*_test.*' ! -name '*.test.*' ! -name '*.spec.*' \
|
|
32
32
|
-print 2>/dev/null | only_code | own_samples_filter "$DIR" \
|
|
33
|
-
|
|
|
33
|
+
| drop_generated \
|
|
34
34
|
| xargs -r awk -v MAX="$MAX" '
|
|
35
35
|
# Один обход на все файлы: процесс на каждый файл дал 19 секунд на 4000 файлов.
|
|
36
36
|
# Разметка вложена по природе: пять уровней тегов — это не сложная логика, а обычная
|
|
@@ -40,12 +40,11 @@ find "$DIR" $(skip_find "$DIR") $TESTS -type f \
|
|
|
40
40
|
FNR == 1 { flush(); worst = 0; wl = 0; wf = FILENAME }
|
|
41
41
|
/^[[:space:]]*$/ { next }
|
|
42
42
|
{
|
|
43
|
-
# ширина отступа: табуляция считается за четыре
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
43
|
+
# ширина отступа: табуляция считается за четыре пробела. Отступ берётся ОДНИМ
|
|
44
|
+
# match, а не посимвольным циклом с substr: цикл выделял новую строку на каждый
|
|
45
|
+
# символ отступа и стоил больше, чем весь остальной разбор. Замер 2026-09-08 на uv.
|
|
46
|
+
match($0, /^[ \t]*/); ind = substr($0, 1, RLENGTH)
|
|
47
|
+
n = gsub(/\t/, "", ind) * 4 + length(ind)
|
|
49
48
|
depth = int(n / 4)
|
|
50
49
|
if (depth > worst) { worst = depth; wl = FNR }
|
|
51
50
|
}
|
|
@@ -30,7 +30,7 @@ TESTS="-name test -prune -o -name tests -prune -o -name spec -prune -o -name __t
|
|
|
30
30
|
find "$DIR" $(skip_find "$DIR") $TESTS -type f \
|
|
31
31
|
! -name 'test_*' ! -name '*_test.*' ! -name '*.test.*' ! -name '*.spec.*' \
|
|
32
32
|
-print 2>/dev/null | only_code | own_samples_filter "$DIR" \
|
|
33
|
-
|
|
|
33
|
+
| drop_generated \
|
|
34
34
|
| LC_ALL=C sort \
|
|
35
35
|
| xargs -r env LC_ALL=C awk -v WIN="$WIN" '
|
|
36
36
|
# Строка-объявление ввоза: `import`, `from … import`, `use …;`, путь в кавычках внутри
|
|
@@ -54,6 +54,10 @@ find "$DIR" $(skip_find "$DIR") $TESTS -type f \
|
|
|
54
54
|
buf[++n] = line
|
|
55
55
|
imp[n] = isimport(line)
|
|
56
56
|
if (n >= WIN) {
|
|
57
|
+
# Ключ и признак «есть ли код» собираются ОДНИМ проходом по окну. Разнести их на
|
|
58
|
+
# два цикла выглядело ускорением — окно из одного ввоза отбрасывалось бы до сборки
|
|
59
|
+
# ключа. Замерено 2026-09-08 на uv: стало 2791 мс против 1770. Второй обход окна
|
|
60
|
+
# дороже, чем сборка ключа, которую он экономит; правка откачена по замеру.
|
|
57
61
|
key = ""; code = 0
|
|
58
62
|
for (i = n - WIN + 1; i <= n; i++) { key = key buf[i] "\x1e"; if (!imp[i]) code = 1 }
|
|
59
63
|
if (!code) next # окно целиком из ввоза — не дубль
|
|
@@ -7,7 +7,10 @@ MISS=0
|
|
|
7
7
|
for MD in "$DIR"/*.md; do
|
|
8
8
|
[ -f "$MD" ] || continue
|
|
9
9
|
# вытащить цели ссылок вида [текст](путь)
|
|
10
|
-
|
|
10
|
+
# Строки со вставками в обратных кавычках чистятся ДО разбора: «`[name](url)`» — это пример
|
|
11
|
+
# оформления ссылки, а не ссылка. Замер 2026-09-08: в uv/STYLE.md именно так и было, и гейт
|
|
12
|
+
# требовал создать файл с именем «url».
|
|
13
|
+
TARGETS=$(sed 's/`[^`]*`//g' "$MD" | sed -n 's/.*](\([^)]*\)).*/\1/p')
|
|
11
14
|
for T in $TARGETS; do
|
|
12
15
|
# автоссылки бывают обёрнуты как <(https://...)> — искать http где угодно внутри,
|
|
13
16
|
# не только в начале строки.
|
|
@@ -6,3 +6,5 @@
|
|
|
6
6
|
Ссылка на страницу сайта документации: [руководство](tutorial/#install) — путь опубликованного
|
|
7
7
|
сайта, а не файл на диске. Найдено замером по fastapi: `[installation guide](tutorial/#install-fastapi)`
|
|
8
8
|
в README читалась как битая, хотя на сайте работает. Так ссылаются mkdocs, docusaurus и jekyll.
|
|
9
|
+
|
|
10
|
+
Пример оформления ссылки: `[name](url)` — это пример, а не ссылка.
|
|
@@ -23,7 +23,7 @@ fi
|
|
|
23
23
|
# не укладывался в две минуты.
|
|
24
24
|
# shellcheck disable=SC2046
|
|
25
25
|
find "$DIR" $(skip_find "$DIR") -type f -print 2>/dev/null | only_code | own_samples_filter "$DIR" \
|
|
26
|
-
|
|
|
26
|
+
| drop_generated \
|
|
27
27
|
| xargs -r wc -l 2>/dev/null \
|
|
28
28
|
| awk '
|
|
29
29
|
$2 == "total" { next }
|
|
@@ -30,7 +30,11 @@ FILES=$(find "$DIR/$LESSONS" -type f -name '*.md' 2>/dev/null)
|
|
|
30
30
|
# старые записи несут её строкой ">", новые — абзацем "**Вывод.**"; оба варианта законны.
|
|
31
31
|
printf '%s\n' $FILES | while read -r F; do
|
|
32
32
|
awk -v FILE="$F" '
|
|
33
|
-
|
|
33
|
+
# Дата выписана поразрядно, а не как [0-9]{4}: часть сборок mawk повторители не понимает
|
|
34
|
+
# повторители в регулярках, и такая строка не совпадала НИКОГДА. Гейт молча выходил с нулём
|
|
35
|
+
# на собственном красном образце. Поймано сборкой образа 2026-09-08: на хосте gawk, в образе
|
|
36
|
+
# mawk, и приёмка каталога внутри образа отклонила две записи из двадцати.
|
|
37
|
+
/^## [0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/ { flush(); title = $0; sub(/^## /, "", title); is_entry = 1; has_mark = 0; next }
|
|
34
38
|
/^## / { flush(); is_entry = 0; next }
|
|
35
39
|
(/✅/ || /🔧/ || /📜/ || /👤/) { has_mark = 1 }
|
|
36
40
|
END { flush() }
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# mcp-server-resolves
|
|
2
|
+
|
|
3
|
+
**Что ловит.** Объявленный MCP-сервер, которого на деле нет: команда указывает на несуществующий
|
|
4
|
+
файл, либо версия не закреплена и завтра приедет другая.
|
|
5
|
+
|
|
6
|
+
**Почему это отдельная запись.** Сервер, который не поднялся, агенту **никак не виден**: у него
|
|
7
|
+
просто нет этих инструментов, и он молча работает без них. Ни ошибки, ни строки в журнале.
|
|
8
|
+
Это тот же класс, что хук с опечаткой в имени события: настройка выглядит как возможность и
|
|
9
|
+
возможностью не является.
|
|
10
|
+
|
|
11
|
+
## Шишка, из которой она выросла
|
|
12
|
+
|
|
13
|
+
2026-09-08. В проект записали настройку `chrome-devtools-mcp` — выглядела безупречно:
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
"chrome-devtools": { "command": "npx", "args": ["-y", "chrome-devtools-mcp@1.9.0"] }
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Сервер был мёртв. Puppeteer подхватывал Windows-овый Chrome из `/mnt/c/` и падал с
|
|
20
|
+
`Target closed`. Узнали только потому, что запустили руками и прочитали журнал — обычно так не
|
|
21
|
+
делают: записали конфиг, увидели «сохранено», пошли дальше.
|
|
22
|
+
|
|
23
|
+
## Замер
|
|
24
|
+
|
|
25
|
+
Двадцать настоящих настроек `.mcp.json`, взятых поиском по GitHub 2026-09-08.
|
|
26
|
+
|
|
27
|
+
| | |
|
|
28
|
+
|---|---|
|
|
29
|
+
| конфигов проверено | 20 |
|
|
30
|
+
| покраснело | 9 |
|
|
31
|
+
| находок | 14 |
|
|
32
|
+
| ложных срабатываний | **0** |
|
|
33
|
+
|
|
34
|
+
Все находки — чужие серверы, которые тянутся незакреплёнными при каждом запуске:
|
|
35
|
+
`@modelcontextprotocol/server-github`, `firecrawl-mcp`, `mcp-server-postgres`, `@upstash/context7-mcp`.
|
|
36
|
+
Серверу с таким доступом «сегодня одна версия, завтра другая» — открытая дверь в цепочке поставок.
|
|
37
|
+
|
|
38
|
+
Замер тут же нашёл и два ложных срабатывания, оба починены до объявления записи рабочей:
|
|
39
|
+
|
|
40
|
+
1. `npx tsx rag/src/index.ts` — сервер **свой**, лежит в репозитории и уже под версией. Требовать
|
|
41
|
+
закрепить `tsx` значит краснеть на нормальном укладе, а такой гейт выключают в первый день.
|
|
42
|
+
2. Починка первого съела настоящую находку: `@meridian/docs-mcp` — имя пакета со слэшем, а слэш
|
|
43
|
+
считался признаком локального файла. Видно было только построчным сравнением до и после.
|
|
44
|
+
|
|
45
|
+
## Готовый аналог
|
|
46
|
+
|
|
47
|
+
Готового аналога нет. Проверено 2026-09-08 поиском: линтеры настроек агента существуют
|
|
48
|
+
(`claudelint`, `agent-lint`), но смотрят на права и хуки, а не на MCP. Ближайшее по смыслу —
|
|
49
|
+
наш собственный `deps-are-pinned`: там то же правило про версии, но для зависимостей сборки,
|
|
50
|
+
и файла `.mcp.json` он не видит.
|
|
51
|
+
|
|
52
|
+
## Чего НЕ ловит
|
|
53
|
+
|
|
54
|
+
- **Не запускает серверы.** Мёртвый по любой другой причине сервер — как в шишке выше, где путь
|
|
55
|
+
и версия были верны, а падал сам браузер, — эта проверка не увидит. Запуск чужих команд на
|
|
56
|
+
каждый коммит означает скачивание пакетов и минуты ожидания; такую проверку выключают сразу.
|
|
57
|
+
Живое рукопожатие — то, что делают руками при подключении сервера, и это записано в методичке,
|
|
58
|
+
а не в гейте.
|
|
59
|
+
- **Не судит об относительных путях.** `"command": "./bin/server"` не проверяется: рабочий
|
|
60
|
+
каталог сервера задаётся полем `cwd`, и проверить существование без запуска нельзя.
|
|
61
|
+
- **Не знает, что пакет существует в реестре.** Это делает `no-phantom-package`, но только для
|
|
62
|
+
того, что упомянуто в документации.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
# Объявленный MCP-сервер, которого на деле нет: команда указывает на файл, которого не
|
|
3
|
+
# существует, либо версия не закреплена и завтра приедет другая.
|
|
4
|
+
#
|
|
5
|
+
# ЗАЧЕМ. Сервер, который не поднялся, агенту НИКАК не виден: у него просто нет этих
|
|
6
|
+
# инструментов, и он молча работает без них. Ни ошибки, ни строки в журнале — та же тишина,
|
|
7
|
+
# что у хука с опечаткой в имени события. Настройка выглядит как возможность и возможностью
|
|
8
|
+
# не является.
|
|
9
|
+
#
|
|
10
|
+
# ПРОВЕРЕНО НА СЕБЕ 2026-09-08. Записали в проект настройку chrome-devtools-mcp, выглядела
|
|
11
|
+
# безупречно. Сервер был мёртв: puppeteer подхватывал Windows-овый Chrome из /mnt/c/ и падал
|
|
12
|
+
# с «Target closed». Узнали только потому, что запустили руками и прочитали журнал; обычно
|
|
13
|
+
# так не делают — записали конфиг, увидели «сохранено», пошли дальше.
|
|
14
|
+
#
|
|
15
|
+
# ЧТО ЭТА ПРОВЕРКА НЕ ДЕЛАЕТ. Она НЕ запускает серверы. Запуск — это исполнение чужих команд
|
|
16
|
+
# на каждый коммит, скачивание пакетов и минуты ожидания; такую проверку выключают в первый
|
|
17
|
+
# день. Здесь ловится только то, что видно без запуска, и этого достаточно для двух самых
|
|
18
|
+
# частых смертей: файла нет и версия плавает.
|
|
19
|
+
DIR="${1:-.}"
|
|
20
|
+
SKIP_LIB="$(dirname "$0")/../_skip.sh"
|
|
21
|
+
if [ ! -f "$SKIP_LIB" ]; then
|
|
22
|
+
echo "рядом с проверкой нет _skip.sh — обход не собран, проверка не состоялась"
|
|
23
|
+
echo " почини: скопируй гейт вместе с файлом kit/gates/_skip.sh, он общий на весь каталог"
|
|
24
|
+
exit 2
|
|
25
|
+
fi
|
|
26
|
+
. "$SKIP_LIB"
|
|
27
|
+
|
|
28
|
+
OUT=""
|
|
29
|
+
for F in "$DIR/.mcp.json" "$DIR/.cursor/mcp.json" "$DIR/.vscode/mcp.json" "$DIR/.claude/mcp.json"; do
|
|
30
|
+
[ -f "$F" ] || continue
|
|
31
|
+
RES=$(awk -v file="$F" -v dir="$DIR" '
|
|
32
|
+
# Номера строк с командой копятся отдельно от текста: разбор идёт в END по всему файлу
|
|
33
|
+
# одной строкой (иначе минифицированный JSON не разобрать), а находку надо привязать к
|
|
34
|
+
# строке — без неё `--since` не сверит путь с дифом и гейт зазеленеет на чужом дифе.
|
|
35
|
+
/"command"[ \t]*:/ { cline[++ci] = NR }
|
|
36
|
+
{ buf = buf $0 " " }
|
|
37
|
+
END {
|
|
38
|
+
n = split(buf, part, /"command"[ \t]*:/)
|
|
39
|
+
for (i = 2; i <= n; i++) {
|
|
40
|
+
chunk = part[i]
|
|
41
|
+
if (!match(chunk, /"[^"]*"/)) continue
|
|
42
|
+
cmd = substr(chunk, RSTART + 1, RLENGTH - 2)
|
|
43
|
+
line = cline[i - 1] ? cline[i - 1] : 1
|
|
44
|
+
|
|
45
|
+
# 1. Абсолютный путь, которого нет. Самая частая смерть у пакетов, чья версия вшита
|
|
46
|
+
# в имя файла: обновились — старый файл удалён, сервер умер, никто не заметил.
|
|
47
|
+
if (substr(cmd, 1, 1) == "/") {
|
|
48
|
+
if (system("test -e \"" cmd "\"") != 0)
|
|
49
|
+
print file ":" line ": команда сервера не существует: " cmd
|
|
50
|
+
continue
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# 2. Скачивающий запуск без точной версии. `@latest` и голое имя означают «сегодня
|
|
54
|
+
# одно, завтра другое», а для сервера, которому доверен браузер и файлы, это дверь
|
|
55
|
+
# в цепочке поставок. Правило то же, что у нас для зависимостей.
|
|
56
|
+
if (cmd == "npx" || cmd == "uvx" || cmd == "pipx" || cmd == "bunx" || cmd == "pnpm") {
|
|
57
|
+
args = ""
|
|
58
|
+
if (match(chunk, /"args"[ \t]*:[ \t]*\[[^]]*\]/)) args = substr(chunk, RSTART, RLENGTH)
|
|
59
|
+
if (args == "") continue
|
|
60
|
+
if (args ~ /@latest/) {
|
|
61
|
+
print file ":" line ": версия сервера не закреплена (@latest): " cmd
|
|
62
|
+
continue
|
|
63
|
+
}
|
|
64
|
+
# Имя пакета — первый довод, не начинающийся с дефиса. Точная версия пишется как
|
|
65
|
+
# name@1.2.3 (npm) или name==1.2.3 (python); без неё запуск невоспроизводим.
|
|
66
|
+
m = args
|
|
67
|
+
gsub(/"args"[ \t]*:[ \t]*\[/, "", m); gsub(/\]/, "", m)
|
|
68
|
+
k = split(m, tok, ",")
|
|
69
|
+
|
|
70
|
+
# СЕРВЕР ИЗ СВОЕГО РЕПОЗИТОРИЯ — не находка. `npx tsx rag/src/index.ts` запускает
|
|
71
|
+
# СВОЙ файл, а npx здесь только запускалка: код сервера лежит в репозитории и уже
|
|
72
|
+
# под версией — той же, что и весь проект. Требовать закрепить `tsx` значит краснеть
|
|
73
|
+
# на нормальном укладе, а такой гейт выключают в первый день. Найдено замером по
|
|
74
|
+
# двадцати чужим настройкам с GitHub 2026-09-08: две из десяти находок были ровно
|
|
75
|
+
# этим. Тот же класс, что четыре ложных срабатывания на чужом коде до этого.
|
|
76
|
+
local_entry = 0
|
|
77
|
+
for (j = 1; j <= k; j++) {
|
|
78
|
+
t = tok[j]; gsub(/^[ \t"]+|[ \t"]+$/, "", t)
|
|
79
|
+
# Признак локального входа — расширение файла или явно относительный путь.
|
|
80
|
+
# Просто «есть слэш» не годится: `@scope/name` — это имя пакета в npm, и первая
|
|
81
|
+
# версия этой проверки съела настоящую находку `npx @meridian/docs-mcp`. Поймано
|
|
82
|
+
# тем же замером по двадцати чужим настройкам: находок стало восемь вместо девяти,
|
|
83
|
+
# и пропажу было видно только построчным сравнением до и после.
|
|
84
|
+
if (t ~ /\.(ts|js|mjs|cjs|py|rb|sh)$/ || t ~ /^\// || t ~ /^\.\// || t ~ /^\.\.\//) { local_entry = 1; break }
|
|
85
|
+
}
|
|
86
|
+
if (local_entry) continue
|
|
87
|
+
|
|
88
|
+
for (j = 1; j <= k; j++) {
|
|
89
|
+
t = tok[j]; gsub(/^[ \t"]+|[ \t"]+$/, "", t)
|
|
90
|
+
if (t == "" || substr(t, 1, 1) == "-") continue
|
|
91
|
+
if (t ~ /@[0-9]/ || t ~ /==[0-9]/) break
|
|
92
|
+
print file ":" line ": версия сервера не закреплена: " cmd " " t
|
|
93
|
+
break
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
' "$F")
|
|
99
|
+
[ -n "$RES" ] && OUT="${OUT}${RES}
|
|
100
|
+
"
|
|
101
|
+
done
|
|
102
|
+
|
|
103
|
+
OUT=$(printf '%s' "$OUT" | own_samples_filter "$DIR" | sed '/^$/d')
|
|
104
|
+
if [ -n "$OUT" ]; then
|
|
105
|
+
printf '%s\n' "$OUT"
|
|
106
|
+
echo " почини: закрепи точную версию (пакет@1.2.3) и убедись, что файл команды существует."
|
|
107
|
+
echo " мёртвый сервер агенту не виден: он просто работает без этих инструментов и молчит."
|
|
108
|
+
exit 1
|
|
109
|
+
fi
|
|
110
|
+
exit 0
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
intent: объявленный MCP-сервер существует и закреплён по версии — иначе агент молча работает без него
|
|
2
|
+
intent_en: a declared MCP server exists and is version-pinned — otherwise the agent silently works without it
|
|
3
|
+
|
|
4
|
+
# Только там, где агенту подключали внешние инструменты. В проекте без `.mcp.json` проверять
|
|
5
|
+
# нечего, а запись, показанная не тому, стоит доверия всему каталогу.
|
|
6
|
+
trigger:
|
|
7
|
+
has_mcp: true
|
|
8
|
+
|
|
9
|
+
recipes:
|
|
10
|
+
any: bash {gate}/check.sh {dir}
|
|
11
|
+
|
|
12
|
+
samples_for: any
|
|
13
|
+
|
|
14
|
+
proof: incidents/README.md, 2026-09-08 «конфиг был написан верно, а сервер мёртв» — в проект
|
|
15
|
+
записали настройку `chrome-devtools-mcp`, выглядела безупречно; сервер падал с «Target closed»,
|
|
16
|
+
потому что puppeteer подхватывал Windows-овый Chrome из `/mnt/c/`. Узнали только запуском
|
|
17
|
+
руками и чтением журнала: агенту мёртвый сервер не виден вовсе — у него просто нет этих
|
|
18
|
+
инструментов, и он молча работает без них
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"mcpServers": {
|
|
3
|
+
"browser": {
|
|
4
|
+
"command": "npx",
|
|
5
|
+
"args": ["-y", "chrome-devtools-mcp@1.9.0"]
|
|
6
|
+
},
|
|
7
|
+
"search": {
|
|
8
|
+
"command": "uvx",
|
|
9
|
+
"args": ["some-search-mcp==0.4.1"]
|
|
10
|
+
},
|
|
11
|
+
"local": {
|
|
12
|
+
"command": "node",
|
|
13
|
+
"args": ["scripts/mcp-server.mjs"]
|
|
14
|
+
},
|
|
15
|
+
"own-code": {
|
|
16
|
+
"command": "npx",
|
|
17
|
+
"args": ["tsx", "rag/src/index.ts"]
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|