@rt-tools/agent-kit 0.2.0 → 0.3.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 +59 -6
- package/assets/hooks/browser-device-id.sh +20 -0
- package/assets/hooks/browser-guard-device-id.sh +27 -0
- package/assets/hooks/browser-guard-no-listing.sh +17 -0
- package/assets/hooks/browser-guard-no-other-drivers.sh +78 -0
- package/assets/hooks/browser-guard-require-select.sh +53 -0
- package/assets/hooks/commit-msg.sh +26 -0
- package/assets/hooks/constitution-index.sh +42 -0
- package/assets/hooks/dev-server-guard.sh +113 -0
- package/assets/hooks/docs-guard.sh +96 -0
- package/assets/hooks/git-guard-delivery.sh +110 -0
- package/assets/hooks/git-guard-main.sh +72 -0
- package/assets/hooks/git-guard-push-tests.sh +73 -0
- package/assets/hooks/lint-after-edit.sh +94 -0
- package/assets/hooks/qa-dataid-guard.sh +81 -0
- package/assets/hooks/reuse-first-guard.sh +83 -0
- package/assets/hooks/skill-gate-rearm.sh +22 -0
- package/assets/hooks/skill-gate.sh +68 -0
- package/assets/hooks/skill-loaded.sh +20 -0
- package/assets/hooks/sql-guard.sh +129 -0
- package/assets/patterns/angular-patterns-state.md +94 -0
- package/assets/patterns/api-layer-pair.md +78 -0
- package/assets/patterns/browser-verification-measure.md +83 -0
- package/assets/patterns/browser-verification-stand.md +79 -0
- package/assets/patterns/component-structure-new.md +98 -0
- package/assets/patterns/doc-style-sweep.md +100 -0
- package/assets/patterns/doc-style-write.md +106 -0
- package/assets/patterns/git-workflow-commit.md +175 -0
- package/assets/patterns/git-workflow-merge.md +82 -0
- package/assets/patterns/git-workflow-migration.md +58 -0
- package/assets/patterns/git-workflow-restart.md +49 -0
- package/assets/patterns/lib-layers-move.md +77 -0
- package/assets/patterns/lib-layers-new.md +70 -0
- package/assets/patterns/permissions-procedure.md +69 -0
- package/assets/patterns/platform-access-di.md +70 -0
- package/assets/patterns/reuse-first-extend.md +73 -0
- package/assets/patterns/seo-page.md +92 -0
- package/assets/patterns/seo-verify.md +64 -0
- package/assets/patterns/shared-code-new.md +80 -0
- package/assets/patterns/spec-driven-domain.md +100 -0
- package/assets/patterns/spec-driven-rule.md +112 -0
- package/assets/patterns/styling-bem-component.md +77 -0
- package/assets/patterns/styling-bem-layout.md +67 -0
- package/assets/patterns/testing-e2e.md +90 -0
- package/assets/patterns/testing-unit.md +93 -0
- package/assets/patterns/translations-key.md +51 -0
- package/assets/patterns/ts-procedure.md +66 -0
- package/assets/rules/angular-patterns.md +52 -0
- package/assets/rules/api-layer.md +53 -0
- package/assets/rules/browser-verification.md +69 -0
- package/assets/rules/component-structure.md +48 -0
- package/assets/rules/doc-style.md +61 -0
- package/assets/rules/git-workflow.md +106 -0
- package/assets/rules/lib-layers.md +54 -0
- package/assets/rules/permissions.md +52 -0
- package/assets/rules/platform-access.md +49 -0
- package/assets/rules/reuse-first.md +69 -0
- package/assets/rules/seo.md +50 -0
- package/assets/rules/shared-code.md +45 -0
- package/assets/rules/spec-driven.md +89 -0
- package/assets/rules/styling-bem.md +59 -0
- package/assets/rules/testing.md +69 -0
- package/assets/rules/translations.md +52 -0
- package/assets/rules/typescript-conventions.md +46 -0
- package/assets/templates/gate-map.sh +37 -0
- package/assets/templates/implementation.md +38 -0
- package/assets/templates/pattern.md +4 -0
- package/assets/templates/project.sh +41 -0
- package/assets/templates/rule.md +12 -23
- package/lib/assets.d.ts +8 -0
- package/lib/assets.d.ts.map +1 -1
- package/lib/assets.js +12 -1
- package/lib/assets.js.map +1 -1
- package/lib/commands.d.ts.map +1 -1
- package/lib/commands.js +21 -2
- package/lib/commands.js.map +1 -1
- package/lib/companion.d.ts +53 -0
- package/lib/companion.d.ts.map +1 -0
- package/lib/companion.js +33 -0
- package/lib/companion.js.map +1 -0
- package/lib/config.d.ts +24 -1
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +33 -1
- package/lib/config.js.map +1 -1
- package/lib/stamp.d.ts +2 -5
- package/lib/stamp.d.ts.map +1 -1
- package/lib/stamp.js +25 -10
- package/lib/stamp.js.map +1 -1
- package/lib/sync.d.ts +3 -0
- package/lib/sync.d.ts.map +1 -1
- package/lib/sync.js +20 -1
- package/lib/sync.js.map +1 -1
- package/package.json +1 -1
- package/rt-tools-agent-kit-0.3.0.tgz +0 -0
- package/rt-tools-agent-kit-0.2.0.tgz +0 -0
package/README.md
CHANGED
|
@@ -9,11 +9,12 @@
|
|
|
9
9
|
соседнего они устаревают молча: правку в исходнике никто не переносит, а расхождение видно
|
|
10
10
|
только по последствиям.
|
|
11
11
|
|
|
12
|
-
Пакет разделяет текст на
|
|
13
|
-
имён
|
|
14
|
-
|
|
12
|
+
Пакет разделяет текст на три слоя. **Закон** — что должно быть верно; он не знает ни путей, ни
|
|
13
|
+
имён файлов. **Правило** — каким приёмом это делается; приём переносится между репозиториями
|
|
14
|
+
так же, как закон. **Паттерн** — готовый код приёма.
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
Все три везёт пакет. В проекте остаётся то, чего пакет знать не может: имена этого дерева. Они
|
|
17
|
+
живут при каждом правиле отдельным файлом — `implementation.md`, — и пишет его проект.
|
|
17
18
|
|
|
18
19
|
## Как пользоваться
|
|
19
20
|
|
|
@@ -35,6 +36,56 @@ npx agent-kit sync --check
|
|
|
35
36
|
Команда ничего не пишет и отказывает, если разложенное отстало от пакета или его правили
|
|
36
37
|
руками.
|
|
37
38
|
|
|
39
|
+
## Что где лежит
|
|
40
|
+
|
|
41
|
+
| Род | Куда ложится | Что это |
|
|
42
|
+
| --- | --- | --- |
|
|
43
|
+
| `laws` | `docs/constitution/` | что должно быть верно |
|
|
44
|
+
| `rules` | `.claude/skills/<имя>/SKILL.md` | каким приёмом это делается |
|
|
45
|
+
| `patterns` | `.claude/skills/<имя>/SKILL.md` | готовый код приёма |
|
|
46
|
+
| `hooks` | `.claude/hooks/` | что не даёт нарушить |
|
|
47
|
+
| `agents`, `commands`, `workflows` | `.claude/` | роли, слеш-команды и многошаговые прогоны |
|
|
48
|
+
| `checks` | `tools/` | проверки, которые зовёт гейт |
|
|
49
|
+
| `templates` | `.claude/rt-kit/templates/` | формы правила, паттерна, компаньона и карты гейта |
|
|
50
|
+
|
|
51
|
+
Правило и паттерн ложатся одинаково — оба скилы; различает их `kind` во вступлении файла.
|
|
52
|
+
|
|
53
|
+
## Правило и его компаньон
|
|
54
|
+
|
|
55
|
+
Правило говорит приёмом и потому переносимо. Всё, что знает только это дерево — как что здесь
|
|
56
|
+
называется, где лежит, чем проверяется, — живёт рядом с ним в `implementation.md`.
|
|
57
|
+
|
|
58
|
+
Черновик компаньона `sync` кладёт **один раз**, при первой раскладке правила, и больше к нему
|
|
59
|
+
не возвращается: своего текста у пакета там нет, а перекладывать значило бы стирать написанное
|
|
60
|
+
проектом. Пока в черновике осталась метка `<!-- заполняет проект -->`, `sync --check` отказывает:
|
|
61
|
+
правило без имён этого дерева — это закон, и агент по нему работать не может.
|
|
62
|
+
|
|
63
|
+
## Хуки
|
|
64
|
+
|
|
65
|
+
Закон и правило, которых никто не открывает, не действуют, поэтому пакет везёт и хуки, которые
|
|
66
|
+
их зовут.
|
|
67
|
+
|
|
68
|
+
**Гейт правил.** `skill-gate.sh` отбивает правку, пока не загружено правило под неё,
|
|
69
|
+
`skill-loaded.sh` записывает загрузку, `skill-gate-rearm.sh` взводит гейт заново после сжатия
|
|
70
|
+
контекста, `constitution-index.sh` печатает указатель законов на старте сессии.
|
|
71
|
+
|
|
72
|
+
**Сторожевые хуки.** Главная ветка, поставка, документ в одном коммите с правкой, проверки
|
|
73
|
+
перед пушем, линтер по следам правки, пишущие запросы к хранилищу, второй сервер разработки,
|
|
74
|
+
якорь для спек, переизобретение готового, формат сообщения коммита, закреплённый профиль
|
|
75
|
+
браузера.
|
|
76
|
+
|
|
77
|
+
**Что при этом остаётся проекту.** Хуки везут механизм, но не знают ни путей, ни команд, ни
|
|
78
|
+
инвентаря — это дерево знает только оно само:
|
|
79
|
+
|
|
80
|
+
| Файл проекта | Что в нём | Кто читает |
|
|
81
|
+
| --- | --- | --- |
|
|
82
|
+
| `.claude/rt-kit/gate-map.sh` | что правится — какое правило | гейт правил |
|
|
83
|
+
| `.claude/rt-kit/project.sh` | команды, стенды, пары «правка — документ», форма имени ветки | сторожевые хуки |
|
|
84
|
+
| `.claude/rt-kit/browser-device-id` | закреплённый профиль браузера этой машины | браузерные гарды |
|
|
85
|
+
|
|
86
|
+
Формы первых двух лежат в шаблонах. Нет файла — хук пропускает: пустой гард лучше гарда,
|
|
87
|
+
отбивающего наугад, и это же правило действует на каждую отдельную функцию профиля.
|
|
88
|
+
|
|
38
89
|
## Выбор законов
|
|
39
90
|
|
|
40
91
|
Половина законов пакета про то, чего в конкретном приложении нет вовсе: у сервиса без админки
|
|
@@ -107,5 +158,7 @@ npx agent-kit init --laws access,delivery,verifiability
|
|
|
107
158
|
|
|
108
159
|
## Чего в пакете пока нет
|
|
109
160
|
|
|
110
|
-
|
|
111
|
-
|
|
161
|
+
Проверок, ролей, слеш-команд и воркфлоу он не везёт: роды заведены, ресурсов в них ещё нет.
|
|
162
|
+
Пресета карты хуков для настроек агента тоже нет — раскладка кладёт файлы, а карта хуков живёт
|
|
163
|
+
разделом в чужом JSON, и сливать его она пока не умеет. Генератора черновиков правил по закону
|
|
164
|
+
нет.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Общий помощник: печатает идентификатор закреплённого профиля браузера.
|
|
3
|
+
#
|
|
4
|
+
# Идентификатор локален для машины и в пакет не едет вовсе. Он берётся из переменной окружения,
|
|
5
|
+
# а если её нет — из файла рядом с конфигом раскладки. Ни там ни там ничего нет — помощник
|
|
6
|
+
# молчит, и все браузерные гарды пропускают: гард, который не может назвать нужный профиль,
|
|
7
|
+
# ничего не предлагает взамен, и слепой отказ только заводил бы работу в тупик.
|
|
8
|
+
#
|
|
9
|
+
# Файл с идентификатором в репозиторий не коммитится: у каждой машины он свой.
|
|
10
|
+
|
|
11
|
+
if [ -n "${RT_BROWSER_DEVICE_ID:-}" ]; then
|
|
12
|
+
printf '%s\n' "$RT_BROWSER_DEVICE_ID"
|
|
13
|
+
exit 0
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
file="${CLAUDE_PROJECT_DIR:-.}/.claude/rt-kit/browser-device-id"
|
|
17
|
+
[ -f "$file" ] || exit 0
|
|
18
|
+
|
|
19
|
+
tr -d '[:space:]' <"$file"
|
|
20
|
+
printf '\n'
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Гард выбора браузера. PreToolUse на выборе браузера расширением.
|
|
3
|
+
#
|
|
4
|
+
# Отклоняет любой профиль, кроме закреплённого: чужой стоит лишнего круга и приводит в браузер,
|
|
5
|
+
# где сессий этого проекта нет вовсе.
|
|
6
|
+
#
|
|
7
|
+
# На совпадении ставит метку сессии. Гард свежести читает ВОЗРАСТ этой метки — она и делает
|
|
8
|
+
# законной всю дальнейшую работу с браузером.
|
|
9
|
+
#
|
|
10
|
+
# ОТКАЗ В ПОЛЬЗУ РАБОТЫ: помощник не назвал профиль — пропуск.
|
|
11
|
+
|
|
12
|
+
input="$(cat 2>/dev/null)"
|
|
13
|
+
|
|
14
|
+
device_id="$("${CLAUDE_PROJECT_DIR:-.}/{{hooksDir}}/browser-device-id.sh" 2>/dev/null)"
|
|
15
|
+
[ -z "$device_id" ] && exit 0
|
|
16
|
+
|
|
17
|
+
requested="$(printf '%s' "$input" | jq -r '.tool_input.deviceId // empty' 2>/dev/null)"
|
|
18
|
+
|
|
19
|
+
if [ "$requested" = "$device_id" ]; then
|
|
20
|
+
sid="$(printf '%s' "$input" | jq -r '.session_id // "nosession"' 2>/dev/null)"
|
|
21
|
+
marker_dir="${TMPDIR:-/tmp}/claude-browser-guard"
|
|
22
|
+
mkdir -p "$marker_dir" 2>/dev/null && : >"$marker_dir/${sid}" 2>/dev/null
|
|
23
|
+
exit 0
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
echo "Профиль «${requested}» не тот, что закреплён за проектом. Бери ${device_id} — единственный профиль, где сделан вход." >&2
|
|
27
|
+
exit 2
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Гард перечисления и переключения браузеров. PreToolUse.
|
|
3
|
+
#
|
|
4
|
+
# Сессии проекта живут в одном закреплённом профиле. Перечисление и переключение отдают общие
|
|
5
|
+
# неустойчивые имена, которые не опознают ничего, а выбор из них приводит в профиль без входа —
|
|
6
|
+
# поэтому единственный поддержанный путь — выбор по закреплённому идентификатору.
|
|
7
|
+
#
|
|
8
|
+
# ОТКАЗ В ПОЛЬЗУ РАБОТЫ: помощник не назвал профиль — пропуск. Гард, который не может назвать
|
|
9
|
+
# нужный профиль, ничего не предлагает взамен, и слепой отказ только заводил бы работу в тупик.
|
|
10
|
+
|
|
11
|
+
cat >/dev/null 2>&1
|
|
12
|
+
|
|
13
|
+
device_id="$("${CLAUDE_PROJECT_DIR:-.}/{{hooksDir}}/browser-device-id.sh" 2>/dev/null)"
|
|
14
|
+
[ -z "$device_id" ] && exit 0
|
|
15
|
+
|
|
16
|
+
echo "Не перечисляй и не переключай браузеры. Вызови выбор браузера с профилем ${device_id} — единственным, где сделан вход." >&2
|
|
17
|
+
exit 2
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Гард обходных путей к браузеру. PreToolUse.
|
|
3
|
+
#
|
|
4
|
+
# Закрепление профиля чего-то стоит только тогда, когда дверь одна. Здесь перечислены двери,
|
|
5
|
+
# которые обходят её целиком и закреплённый профиль не спрашивают вовсе: второй драйвер,
|
|
6
|
+
# третий драйвер, открытие ссылки средствами системы, управление браузером через сценарий
|
|
7
|
+
# автоматизации, отдельная утилита и прямой запуск бинарника.
|
|
8
|
+
#
|
|
9
|
+
# Написание сквозных спек при этом остаётся законным: прогон спеки не выдаёт агенту
|
|
10
|
+
# интерактивный браузер. Отбиваются только глаголы вождения.
|
|
11
|
+
#
|
|
12
|
+
# ОТКАЗ В ПОЛЬЗУ РАБОТЫ: помощник не назвал профиль — пропуск.
|
|
13
|
+
|
|
14
|
+
input="$(cat 2>/dev/null)"
|
|
15
|
+
|
|
16
|
+
device_id="$("${CLAUDE_PROJECT_DIR:-.}/{{hooksDir}}/browser-device-id.sh" 2>/dev/null)"
|
|
17
|
+
[ -z "$device_id" ] && exit 0
|
|
18
|
+
|
|
19
|
+
tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
|
|
20
|
+
|
|
21
|
+
deny() {
|
|
22
|
+
echo "$1 Води браузер закреплённым расширением: выбери профиль ${device_id} и работай его инструментами." >&2
|
|
23
|
+
exit 2
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
case "$tool" in
|
|
27
|
+
mcp__playwright__*)
|
|
28
|
+
deny "Второй драйвер браузера в этом проекте не используется — в его профиле вход не сделан." ;;
|
|
29
|
+
mcp__chrome-devtools__*)
|
|
30
|
+
deny "Третий драйвер браузера в этом проекте не используется — закреплённый профиль он не спрашивает." ;;
|
|
31
|
+
esac
|
|
32
|
+
|
|
33
|
+
# Терминал среды разработки запускает те же драйверы той же командной строкой.
|
|
34
|
+
case "$tool" in
|
|
35
|
+
Bash | mcp__webstorm__execute_terminal_command | mcp__webstorm__execute_tool) ;;
|
|
36
|
+
*) exit 0 ;;
|
|
37
|
+
esac
|
|
38
|
+
|
|
39
|
+
cmd="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null)"
|
|
40
|
+
[ -z "$cmd" ] && exit 0
|
|
41
|
+
|
|
42
|
+
if [ "$tool" = "mcp__webstorm__execute_tool" ] && command -v perl >/dev/null 2>&1; then
|
|
43
|
+
inner="$(printf '%s' "$cmd" | perl -0ne '
|
|
44
|
+
if (/--command(?:=|\s+)(?:"((?:[^"\\]|\\.)*)"|\x27([^\x27]*)\x27|(.+))/s) {
|
|
45
|
+
print defined $1 ? $1 : (defined $2 ? $2 : $3);
|
|
46
|
+
}
|
|
47
|
+
' 2>/dev/null)"
|
|
48
|
+
[ -n "$inner" ] && cmd="$inner"
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
# Прогон сквозных спек — законный путь, и он не отбивается никогда.
|
|
52
|
+
case "$cmd" in
|
|
53
|
+
*playwright\ open*|*playwright\ codegen*|*playwright\ screenshot*|*playwright\ cr*)
|
|
54
|
+
deny "Вождение браузера из командной строки драйвера обходит закреплённый профиль." ;;
|
|
55
|
+
esac
|
|
56
|
+
|
|
57
|
+
case "$cmd" in
|
|
58
|
+
*open\ http*|*open\ -a\ *Chrome*|*open\ -a\ *chrome*)
|
|
59
|
+
deny "Открытие адреса средствами системы поднимает браузер по умолчанию, а не закреплённый профиль." ;;
|
|
60
|
+
*osascript*Chrome*|*osascript*chrome*)
|
|
61
|
+
deny "Управление браузером сценарием автоматизации обходит закреплённый профиль." ;;
|
|
62
|
+
*chrome-cli*)
|
|
63
|
+
deny "Эта утилита обходит закреплённый профиль." ;;
|
|
64
|
+
esac
|
|
65
|
+
|
|
66
|
+
# Бинарник браузера — и только в позиции команды.
|
|
67
|
+
#
|
|
68
|
+
# Голый образец с именем движка здесь непригоден: он совпадает и со значением флага, которым
|
|
69
|
+
# помечают движок в прогоне сквозных спек, — такой образец отбил бы сам прогон в день, когда
|
|
70
|
+
# появился. Поэтому якорь на границе команды и требование похожего на исполняемый файл слова,
|
|
71
|
+
# а не значения флага.
|
|
72
|
+
printf '%s' "$cmd" | grep -qE '(^|[;&|(]|[[:space:]]&&|[[:space:]]\|\|)[[:space:]]*(/[^[:space:]]*/)?(google-chrome|chromium)([[:space:]]|$)' \
|
|
73
|
+
&& deny "Прямой запуск бинарника браузера обходит закреплённый профиль."
|
|
74
|
+
|
|
75
|
+
printf '%s' "$cmd" | grep -qF 'Google Chrome.app/Contents/MacOS' \
|
|
76
|
+
&& deny "Прямой запуск бинарника браузера обходит закреплённый профиль."
|
|
77
|
+
|
|
78
|
+
exit 0
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Гард свежести выбора браузера. PreToolUse на всех остальных вызовах расширения.
|
|
3
|
+
#
|
|
4
|
+
# ЗАЧЕМ ОН ЕСТЬ — отказ, из которого он вырос: расширение действует на тот браузер, который
|
|
5
|
+
# считает активным сейчас, и этот выбор ПЛЫВЁТ. Выбор, сделанный в начале сессии, не держится:
|
|
6
|
+
# после долгого перерыва на работу без браузера следующий же вызов открыл вкладку в другом
|
|
7
|
+
# профиле — молча. Гард на самом выборе такого не ловит: в этот момент выбор никто не вызывает,
|
|
8
|
+
# а тот, что был сделан раньше, был верным.
|
|
9
|
+
#
|
|
10
|
+
# Поэтому гард про СВЕЖЕСТЬ, а не про «выбирали ли вообще»:
|
|
11
|
+
# - гард выбора ставит метку на каждом принятом выборе;
|
|
12
|
+
# - каждый прошедший здесь вызов метку обновляет, поэтому непрерывная работа идёт свободно;
|
|
13
|
+
# - как только метка старше окна, следующий вызов отбивается и требует выбрать заново.
|
|
14
|
+
# Перерыв — это ровно то, когда выбор уплывает, поэтому перерыв гард и взводит.
|
|
15
|
+
#
|
|
16
|
+
# Выбор — один дешёвый повторяемый вызов, и повторить его стоит несравнимо меньше, чем попасть
|
|
17
|
+
# не в тот браузер.
|
|
18
|
+
#
|
|
19
|
+
# ОТКАЗ В ПОЛЬЗУ РАБОТЫ: помощник не назвал профиль — пропуск.
|
|
20
|
+
|
|
21
|
+
input="$(cat 2>/dev/null)"
|
|
22
|
+
|
|
23
|
+
device_id="$("${CLAUDE_PROJECT_DIR:-.}/{{hooksDir}}/browser-device-id.sh" 2>/dev/null)"
|
|
24
|
+
[ -z "$device_id" ] && exit 0
|
|
25
|
+
|
|
26
|
+
tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
|
|
27
|
+
# У перечисления, переключения и самого выбора свои гарды.
|
|
28
|
+
case "$tool" in
|
|
29
|
+
*list_connected_browsers|*switch_browser|*select_browser) exit 0 ;;
|
|
30
|
+
esac
|
|
31
|
+
|
|
32
|
+
ttl=300
|
|
33
|
+
|
|
34
|
+
sid="$(printf '%s' "$input" | jq -r '.session_id // "nosession"' 2>/dev/null)"
|
|
35
|
+
marker="${TMPDIR:-/tmp}/claude-browser-guard/${sid}"
|
|
36
|
+
|
|
37
|
+
if [ ! -f "$marker" ]; then
|
|
38
|
+
echo "В этой сессии браузер не выбран. Вызови выбор браузера с профилем ${device_id} до любого другого вызова." >&2
|
|
39
|
+
exit 2
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
now="$(date +%s)"
|
|
43
|
+
stamped="$(stat -f %m "$marker" 2>/dev/null || stat -c %Y "$marker" 2>/dev/null || echo 0)"
|
|
44
|
+
age=$(( now - stamped ))
|
|
45
|
+
|
|
46
|
+
if [ "$age" -gt "$ttl" ]; then
|
|
47
|
+
rm -f "$marker" 2>/dev/null
|
|
48
|
+
echo "Последний выбор браузера был ${age} с назад (предел ${ttl} с) — на таких перерывах активный браузер расширения уплывает, и это может быть уже не закреплённый профиль. Вызови выбор с профилем ${device_id} заново и повтори." >&2
|
|
49
|
+
exit 2
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
: >"$marker" 2>/dev/null
|
|
53
|
+
exit 0
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Проверка сообщения коммита. Хук самого гита, а не агента.
|
|
3
|
+
#
|
|
4
|
+
# Версионируемый шаблон: рабочая копия хуков гита не версионируется, поэтому файл ставится в
|
|
5
|
+
# неё отдельно — скриптом подготовки при установке зависимостей. Переустановка руками, если
|
|
6
|
+
# подготовка почему-то не отработала:
|
|
7
|
+
# cp {{hooksDir}}/commit-msg.sh .git/hooks/commit-msg && chmod +x .git/hooks/commit-msg
|
|
8
|
+
#
|
|
9
|
+
# Намеренно НЕ через обёртку, подменяющую путь хуков: подмена уводит гит в свой каталог и рвёт
|
|
10
|
+
# остальные хуки, уже лежащие в рабочей копии. Родной путь оставляет их нетронутыми.
|
|
11
|
+
#
|
|
12
|
+
# Разобранный по типу и области заголовок читается списком, а свободный текст — только
|
|
13
|
+
# целиком; поэтому формат и проверяется здесь, на месте, а не глазами на разборе.
|
|
14
|
+
#
|
|
15
|
+
# ОТКАЗ В ПОЛЬЗУ РАБОТЫ: проверяльщика нет (свежий клон без установленных зависимостей) —
|
|
16
|
+
# пропуск. Формат тогда не проверяется, и это лучше, чем заклинивший коммит.
|
|
17
|
+
|
|
18
|
+
msg_file="$1"
|
|
19
|
+
[ -z "$msg_file" ] && exit 0
|
|
20
|
+
|
|
21
|
+
repo_root="$(git rev-parse --show-toplevel 2>/dev/null)" || exit 0
|
|
22
|
+
cd "$repo_root" 2>/dev/null || exit 0
|
|
23
|
+
|
|
24
|
+
[ -x node_modules/.bin/commitlint ] || exit 0
|
|
25
|
+
|
|
26
|
+
node_modules/.bin/commitlint --edit "$msg_file"
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Вход в слой законов. SessionStart.
|
|
3
|
+
#
|
|
4
|
+
# Файл закона сам по себе не приносит в контекст ничего — его читают, только когда за ним
|
|
5
|
+
# пошли. Хук печатает указатель (имя файла и его заголовок) один раз за сессию: слой известен,
|
|
6
|
+
# что существует, и открывается, когда решение его задевает.
|
|
7
|
+
#
|
|
8
|
+
# Указатель ЧИТАЕТСЯ ИЗ КАТАЛОГА, а не выписан руками: выписанный разошёлся бы с тем, что
|
|
9
|
+
# разложено на самом деле, и сказать об этом было бы нечем.
|
|
10
|
+
#
|
|
11
|
+
# ОТКАЗ В ПОЛЬЗУ РАБОТЫ: любая ошибка начинает сессию без добавленного контекста (exit 0).
|
|
12
|
+
# Сломанный вход не имеет права остановить сессию.
|
|
13
|
+
|
|
14
|
+
dir="${CLAUDE_PROJECT_DIR:-.}/{{lawsDir}}"
|
|
15
|
+
[ -d "$dir" ] || exit 0
|
|
16
|
+
|
|
17
|
+
index=""
|
|
18
|
+
for law in "$dir"/*.md; do
|
|
19
|
+
[ -f "$law" ] || continue
|
|
20
|
+
title="$(grep -m1 '^# ' "$law" 2>/dev/null | sed 's/^# //')"
|
|
21
|
+
[ -z "$title" ] && continue
|
|
22
|
+
index="${index} {{lawsDir}}/$(basename "$law") — ${title}"$'\n'
|
|
23
|
+
done
|
|
24
|
+
[ -z "$index" ] && exit 0
|
|
25
|
+
|
|
26
|
+
read -r -d '' context <<EOF
|
|
27
|
+
ЗАКОНЫ ПРОЕКТА (слой @rt-tools/agent-kit, разложен в {{lawsDir}}/).
|
|
28
|
+
|
|
29
|
+
Закон говорит, ЧТО должно быть верно, и не знает ни путей, ни имён файлов. Правило — каким
|
|
30
|
+
приёмом это делается — живёт в {{rulesDir}}/, а имена этого дерева при нём — в
|
|
31
|
+
implementation.md рядом. Закон не отменяет правила и не заменяется им: перед решением, которое
|
|
32
|
+
закон задевает, читается закон целиком, правило — как обычно.
|
|
33
|
+
|
|
34
|
+
${index}
|
|
35
|
+
Разложенные файлы правятся не руками, а надстройкой в .claude/rt-kit/overrides/<ресурс>:
|
|
36
|
+
правка на месте теряется на следующем \`agent-kit sync\`, и он на неё отказывает. Сверить
|
|
37
|
+
разложенное с пакетом: \`agent-kit sync --check\`.
|
|
38
|
+
EOF
|
|
39
|
+
|
|
40
|
+
jq -n --arg c "$context" '{hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:$c}}' 2>/dev/null || exit 0
|
|
41
|
+
|
|
42
|
+
exit 0
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Гард второго сервера разработки. PreToolUse.
|
|
3
|
+
#
|
|
4
|
+
# Приложения уже подняты владельцем, и всякая проверка через браузер идёт туда. Второй
|
|
5
|
+
# экземпляр занимает лишний порт, отдаёт другую сборку и уводит разбор в сторону: расхождение
|
|
6
|
+
# между двумя серверами читается как дефект правки. Вдобавок сборка, запущенная между делом,
|
|
7
|
+
# гасит уже поднятый сервер молча.
|
|
8
|
+
#
|
|
9
|
+
# Отбивается всё, что ПОДНИМАЕТ сервер. Сборка, тесты, линтеры, запросы к поднятым портам и
|
|
10
|
+
# осмотр слушателей проходят.
|
|
11
|
+
#
|
|
12
|
+
# Где именно подняты приложения, знает профиль проекта: {{projectProfile}}, переменная
|
|
13
|
+
# RT_STANDS. Нет профиля — текст отказа остаётся общим, сам гард работает.
|
|
14
|
+
|
|
15
|
+
input="$(cat 2>/dev/null)"
|
|
16
|
+
|
|
17
|
+
tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
|
|
18
|
+
case "$tool" in
|
|
19
|
+
Bash | mcp__webstorm__execute_terminal_command | mcp__webstorm__execute_tool) ;;
|
|
20
|
+
# Готовая конфигурация запуска командной строки не показывает — видно только её имя.
|
|
21
|
+
# Отсюда правило: «serve», «dev» и «start» в имени отклоняются, потому что проверить, что
|
|
22
|
+
# за ними стоит, гард не может, а второй сервер стоит дороже лишнего отказа.
|
|
23
|
+
mcp__webstorm__execute_run_configuration)
|
|
24
|
+
name="$(printf '%s' "$input" | jq -r '.tool_input.configurationName // empty' 2>/dev/null)"
|
|
25
|
+
# Второй режим инструмента — временная конфигурация из пары «файл и строка»: имени у
|
|
26
|
+
# неё нет вовсе, и проверка по имени её пропускала. Именно так поднимается скрипт из
|
|
27
|
+
# манифеста пакета.
|
|
28
|
+
if [ -z "$name" ]; then
|
|
29
|
+
file="$(printf '%s' "$input" | jq -r '.tool_input.filePath // empty' 2>/dev/null)"
|
|
30
|
+
case "$file" in
|
|
31
|
+
*/package.json|package.json)
|
|
32
|
+
echo "Запуск скрипта прямо из манифеста: гард видит только файл и строку, а не сам скрипт, поэтому не может отличить подъём сервера от сборки. Приложения уже подняты владельцем — если нужна сборка или тест, запусти их командой в терминале." >&2
|
|
33
|
+
exit 2 ;;
|
|
34
|
+
esac
|
|
35
|
+
exit 0
|
|
36
|
+
fi
|
|
37
|
+
# Слово «start» без границы ловило и «restart», который сервер не поднимает.
|
|
38
|
+
case "$(printf '%s' "$name" | tr '[:upper:]' '[:lower:]')" in
|
|
39
|
+
*serve*|*dev*|start*|*\ start*|*:start*)
|
|
40
|
+
echo "Конфигурация «${name}» похожа на подъём сервера разработки, а приложения уже подняты владельцем — проверяй их. Если конфигурация делает другое, запусти это командой: по имени гард содержимого не видит." >&2
|
|
41
|
+
exit 2 ;;
|
|
42
|
+
esac
|
|
43
|
+
exit 0 ;;
|
|
44
|
+
*) exit 0 ;;
|
|
45
|
+
esac
|
|
46
|
+
|
|
47
|
+
cmd="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null)"
|
|
48
|
+
[ -z "$cmd" ] && exit 0
|
|
49
|
+
|
|
50
|
+
# Универсальный исполнитель среды передаёт настоящую команду вложенной строкой. Разбирать надо
|
|
51
|
+
# её, а не обёртку: иначе имя раннера стоит сразу за кавычкой и ни одно правило до него не
|
|
52
|
+
# дотягивается.
|
|
53
|
+
if [ "$tool" = "mcp__webstorm__execute_tool" ] && command -v perl >/dev/null 2>&1; then
|
|
54
|
+
inner="$(printf '%s' "$cmd" | perl -0ne '
|
|
55
|
+
if (/--command(?:=|\s+)(?:"((?:[^"\\]|\\.)*)"|\x27([^\x27]*)\x27|(.+))/s) {
|
|
56
|
+
print defined $1 ? $1 : (defined $2 ? $2 : $3);
|
|
57
|
+
}
|
|
58
|
+
' 2>/dev/null)"
|
|
59
|
+
[ -n "$inner" ] && cmd="$inner"
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
# Гит ничего не слушает на портах, а тексты сообщений и веток свободно содержат слова вроде
|
|
63
|
+
# «serve» — без этой ветки гард ловит собственный коммит про себя же.
|
|
64
|
+
case "$cmd" in
|
|
65
|
+
git\ *|*/git\ *)
|
|
66
|
+
printf '%s' "$cmd" | grep -qE '(^|[[:space:]])git[[:space:]]+daemon([[:space:]]|$)' || exit 0 ;;
|
|
67
|
+
esac
|
|
68
|
+
|
|
69
|
+
stands=""
|
|
70
|
+
profile="${CLAUDE_PROJECT_DIR:-.}/{{projectProfile}}"
|
|
71
|
+
if [ -f "$profile" ]; then
|
|
72
|
+
# shellcheck disable=SC1090
|
|
73
|
+
. "$profile" 2>/dev/null && stands="${RT_STANDS:-}"
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
deny() {
|
|
77
|
+
if [ -n "$stands" ]; then
|
|
78
|
+
echo "$1 Приложения уже подняты владельцем: ${stands} — проверяй их. Свой экземпляр не поднимай; если порт не отвечает, скажи владельцу, а не запускай второй." >&2
|
|
79
|
+
else
|
|
80
|
+
echo "$1 Приложения уже подняты владельцем — проверяй их. Свой экземпляр не поднимай; если порт не отвечает, скажи владельцу, а не запускай второй." >&2
|
|
81
|
+
fi
|
|
82
|
+
exit 2
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
# Раннеры перечисляются явно. Группа «любое слово перед именем» отклоняла даже заметку о том,
|
|
86
|
+
# что сервер поднимает владелец.
|
|
87
|
+
RUNNER='((npx|pnpm|yarn|bun|npm)([[:space:]]+(exec|run|dlx))?[[:space:]]+)?'
|
|
88
|
+
|
|
89
|
+
# Начало вызова: начало строки или разделитель команд. Кавычку в границы вносить нельзя — тогда
|
|
90
|
+
# поиск по тексту и снятие процесса по шаблону читаются как запуск.
|
|
91
|
+
BOUND='(^|[;&|(]|&&|\|\|)[[:space:]]*'
|
|
92
|
+
|
|
93
|
+
printf '%s' "$cmd" | grep -qE "${BOUND}${RUNNER}(nx|ng)[[:space:]]+(run[[:space:]]+[^[:space:]]*:)?(serve|dev)" \
|
|
94
|
+
&& deny "Запуск ещё одного сервера разработки через каркас."
|
|
95
|
+
|
|
96
|
+
# Требование пробела сразу после имени рвало совпадение на двоеточии: скрипты вида «serve:site»
|
|
97
|
+
# гард пропускал — то есть ровно те команды, ради которых написан.
|
|
98
|
+
printf '%s' "$cmd" | grep -qE "${BOUND}(npm|pnpm|yarn|bun)([[:space:]]+run)?[[:space:]]+(dev|start|serve)([:._-][A-Za-z0-9:._-]*)?([[:space:]]|\$)" \
|
|
99
|
+
&& deny "Запуск ещё одного сервера разработки через пакетный раннер."
|
|
100
|
+
|
|
101
|
+
# Подкоманда обязательна: пока она была необязательной, под правило попадало голое слово
|
|
102
|
+
# сборщика — то есть любой однострочник, где оно встречается внутри текста.
|
|
103
|
+
printf '%s' "$cmd" | grep -qE "${BOUND}${RUNNER}vite([[:space:]]+(dev|serve|preview))?[[:space:]]*(\$|[;&|\"'])" \
|
|
104
|
+
&& deny "Запуск ещё одного сервера разработки."
|
|
105
|
+
printf '%s' "$cmd" | grep -qE "${BOUND}${RUNNER}(next|astro|nuxt)[[:space:]]+(dev|start|preview)([[:space:]]|\$)" \
|
|
106
|
+
&& deny "Запуск ещё одного сервера разработки."
|
|
107
|
+
|
|
108
|
+
# Статика поверх сборки — тот же второй экземпляр. Каждое имя под общим якорем начала команды:
|
|
109
|
+
# без него перечисление пакетов и поиск по документам читались как запуск.
|
|
110
|
+
printf '%s' "$cmd" | grep -qE "${BOUND}(python3?[[:space:]]+-m[[:space:]]+http\.server|${RUNNER}(http-server|live-server|serve)([[:space:]]|\$))" \
|
|
111
|
+
&& deny "Подъём статического сервера поверх сборки — тот же второй экземпляр."
|
|
112
|
+
|
|
113
|
+
exit 0
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Гард пары «правка и её документ». PreToolUse.
|
|
3
|
+
#
|
|
4
|
+
# Расхождение кода с текстом беззвучно. Ни линтер, ни сборка, ни тесты не читают правила,
|
|
5
|
+
# спеки и README, поэтому текст, описывающий прежнее устройство, живёт дальше и выглядит
|
|
6
|
+
# действующей справкой — тем убедительнее, чем он старше. Ловится это только чтением, и ловит
|
|
7
|
+
# обычно владелец, а не проверка.
|
|
8
|
+
#
|
|
9
|
+
# Гард требует ровно тех пар, где связь механическая и спорить не о чем. Какие это пары, знает
|
|
10
|
+
# профиль проекта: {{projectProfile}}, функция `rt_docs_pair_for <файл>` — печатает образец
|
|
11
|
+
# пути, который обязан ехать тем же коммитом, или молчит.
|
|
12
|
+
#
|
|
13
|
+
# Отдельно — законы. Совпал ли код с законом, машина не знает, поэтому правка самого закона не
|
|
14
|
+
# отклоняется, а выносится вопросом владельцу: закон описывает договорённость о продукте, и
|
|
15
|
+
# менять её молча гард не даёт.
|
|
16
|
+
#
|
|
17
|
+
# Обход — строка `Docs-skip: <причина>` в теле коммита. Причина остаётся в истории и видна при
|
|
18
|
+
# разборе ветки; пустая не принимается.
|
|
19
|
+
#
|
|
20
|
+
# ОТКАЗ В ПОЛЬЗУ РАБОТЫ: не репозиторий, нет разборщика, битый ввод, пустой список файлов —
|
|
21
|
+
# пропуск.
|
|
22
|
+
|
|
23
|
+
input="$(cat 2>/dev/null)"
|
|
24
|
+
[ -z "$input" ] && exit 0
|
|
25
|
+
command -v jq >/dev/null 2>&1 || exit 0
|
|
26
|
+
|
|
27
|
+
tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
|
|
28
|
+
|
|
29
|
+
decide() {
|
|
30
|
+
jq -n --arg d "$1" --arg r "$2" \
|
|
31
|
+
'{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:$d,permissionDecisionReason:$r}}' 2>/dev/null \
|
|
32
|
+
|| printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Документ едет тем же коммитом."}}\n'
|
|
33
|
+
exit 0
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# ── Правка закона спрашивает владельца ────────────────────────────────────────
|
|
37
|
+
case "$tool" in
|
|
38
|
+
Edit | Write | MultiEdit | mcp__webstorm__create_new_file)
|
|
39
|
+
target="$(printf '%s' "$input" | jq -r '.tool_input.file_path // .tool_input.pathInProject // empty' 2>/dev/null)"
|
|
40
|
+
case "$target" in
|
|
41
|
+
*/{{lawsDir}}/*.md | {{lawsDir}}/*.md)
|
|
42
|
+
decide ask "Правка закона: \`${target##*/}\`. Закон описывает договорённость о продукте, а не устройство кода, — назови владельцу, что и почему меняешь, и дождись ответа. Если правка уже согласована, подтверди вызов." ;;
|
|
43
|
+
esac
|
|
44
|
+
exit 0 ;;
|
|
45
|
+
esac
|
|
46
|
+
|
|
47
|
+
# ── Коммит: пара обязана ехать тем же коммитом ───────────────────────────────
|
|
48
|
+
case "$tool" in
|
|
49
|
+
Bash | mcp__webstorm__execute_terminal_command | mcp__webstorm__execute_tool) ;;
|
|
50
|
+
*) exit 0 ;;
|
|
51
|
+
esac
|
|
52
|
+
|
|
53
|
+
cmd="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null)"
|
|
54
|
+
case "$cmd" in
|
|
55
|
+
*git\ commit*) ;;
|
|
56
|
+
*) exit 0 ;;
|
|
57
|
+
esac
|
|
58
|
+
|
|
59
|
+
# Обход с причиной. Пустая причина не принимается: «Docs-skip:» без слов означает только то,
|
|
60
|
+
# что строку дописали, чтобы пройти гард.
|
|
61
|
+
if printf '%s' "$cmd" | grep -qE 'Docs-skip:[[:space:]]*[^[:space:]]'; then
|
|
62
|
+
exit 0
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
workdir="$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null)"
|
|
66
|
+
[ -z "$workdir" ] && workdir="${CLAUDE_PROJECT_DIR:-.}"
|
|
67
|
+
cd "$workdir" 2>/dev/null || exit 0
|
|
68
|
+
git rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0
|
|
69
|
+
|
|
70
|
+
profile="${CLAUDE_PROJECT_DIR:-.}/{{projectProfile}}"
|
|
71
|
+
[ -f "$profile" ] || exit 0
|
|
72
|
+
# shellcheck disable=SC1090
|
|
73
|
+
. "$profile" 2>/dev/null || exit 0
|
|
74
|
+
command -v rt_docs_pair_for >/dev/null 2>&1 || exit 0
|
|
75
|
+
|
|
76
|
+
staged="$(git diff --cached --name-only 2>/dev/null)"
|
|
77
|
+
[ -z "$staged" ] && exit 0
|
|
78
|
+
|
|
79
|
+
missing=""
|
|
80
|
+
while IFS= read -r file; do
|
|
81
|
+
[ -z "$file" ] && continue
|
|
82
|
+
want="$(rt_docs_pair_for "$file" 2>/dev/null)"
|
|
83
|
+
[ -z "$want" ] && continue
|
|
84
|
+
# Пара считается приехавшей, если хоть один подготовленный файл подходит под образец.
|
|
85
|
+
printf '%s\n' "$staged" | grep -qE "$want" && continue
|
|
86
|
+
missing="${missing} ${file} — ждёт документ: ${want}"$'\n'
|
|
87
|
+
done <<EOF
|
|
88
|
+
$staged
|
|
89
|
+
EOF
|
|
90
|
+
|
|
91
|
+
[ -z "$missing" ] && exit 0
|
|
92
|
+
|
|
93
|
+
decide deny "Документ едет в том же коммите, что и правка, которую он описывает. Не хватает пар:
|
|
94
|
+
|
|
95
|
+
${missing}
|
|
96
|
+
Если документа здесь правда не нужно — строка «Docs-skip: <причина>» в теле коммита; пустая причина не принимается."
|