@rt-tools/agent-kit 0.1.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.
Files changed (131) hide show
  1. package/README.md +88 -9
  2. package/assets/hooks/browser-device-id.sh +20 -0
  3. package/assets/hooks/browser-guard-device-id.sh +27 -0
  4. package/assets/hooks/browser-guard-no-listing.sh +17 -0
  5. package/assets/hooks/browser-guard-no-other-drivers.sh +78 -0
  6. package/assets/hooks/browser-guard-require-select.sh +53 -0
  7. package/assets/hooks/commit-msg.sh +26 -0
  8. package/assets/hooks/constitution-index.sh +42 -0
  9. package/assets/hooks/dev-server-guard.sh +113 -0
  10. package/assets/hooks/docs-guard.sh +96 -0
  11. package/assets/hooks/git-guard-delivery.sh +110 -0
  12. package/assets/hooks/git-guard-main.sh +72 -0
  13. package/assets/hooks/git-guard-push-tests.sh +73 -0
  14. package/assets/hooks/lint-after-edit.sh +94 -0
  15. package/assets/hooks/qa-dataid-guard.sh +81 -0
  16. package/assets/hooks/reuse-first-guard.sh +83 -0
  17. package/assets/hooks/skill-gate-rearm.sh +22 -0
  18. package/assets/hooks/skill-gate.sh +68 -0
  19. package/assets/hooks/skill-loaded.sh +20 -0
  20. package/assets/hooks/sql-guard.sh +129 -0
  21. package/assets/laws/access.md +3 -12
  22. package/assets/laws/admin-lists.md +9 -21
  23. package/assets/laws/admin-navigation.md +3 -15
  24. package/assets/laws/code-structure.md +5 -16
  25. package/assets/laws/delivery.md +2 -16
  26. package/assets/laws/entity-editing.md +7 -20
  27. package/assets/laws/entity-models.md +10 -17
  28. package/assets/laws/frontend-application.md +3 -14
  29. package/assets/laws/lib-imports.md +0 -13
  30. package/assets/laws/locales.md +2 -11
  31. package/assets/laws/project-documentation.md +7 -20
  32. package/assets/laws/reuse-first.md +0 -9
  33. package/assets/laws/search-visibility.md +0 -13
  34. package/assets/laws/shared-code.md +0 -13
  35. package/assets/laws/verifiability.md +0 -13
  36. package/assets/patterns/angular-patterns-state.md +94 -0
  37. package/assets/patterns/api-layer-pair.md +78 -0
  38. package/assets/patterns/browser-verification-measure.md +83 -0
  39. package/assets/patterns/browser-verification-stand.md +79 -0
  40. package/assets/patterns/component-structure-new.md +98 -0
  41. package/assets/patterns/doc-style-sweep.md +100 -0
  42. package/assets/patterns/doc-style-write.md +106 -0
  43. package/assets/patterns/git-workflow-commit.md +175 -0
  44. package/assets/patterns/git-workflow-merge.md +82 -0
  45. package/assets/patterns/git-workflow-migration.md +58 -0
  46. package/assets/patterns/git-workflow-restart.md +49 -0
  47. package/assets/patterns/lib-layers-move.md +77 -0
  48. package/assets/patterns/lib-layers-new.md +70 -0
  49. package/assets/patterns/permissions-procedure.md +69 -0
  50. package/assets/patterns/platform-access-di.md +70 -0
  51. package/assets/patterns/reuse-first-extend.md +73 -0
  52. package/assets/patterns/seo-page.md +92 -0
  53. package/assets/patterns/seo-verify.md +64 -0
  54. package/assets/patterns/shared-code-new.md +80 -0
  55. package/assets/patterns/spec-driven-domain.md +100 -0
  56. package/assets/patterns/spec-driven-rule.md +112 -0
  57. package/assets/patterns/styling-bem-component.md +77 -0
  58. package/assets/patterns/styling-bem-layout.md +67 -0
  59. package/assets/patterns/testing-e2e.md +90 -0
  60. package/assets/patterns/testing-unit.md +93 -0
  61. package/assets/patterns/translations-key.md +51 -0
  62. package/assets/patterns/ts-procedure.md +66 -0
  63. package/assets/rules/angular-patterns.md +52 -0
  64. package/assets/rules/api-layer.md +53 -0
  65. package/assets/rules/browser-verification.md +69 -0
  66. package/assets/rules/component-structure.md +48 -0
  67. package/assets/rules/doc-style.md +61 -0
  68. package/assets/rules/git-workflow.md +106 -0
  69. package/assets/rules/lib-layers.md +54 -0
  70. package/assets/rules/permissions.md +52 -0
  71. package/assets/rules/platform-access.md +49 -0
  72. package/assets/rules/reuse-first.md +69 -0
  73. package/assets/rules/seo.md +50 -0
  74. package/assets/rules/shared-code.md +45 -0
  75. package/assets/rules/spec-driven.md +89 -0
  76. package/assets/rules/styling-bem.md +59 -0
  77. package/assets/rules/testing.md +69 -0
  78. package/assets/rules/translations.md +52 -0
  79. package/assets/rules/typescript-conventions.md +46 -0
  80. package/assets/templates/gate-map.sh +37 -0
  81. package/assets/templates/implementation.md +38 -0
  82. package/assets/templates/pattern.md +4 -0
  83. package/assets/templates/project.sh +41 -0
  84. package/assets/templates/rule.md +12 -23
  85. package/bin/agent-kit.d.ts +1 -1
  86. package/bin/agent-kit.d.ts.map +1 -1
  87. package/bin/agent-kit.js +63 -7
  88. package/bin/agent-kit.js.map +1 -1
  89. package/bin/prompt.d.ts +9 -0
  90. package/bin/prompt.d.ts.map +1 -0
  91. package/bin/prompt.js +57 -0
  92. package/bin/prompt.js.map +1 -0
  93. package/index.d.ts +2 -0
  94. package/index.d.ts.map +1 -1
  95. package/index.js +2 -0
  96. package/index.js.map +1 -1
  97. package/lib/assets.d.ts +10 -2
  98. package/lib/assets.d.ts.map +1 -1
  99. package/lib/assets.js +24 -28
  100. package/lib/assets.js.map +1 -1
  101. package/lib/catalog.d.ts +44 -0
  102. package/lib/catalog.d.ts.map +1 -0
  103. package/lib/catalog.js +86 -0
  104. package/lib/catalog.js.map +1 -0
  105. package/lib/commands.d.ts +13 -1
  106. package/lib/commands.d.ts.map +1 -1
  107. package/lib/commands.js +106 -11
  108. package/lib/commands.js.map +1 -1
  109. package/lib/companion.d.ts +53 -0
  110. package/lib/companion.d.ts.map +1 -0
  111. package/lib/companion.js +33 -0
  112. package/lib/companion.js.map +1 -0
  113. package/lib/config.d.ts +29 -1
  114. package/lib/config.d.ts.map +1 -1
  115. package/lib/config.js +43 -6
  116. package/lib/config.js.map +1 -1
  117. package/lib/picker.d.ts +47 -0
  118. package/lib/picker.d.ts.map +1 -0
  119. package/lib/picker.js +112 -0
  120. package/lib/picker.js.map +1 -0
  121. package/lib/stamp.d.ts +2 -5
  122. package/lib/stamp.d.ts.map +1 -1
  123. package/lib/stamp.js +25 -10
  124. package/lib/stamp.js.map +1 -1
  125. package/lib/sync.d.ts +3 -0
  126. package/lib/sync.d.ts.map +1 -1
  127. package/lib/sync.js +20 -1
  128. package/lib/sync.js.map +1 -1
  129. package/package.json +1 -1
  130. package/rt-tools-agent-kit-0.3.0.tgz +0 -0
  131. package/rt-tools-agent-kit-0.1.0.tgz +0 -0
package/README.md CHANGED
@@ -9,19 +9,21 @@
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
 
20
21
  ```bash
21
22
  pnpm add -D @rt-tools/agent-kit
22
23
 
23
- npx agent-kit init # завести .claude/rt-kit.json и каталог надстроек
24
- npx agent-kit sync # разложить законы в docs/constitution/
24
+ npx agent-kit list # что везёт пакет и что из этого взято здесь
25
+ npx agent-kit init # спросить законы галочками и завести .claude/rt-kit.json
26
+ npx agent-kit sync # разложить выбранное в docs/constitution/
25
27
  npx agent-kit doctor # что разложено, что отстало, чего не хватает
26
28
  ```
27
29
 
@@ -34,6 +36,76 @@ npx agent-kit sync --check
34
36
  Команда ничего не пишет и отказывает, если разложенное отстало от пакета или его правили
35
37
  руками.
36
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
+
89
+ ## Выбор законов
90
+
91
+ Половина законов пакета про то, чего в конкретном приложении нет вовсе: у сервиса без админки
92
+ нет ни её списков, ни её навигации. Поэтому `init` спрашивает, какие законы брать, а не кладёт
93
+ все пятнадцать молча.
94
+
95
+ ```bash
96
+ npx agent-kit init # спросить галочками
97
+ npx agent-kit init --all # взять все, ни о чём не спрашивая
98
+ npx agent-kit init --laws access,delivery,verifiability
99
+ ```
100
+
101
+ Без терминала — в CI, в конвейере, в чужом скрипте — спрашивать некого, и `init` без флага
102
+ отказывает вместо того, чтобы решить за проект: «взять всё» тоже решение, и принимать его
103
+ молча он не вправе.
104
+
105
+ Выбор уезжает в конфиг ключом `only` и оттуда же правится потом: `init` заведённый конфиг не
106
+ трогает. Имена законов — из `agent-kit list`, там же видно, что взято, что не выбрано и что
107
+ пропущено.
108
+
37
109
  ## Конфиг
38
110
 
39
111
  `.claude/rt-kit.json` коммитится: раскладка обязана повторяться на чужой машине без вопросов.
@@ -42,6 +114,7 @@ npx agent-kit sync --check
42
114
  {
43
115
  "vars": { "mainBranch": "main" },
44
116
  "layout": { "laws": "docs/constitution" },
117
+ "only": ["laws/access.md", "laws/delivery.md"],
45
118
  "skip": ["laws/admin-lists.md"]
46
119
  }
47
120
  ```
@@ -52,7 +125,11 @@ npx agent-kit sync --check
52
125
  посреди правила агент прочтёт как имя.
53
126
  - **`layout`** — куда класть каждый род ресурса. Умолчания менять без нужды не стоит: правила
54
127
  ссылаются на законы теми же путями.
55
- - **`skip`** — ресурсы, от которых проект отказался, идентификаторами вида `laws/<закон>.md`.
128
+ - **`only`** — что проект выбрал, идентификаторами вида `laws/<закон>.md`. Пусто — берётся всё.
129
+ Ограничивает только те роды, которые сам называет: перечислив законы, проект говорит о
130
+ законах, а не обо всём, что пакет везёт, — шаблоны при нём остаются.
131
+ - **`skip`** — ресурсы, от которых проект отказался, теми же идентификаторами. Вычитает из
132
+ выбранного, поэтому отказ от одного закона не требует переписывать список из пятнадцати.
56
133
 
57
134
  ## Надстройки
58
135
 
@@ -81,5 +158,7 @@ npx agent-kit sync --check
81
158
 
82
159
  ## Чего в пакете пока нет
83
160
 
84
- Хуков, проверок, агентов и пресетов карты гейта он не везёт только законы и шаблоны правила и
85
- паттерна. Генератора черновиков правил по закону тоже нет.
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: <причина>» в теле коммита; пустая причина не принимается."