@rt-tools/agent-kit 0.2.0 → 0.4.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 (203) hide show
  1. package/README.md +235 -18
  2. package/assets/agents/business-analyst.md +74 -0
  3. package/assets/agents/project-manager.md +70 -0
  4. package/assets/agents/qa-engineer.md +72 -0
  5. package/assets/agents/skill-curator.md +110 -0
  6. package/assets/agents/spec-critic.md +44 -0
  7. package/assets/agents/spec-writer.md +50 -0
  8. package/assets/checks/board.github.mjs +286 -0
  9. package/assets/checks/check-board.github.mjs +188 -0
  10. package/assets/checks/check-doc-paths.mjs +163 -0
  11. package/assets/checks/check-dupes.mjs +277 -0
  12. package/assets/checks/check-lib-layers.mjs +573 -0
  13. package/assets/checks/check-reuse.mjs +208 -0
  14. package/assets/checks/check-schema-drift.mjs +186 -0
  15. package/assets/checks/check-specs.mjs +1007 -0
  16. package/assets/checks/check-styles.mjs +109 -0
  17. package/assets/checks/rt-kit-checks.config.mjs +134 -0
  18. package/assets/checks/task-new.github.mjs +198 -0
  19. package/assets/commands/skill-curator.md +70 -0
  20. package/assets/defaults/gate-map.sh +100 -0
  21. package/assets/defaults/project.sh +179 -0
  22. package/assets/hooks/browser-device-id.sh +20 -0
  23. package/assets/hooks/browser-guard-device-id.sh +28 -0
  24. package/assets/hooks/browser-guard-no-asking.sh +27 -0
  25. package/assets/hooks/browser-guard-no-listing.sh +18 -0
  26. package/assets/hooks/browser-guard-no-other-drivers.sh +79 -0
  27. package/assets/hooks/browser-guard-require-select.sh +54 -0
  28. package/assets/hooks/commit-msg.sh +26 -0
  29. package/assets/hooks/constitution-index.sh +43 -0
  30. package/assets/hooks/dev-server-guard.sh +115 -0
  31. package/assets/hooks/docs-guard.sh +282 -0
  32. package/assets/hooks/git-guard-delivery.sh +167 -0
  33. package/assets/hooks/git-guard-main.sh +73 -0
  34. package/assets/hooks/git-guard-push-tests.sh +94 -0
  35. package/assets/hooks/glossary-load.sh +23 -0
  36. package/assets/hooks/lint-after-edit.sh +219 -0
  37. package/assets/hooks/qa-dataid-guard.sh +121 -0
  38. package/assets/hooks/reuse-first-guard.sh +154 -0
  39. package/assets/hooks/skill-gate-rearm.sh +23 -0
  40. package/assets/hooks/skill-gate.sh +128 -0
  41. package/assets/hooks/skill-loaded.sh +21 -0
  42. package/assets/hooks/sql-guard.sh +679 -0
  43. package/assets/hooks/task-context-load.sh +100 -0
  44. package/assets/hooks/task-flow-guard.sh +107 -0
  45. package/assets/laws/{access.md → application/access.md} +1 -4
  46. package/assets/laws/{locales.md → application/locales.md} +1 -3
  47. package/assets/laws/application/money.md +41 -0
  48. package/assets/laws/application/ownership.md +32 -0
  49. package/assets/laws/{search-visibility.md → application/search-visibility.md} +1 -1
  50. package/assets/laws/code-structure.md +7 -6
  51. package/assets/laws/delivery.md +53 -3
  52. package/assets/laws/entity-editing.md +49 -55
  53. package/assets/laws/entity-models.md +4 -14
  54. package/assets/laws/frontend-application.md +5 -5
  55. package/assets/laws/lib-imports.md +14 -1
  56. package/assets/laws/lists.md +33 -0
  57. package/assets/laws/navigation.md +40 -0
  58. package/assets/laws/project-documentation.md +17 -8
  59. package/assets/laws/reuse-first.md +26 -21
  60. package/assets/laws/shared-code.md +13 -1
  61. package/assets/laws/verifiability.md +17 -1
  62. package/assets/laws/work-conduct.md +48 -0
  63. package/assets/patterns/admin-lists-screen.md +131 -0
  64. package/assets/patterns/admin-nav-item.md +71 -0
  65. package/assets/patterns/angular-patterns-state.md +101 -0
  66. package/assets/patterns/api-layer-pair.md +88 -0
  67. package/assets/patterns/browser-verification-measure.md +86 -0
  68. package/assets/patterns/browser-verification-stand.md +143 -0
  69. package/assets/patterns/component-structure-new.md +99 -0
  70. package/assets/patterns/dependencies-upgrade.md +65 -0
  71. package/assets/patterns/doc-style-sweep.md +137 -0
  72. package/assets/patterns/doc-style-write.md +109 -0
  73. package/assets/patterns/entity-aside.md +136 -0
  74. package/assets/patterns/entity-models-new.md +124 -0
  75. package/assets/patterns/entity-store.md +91 -0
  76. package/assets/patterns/git-workflow-commit.azure.md +259 -0
  77. package/assets/patterns/git-workflow-commit.github.md +333 -0
  78. package/assets/patterns/git-workflow-commit.gitlab.md +283 -0
  79. package/assets/patterns/git-workflow-merge.md +99 -0
  80. package/assets/patterns/git-workflow-migration.md +88 -0
  81. package/assets/patterns/git-workflow-restart.md +49 -0
  82. package/assets/patterns/lib-layers-move.md +95 -0
  83. package/assets/patterns/lib-layers-new.md +82 -0
  84. package/assets/patterns/ownership-scope-resolve.md +69 -0
  85. package/assets/patterns/permissions-procedure.md +71 -0
  86. package/assets/patterns/platform-access-di.md +84 -0
  87. package/assets/patterns/pricing-quote.md +71 -0
  88. package/assets/patterns/reuse-first-extend.md +73 -0
  89. package/assets/patterns/seo-page.md +104 -0
  90. package/assets/patterns/seo-verify.md +83 -0
  91. package/assets/patterns/shared-code-new.md +86 -0
  92. package/assets/patterns/spec-driven-domain.md +107 -0
  93. package/assets/patterns/spec-driven-rule.md +127 -0
  94. package/assets/patterns/styling-bem-component.md +88 -0
  95. package/assets/patterns/styling-bem-layout.md +73 -0
  96. package/assets/patterns/task-flow-close.md +90 -0
  97. package/assets/patterns/task-flow-resume.md +94 -0
  98. package/assets/patterns/task-flow-start.md +117 -0
  99. package/assets/patterns/testing-e2e.md +92 -0
  100. package/assets/patterns/testing-unit.md +117 -0
  101. package/assets/patterns/translations-key.md +64 -0
  102. package/assets/patterns/ts-procedure.md +65 -0
  103. package/assets/rules/angular-patterns.md +71 -0
  104. package/assets/rules/api-layer.md +71 -0
  105. package/assets/rules/browser-verification.md +87 -0
  106. package/assets/rules/component-structure.md +64 -0
  107. package/assets/rules/dependencies.md +66 -0
  108. package/assets/rules/doc-style.md +103 -0
  109. package/assets/rules/entity-conventions.md +78 -0
  110. package/assets/rules/entity-models.md +70 -0
  111. package/assets/rules/git-workflow.azure.md +116 -0
  112. package/assets/rules/git-workflow.github.md +123 -0
  113. package/assets/rules/git-workflow.gitlab.md +113 -0
  114. package/assets/rules/lib-layers.md +80 -0
  115. package/assets/rules/lists.md +73 -0
  116. package/assets/rules/navigation.md +78 -0
  117. package/assets/rules/ownership-scope.md +63 -0
  118. package/assets/rules/permissions.md +70 -0
  119. package/assets/rules/platform-access.md +77 -0
  120. package/assets/rules/pricing.md +64 -0
  121. package/assets/rules/reuse-first.md +83 -0
  122. package/assets/rules/seo.md +71 -0
  123. package/assets/rules/shared-code.md +70 -0
  124. package/assets/rules/spec-driven.md +135 -0
  125. package/assets/rules/styling-bem.md +74 -0
  126. package/assets/rules/task-flow.md +110 -0
  127. package/assets/rules/testing.md +100 -0
  128. package/assets/rules/translations.md +69 -0
  129. package/assets/rules/typescript-conventions.md +76 -0
  130. package/assets/skills/agent-kit.md +81 -0
  131. package/assets/skills/write-a-skill.md +108 -0
  132. package/assets/templates/gate-map.sh +45 -0
  133. package/assets/templates/implementation.md +44 -0
  134. package/assets/templates/pattern.md +5 -1
  135. package/assets/templates/project.sh +54 -0
  136. package/assets/templates/rule.md +12 -23
  137. package/assets/variants.json +20 -0
  138. package/assets/workflows/feature.js +134 -0
  139. package/assets/workflows/plan.js +150 -0
  140. package/bin/agent-kit.d.ts.map +1 -1
  141. package/bin/agent-kit.js +78 -5
  142. package/bin/agent-kit.js.map +1 -1
  143. package/bin/prompt.d.ts +5 -0
  144. package/bin/prompt.d.ts.map +1 -1
  145. package/bin/prompt.js +19 -7
  146. package/bin/prompt.js.map +1 -1
  147. package/index.d.ts +1 -0
  148. package/index.d.ts.map +1 -1
  149. package/index.js +1 -0
  150. package/index.js.map +1 -1
  151. package/lib/assets.d.ts +14 -1
  152. package/lib/assets.d.ts.map +1 -1
  153. package/lib/assets.js +23 -2
  154. package/lib/assets.js.map +1 -1
  155. package/lib/catalog.d.ts +52 -5
  156. package/lib/catalog.d.ts.map +1 -1
  157. package/lib/catalog.js +104 -16
  158. package/lib/catalog.js.map +1 -1
  159. package/lib/commands.d.ts +22 -1
  160. package/lib/commands.d.ts.map +1 -1
  161. package/lib/commands.js +218 -11
  162. package/lib/commands.js.map +1 -1
  163. package/lib/companion.d.ts +57 -0
  164. package/lib/companion.d.ts.map +1 -0
  165. package/lib/companion.js +60 -0
  166. package/lib/companion.js.map +1 -0
  167. package/lib/config.d.ts +42 -2
  168. package/lib/config.d.ts.map +1 -1
  169. package/lib/config.js +60 -2
  170. package/lib/config.js.map +1 -1
  171. package/lib/freshness.d.ts +14 -0
  172. package/lib/freshness.d.ts.map +1 -0
  173. package/lib/freshness.js +116 -0
  174. package/lib/freshness.js.map +1 -0
  175. package/lib/hooks-map.d.ts +24 -0
  176. package/lib/hooks-map.d.ts.map +1 -0
  177. package/lib/hooks-map.js +72 -0
  178. package/lib/hooks-map.js.map +1 -0
  179. package/lib/integrity.d.ts +36 -0
  180. package/lib/integrity.d.ts.map +1 -0
  181. package/lib/integrity.js +44 -0
  182. package/lib/integrity.js.map +1 -0
  183. package/lib/picker.d.ts +11 -1
  184. package/lib/picker.d.ts.map +1 -1
  185. package/lib/picker.js +44 -6
  186. package/lib/picker.js.map +1 -1
  187. package/lib/stamp.d.ts +2 -5
  188. package/lib/stamp.d.ts.map +1 -1
  189. package/lib/stamp.js +25 -10
  190. package/lib/stamp.js.map +1 -1
  191. package/lib/sync.d.ts +29 -0
  192. package/lib/sync.d.ts.map +1 -1
  193. package/lib/sync.js +78 -4
  194. package/lib/sync.js.map +1 -1
  195. package/lib/variants.d.ts +44 -0
  196. package/lib/variants.d.ts.map +1 -0
  197. package/lib/variants.js +82 -0
  198. package/lib/variants.js.map +1 -0
  199. package/package.json +1 -1
  200. package/rt-tools-agent-kit-0.4.0.tgz +0 -0
  201. package/assets/laws/admin-lists.md +0 -35
  202. package/assets/laws/admin-navigation.md +0 -38
  203. package/rt-tools-agent-kit-0.2.0.tgz +0 -0
@@ -0,0 +1,179 @@
1
+ #!/usr/bin/env bash
2
+ # Профиль дерева: чем здесь проверяется правка и что считается переизобретением.
3
+ #
4
+ # Умолчание пакета. Всё, что общего у деревьев этой мастерской, живёт здесь: запускатель Nx,
5
+ # линтер кода, линтер стилей, набор образцов переизобретения. Своё дерево дописывает
6
+ # надстройкой — `.claude/rt-kit/project.sh`: она грузится второй, объявляет ту же функцию
7
+ # заново и вправе позвать отсюда её же с суффиксом `_default`.
8
+ #
9
+ # Каждая функция вправе промолчать. Молчание значит «правила на это нет», и хук пропускает.
10
+
11
+ # Запускатель пакетов — по локфайлу, а не по договорённости: деревья с pnpm и с npm лежат в
12
+ # одной мастерской, и зашитый здесь `npm` в первом же из них печатал бы команду, которой нет.
13
+ rt_runner() {
14
+ root="${CLAUDE_PROJECT_DIR:-.}"
15
+ if [ -f "$root/pnpm-lock.yaml" ]; then
16
+ printf '%s' 'pnpm exec'
17
+ elif [ -f "$root/yarn.lock" ]; then
18
+ printf '%s' 'yarn'
19
+ else
20
+ printf '%s' 'npx'
21
+ fi
22
+ }
23
+
24
+ # Где подняты приложения. Идёт в текст отказа, когда кто-то поднимает второй экземпляр.
25
+ # Порты у каждого дерева свои, поэтому умолчание молчит: назвать чужой порт хуже, чем не назвать.
26
+ RT_STANDS="${RT_STANDS:-}"
27
+
28
+ # Команды, которые обязаны пройти перед пушем. По одной на строку; первая упавшая отбивает пуш.
29
+ # Линтер стилей отдельной строкой: линтер кода файлы стилей не читает вовсе.
30
+ #
31
+ # Первый параметр — база: ветка, относительно которой считается вклад. Пустая означает, что
32
+ # удалённого нет, и тогда гоняется всё: набор строже нужного безопасен, набор уже нужного — нет.
33
+ rt_push_checks_default() {
34
+ runner="$(rt_runner)"
35
+ if [ -n "$1" ]; then
36
+ printf '%s\n' "$runner nx affected -t lint test --base=$1"
37
+ else
38
+ printf '%s\n' "$runner nx run-many -t lint test --all"
39
+ fi
40
+ [ -f "${CLAUDE_PROJECT_DIR:-.}/stylelint.config.js" ] && printf '%s\n' "$runner stylelint \"**/*.scss\" --max-warnings 0"
41
+
42
+ return 0
43
+ }
44
+
45
+ # Какой документ обязан ехать тем же коммитом, что и этот файл. Печатает образец пути или молчит.
46
+ rt_docs_pair_for_default() {
47
+ case "$1" in
48
+ # Тест — не описание кода: он его проверяет.
49
+ *.spec.ts) return 0 ;;
50
+ # Контракт правится вместе со спеком домена: разошедшись, они врут оба.
51
+ *.proto) printf '%s' 'docs/specs/.*/spec\.md' ;;
52
+ # Правило и его спутник здесь не называются: их пару держит сам гард, и держит точнее —
53
+ # он сверяет раздел с утверждениями, а не факт правки файла.
54
+ # Хук и набор его сценариев: гард разбирает текст команд, и уточнение шаблона обычно
55
+ # смещает границу, а не сужает.
56
+ .claude/hooks/*.sh) printf '%s' '.claude/hooks/tests/.*' ;;
57
+ esac
58
+ }
59
+
60
+ # Чем линтуется этот файл сразу после правки. Печатает команду или молчит.
61
+ #
62
+ # Путь подставляется здесь, а не оставляется позиционным параметром: хук исполняет напечатанное
63
+ # вычислением строки, и `$1` в нём разрешился бы в параметр самого хука, то есть в пустоту.
64
+ rt_lint_for_default() {
65
+ runner="$(rt_runner)"
66
+ case "$1" in
67
+ *.scss) printf '%s stylelint --max-warnings 0 "%s"' "$runner" "$1" ;;
68
+ *.ts | *.html) printf '%s eslint "%s"' "$runner" "$1" ;;
69
+ esac
70
+ }
71
+
72
+ # Каталог папок задач. Пусто — ведения работы папкой в дереве нет, и гард замысла молчит.
73
+ RT_TASKS_DIR="${RT_TASKS_DIR:-docs/tasks}"
74
+
75
+ # Код приложения ли это. Успех — да, и тогда правка требует замысла на диске.
76
+ #
77
+ # Признак — путь, а не оценка на глаз: оценку назначает тот, кому она мешает, и порог плывёт.
78
+ # Правила, тексты, обвязка и зависимости под требование не попадают — иначе разбор задачи
79
+ # нельзя было бы вести до заведения ветки.
80
+ rt_is_app_code_default() {
81
+ case "$1" in
82
+ */apps/* | */libs/* | */projects/*) return 0 ;;
83
+ *) return 1 ;;
84
+ esac
85
+ }
86
+
87
+ # Имя ветки, с которой разрешено открывать заявку на слияние: в имени стоит номер задачи.
88
+ # Приставка — либо род правки, либо метка очереди работ: обе формы носят номер, а он и нужен.
89
+ #
90
+ # Хвост после номера — строчными и через дефис. Имя ветки читают глазами в списке и набирают
91
+ # руками, а регистр в нём различает две ветки, отличающиеся только им, — и та, что нужна, при
92
+ # переключении находится не с первого раза.
93
+ rt_task_branch_ok_default() {
94
+ printf '%s' "$1" \
95
+ | grep -qE '^([A-Z]+-)?[0-9]+-[a-z0-9][a-z0-9-]*$|^(feat|fix|refactor|docs|chore|style|perf|test|build|ci)/[0-9]+-[a-z0-9][a-z0-9-]*$'
96
+ }
97
+
98
+ # Адрес боевого хранилища: образец, по которому его узнают в командной строке — порт туннеля,
99
+ # хост, домен. Любая запись по нему отбивается совсем, и опт-аут не действует. Умолчание
100
+ # молчит: чужой адрес здесь опаснее неназванного — он отбивал бы работу с локальной базой.
101
+ RT_PROD_DSN="${RT_PROD_DSN:-}"
102
+
103
+ # Подключения среды разработки в лицо: по одному тексту запроса бой от локальной копии не
104
+ # отличить, база выбирается идентификатором подключения. Идентификаторы локальны для машины.
105
+ RT_PROD_CONNECTIONS="${RT_PROD_CONNECTIONS:-}"
106
+ RT_LOCAL_CONNECTIONS="${RT_LOCAL_CONNECTIONS:-}"
107
+
108
+ # Порты одноразовых баз: по ним вопрос не задаётся. Не назван — исключения нет вовсе.
109
+ RT_SCRATCH_PORT_RE="${RT_SCRATCH_PORT_RE:-}"
110
+
111
+ # Форма номера в заголовке заявки на слияние. Тот же номер стоит у задачи и у ветки.
112
+ RT_TASK_TITLE_RE="${RT_TASK_TITLE_RE:-^\[[A-Za-z]+-[0-9]+\][[:space:]]+[^[:space:]]}"
113
+
114
+ # Чем заводится задача и чем сверяется очередь работ. Идут в текст отказа гарда поставки:
115
+ # отказ без действия обходят, а не исполняют.
116
+ RT_TASK_NEW_CMD="${RT_TASK_NEW_CMD:-npm run task:new}"
117
+ RT_BOARD_CHECK_CMD="${RT_BOARD_CHECK_CMD:-npm run check:board}"
118
+
119
+ # Учётная запись, которую ставят исполнителем. Умолчание молчит: у каждого дерева она своя.
120
+ RT_TASK_BOT="${RT_TASK_BOT:-}"
121
+
122
+ # Состояние задачи одним объектом: exists, open, onBoard, assigned, numbered. Спрашивает
123
+ # помощника очереди работ — того же, которым пользуются сверка и команда заведения, чтобы
124
+ # гард и очередь одинаково понимали «задача в порядке». Нет узла, нет помощника, нет сети —
125
+ # молчание, и ярус состояния пропускается: проверять нечем.
126
+ rt_task_state_default() {
127
+ command -v node >/dev/null 2>&1 || return 1
128
+ [ -f "${RT_BOARD_HELPER:-tools/board.mjs}" ] || return 1
129
+ state="$(node "${RT_BOARD_HELPER:-tools/board.mjs}" task "$1" 2>/dev/null)" || return 1
130
+ [ -z "$state" ] && return 1
131
+ # Ответ с меткой «сети не было» состоянием не является: по нему нельзя отличить задачу,
132
+ # которой нет, от задачи, о которой не спросили.
133
+ printf '%s' "$state" | jq -e 'has("offline") | not' >/dev/null 2>&1 || return 1
134
+ printf '%s' "$state"
135
+ }
136
+
137
+ # Что в этом дереве считается переизобретением. По строке «образец<таб>чем заменить».
138
+ # Образцы узкие намеренно: гард сверяет только НОВЫЙ текст, и широкий образец отбивал бы
139
+ # правку, которая ничего нового не заводит.
140
+ rt_reinvented_in_default() {
141
+ case "$1" in
142
+ *.ts)
143
+ printf '%s\t%s\n' '@(Input|Output|ViewChild|ViewChildren|ContentChild)\(' 'input(), output(), viewChild(), contentChild() — реактивный вход и выход'
144
+ printf '%s\t%s\n' 'get [a-zA-Z]+\(\)[[:space:]]*(:|\{)' 'computed(): производное значение сигналом, а не геттером'
145
+ ;;
146
+ *.html)
147
+ printf '%s\t%s\n' '\[ngClass\]|\[class\.' 'директивы класса кита'
148
+ ;;
149
+ *.scss)
150
+ printf '%s\t%s\n' '#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?\b' 'токен оформления вместо записанного цвета'
151
+ ;;
152
+ esac
153
+ }
154
+
155
+ # Где якорь для спек не требуется. Витрина, корневая разметка и сборка — общее у всех деревьев;
156
+ # своё дерево дописывает надстройкой.
157
+ RT_QA_SKIP_RE="${RT_QA_SKIP_RE:-/node_modules/|/dist/|\.stories\.html\$|/src/index\.html\$}"
158
+
159
+ # По какому имени тег опознаётся компонентом этого дерева. Умолчание — составной тег: имя с
160
+ # дефисом принадлежит компоненту, а не разметке, и это верно везде, где есть свои компоненты.
161
+ RT_QA_COMPONENT_RE="${RT_QA_COMPONENT_RE:--}"
162
+
163
+ # Теги, которые спека не нажимает: оформление, разметка страницы, индикаторы. Перечисляются
164
+ # именами через пробел. Умолчание молчит: у каждого кита свои, а чужое имя здесь глушило бы
165
+ # проверку на теге, которого в этом дереве нет.
166
+ rt_qa_decorative_default() {
167
+ return 0
168
+ }
169
+
170
+ # Умолчания под общими именами. Надстройка проекта объявит любое из них заново — и позовёт
171
+ # отсюда то же имя с суффиксом `_default` для всего, чего не назвала сама.
172
+ rt_push_checks() { rt_push_checks_default "$@"; }
173
+ rt_docs_pair_for() { rt_docs_pair_for_default "$@"; }
174
+ rt_lint_for() { rt_lint_for_default "$@"; }
175
+ rt_task_branch_ok() { rt_task_branch_ok_default "$@"; }
176
+ rt_reinvented_in() { rt_reinvented_in_default "$@"; }
177
+ rt_is_app_code() { rt_is_app_code_default "$@"; }
178
+ rt_qa_decorative() { rt_qa_decorative_default "$@"; }
179
+ rt_task_state() { rt_task_state_default "$@"; }
@@ -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,28 @@
1
+ #!/usr/bin/env bash
2
+ # rt-hook: PreToolUse mcp__claude-in-chrome__select_browser
3
+ # Гард выбора браузера. PreToolUse на выборе браузера расширением.
4
+ #
5
+ # Отклоняет любой профиль, кроме закреплённого: чужой стоит лишнего круга и приводит в браузер,
6
+ # где сессий этого проекта нет вовсе.
7
+ #
8
+ # На совпадении ставит метку сессии. Гард свежести читает ВОЗРАСТ этой метки — она и делает
9
+ # законной всю дальнейшую работу с браузером.
10
+ #
11
+ # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: помощник не назвал профиль — пропуск.
12
+
13
+ input="$(cat 2>/dev/null)"
14
+
15
+ device_id="$("${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/browser-device-id.sh" 2>/dev/null)"
16
+ [ -z "$device_id" ] && exit 0
17
+
18
+ requested="$(printf '%s' "$input" | jq -r '.tool_input.deviceId // empty' 2>/dev/null)"
19
+
20
+ if [ "$requested" = "$device_id" ]; then
21
+ sid="$(printf '%s' "$input" | jq -r '.session_id // "nosession"' 2>/dev/null)"
22
+ marker_dir="${TMPDIR:-/tmp}/claude-browser-guard"
23
+ mkdir -p "$marker_dir" 2>/dev/null && : >"$marker_dir/${sid}" 2>/dev/null
24
+ exit 0
25
+ fi
26
+
27
+ echo "Профиль «${requested}» не тот, что закреплён за проектом. Бери ${device_id} — единственный профиль, где сделан вход." >&2
28
+ exit 2
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env bash
2
+ # rt-hook: PreToolUse AskUserQuestion
3
+ # PreToolUse guard for AskUserQuestion.
4
+ #
5
+ # The browser choice is already made and pinned in browser-device-id.sh — asking again is noise, and the
6
+ # answer could only ever be the pinned profile. Only questions actually about PICKING a browser
7
+ # are blocked; every other question passes untouched.
8
+ #
9
+ # A bare "browser"/"браузер" substring is too broad for a web-dev project: questions about
10
+ # client-side code, user agents, cross-browser bugs etc. legitimately use the word without
11
+ # being about which Chrome profile to automate. Anchor on selection phrasing instead
12
+ # (deviceId, "which/what browser", "какой браузер").
13
+ #
14
+ # FAIL-OPEN when browser-device-id.sh prints nothing.
15
+
16
+ input="$(cat 2>/dev/null)"
17
+
18
+ device_id="$("${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/browser-device-id.sh" 2>/dev/null)"
19
+ [ -z "$device_id" ] && exit 0
20
+
21
+ questions="$(printf '%s' "$input" | jq -r '[.tool_input.questions[]? | .question, .header, (.options[]?.label)] | join(" ")' 2>/dev/null)"
22
+ [ -z "$questions" ] && exit 0
23
+
24
+ printf '%s' "$questions" | grep -qiE 'deviceid|device id|(which|what|pick|choose|select)[^.]{0,25}\bbrowser\b|\bbrowser\b[^.]{0,25}(profile|to use)|как(ой|ую)?[^.]{0,15}браузер|брауз[а-я]*[^.]{0,20}(использовать|выбрать|выбор|нужен|запустить)' || exit 0
25
+
26
+ echo "Do not ask which browser to use — the profile is pinned. Call select_browser with deviceId ${device_id} ('Main' profile)." >&2
27
+ exit 2
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env bash
2
+ # rt-hook: PreToolUse mcp__claude-in-chrome__(list_connected_browsers|switch_browser)
3
+ # Гард перечисления и переключения браузеров. PreToolUse.
4
+ #
5
+ # Сессии проекта живут в одном закреплённом профиле. Перечисление и переключение отдают общие
6
+ # неустойчивые имена, которые не опознают ничего, а выбор из них приводит в профиль без входа —
7
+ # поэтому единственный поддержанный путь — выбор по закреплённому идентификатору.
8
+ #
9
+ # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: помощник не назвал профиль — пропуск. Гард, который не может назвать
10
+ # нужный профиль, ничего не предлагает взамен, и слепой отказ только заводил бы работу в тупик.
11
+
12
+ cat >/dev/null 2>&1
13
+
14
+ device_id="$("${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/browser-device-id.sh" 2>/dev/null)"
15
+ [ -z "$device_id" ] && exit 0
16
+
17
+ echo "Не перечисляй и не переключай браузеры. Вызови выбор браузера с профилем ${device_id} — единственным, где сделан вход." >&2
18
+ exit 2
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env bash
2
+ # rt-hook: PreToolUse mcp__playwright__.*|mcp__chrome-devtools__.*|Bash
3
+ # Гард обходных путей к браузеру. PreToolUse.
4
+ #
5
+ # Закрепление профиля чего-то стоит только тогда, когда дверь одна. Здесь перечислены двери,
6
+ # которые обходят её целиком и закреплённый профиль не спрашивают вовсе: второй драйвер,
7
+ # третий драйвер, открытие ссылки средствами системы, управление браузером через сценарий
8
+ # автоматизации, отдельная утилита и прямой запуск бинарника.
9
+ #
10
+ # Написание сквозных спек при этом остаётся законным: прогон спеки не выдаёт агенту
11
+ # интерактивный браузер. Отбиваются только глаголы вождения.
12
+ #
13
+ # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: помощник не назвал профиль — пропуск.
14
+
15
+ input="$(cat 2>/dev/null)"
16
+
17
+ device_id="$("${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/browser-device-id.sh" 2>/dev/null)"
18
+ [ -z "$device_id" ] && exit 0
19
+
20
+ tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
21
+
22
+ deny() {
23
+ echo "$1 Води браузер закреплённым расширением: выбери профиль ${device_id} и работай его инструментами." >&2
24
+ exit 2
25
+ }
26
+
27
+ case "$tool" in
28
+ mcp__playwright__*)
29
+ deny "Второй драйвер браузера в этом проекте не используется — в его профиле вход не сделан." ;;
30
+ mcp__chrome-devtools__*)
31
+ deny "Третий драйвер браузера в этом проекте не используется — закреплённый профиль он не спрашивает." ;;
32
+ esac
33
+
34
+ # Терминал среды разработки запускает те же драйверы той же командной строкой.
35
+ case "$tool" in
36
+ Bash | mcp__webstorm__execute_terminal_command | mcp__webstorm__execute_tool) ;;
37
+ *) exit 0 ;;
38
+ esac
39
+
40
+ cmd="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null)"
41
+ [ -z "$cmd" ] && exit 0
42
+
43
+ if [ "$tool" = "mcp__webstorm__execute_tool" ] && command -v perl >/dev/null 2>&1; then
44
+ inner="$(printf '%s' "$cmd" | perl -0ne '
45
+ if (/--command(?:=|\s+)(?:"((?:[^"\\]|\\.)*)"|\x27([^\x27]*)\x27|(.+))/s) {
46
+ print defined $1 ? $1 : (defined $2 ? $2 : $3);
47
+ }
48
+ ' 2>/dev/null)"
49
+ [ -n "$inner" ] && cmd="$inner"
50
+ fi
51
+
52
+ # Прогон сквозных спек — законный путь, и он не отбивается никогда.
53
+ case "$cmd" in
54
+ *playwright\ open*|*playwright\ codegen*|*playwright\ screenshot*|*playwright\ cr*)
55
+ deny "Вождение браузера из командной строки драйвера обходит закреплённый профиль." ;;
56
+ esac
57
+
58
+ case "$cmd" in
59
+ *open\ http*|*open\ -a\ *Chrome*|*open\ -a\ *chrome*)
60
+ deny "Открытие адреса средствами системы поднимает браузер по умолчанию, а не закреплённый профиль." ;;
61
+ *osascript*Chrome*|*osascript*chrome*)
62
+ deny "Управление браузером сценарием автоматизации обходит закреплённый профиль." ;;
63
+ *chrome-cli*)
64
+ deny "Эта утилита обходит закреплённый профиль." ;;
65
+ esac
66
+
67
+ # Бинарник браузера — и только в позиции команды.
68
+ #
69
+ # Голый образец с именем движка здесь непригоден: он совпадает и со значением флага, которым
70
+ # помечают движок в прогоне сквозных спек, — такой образец отбил бы сам прогон в день, когда
71
+ # появился. Поэтому якорь на границе команды и требование похожего на исполняемый файл слова,
72
+ # а не значения флага.
73
+ printf '%s' "$cmd" | grep -qE '(^|[;&|(]|[[:space:]]&&|[[:space:]]\|\|)[[:space:]]*(/[^[:space:]]*/)?(google-chrome|chromium)([[:space:]]|$)' \
74
+ && deny "Прямой запуск бинарника браузера обходит закреплённый профиль."
75
+
76
+ printf '%s' "$cmd" | grep -qF 'Google Chrome.app/Contents/MacOS' \
77
+ && deny "Прямой запуск бинарника браузера обходит закреплённый профиль."
78
+
79
+ exit 0
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env bash
2
+ # rt-hook: PreToolUse mcp__claude-in-chrome__.*
3
+ # Гард свежести выбора браузера. PreToolUse на всех остальных вызовах расширения.
4
+ #
5
+ # ЗАЧЕМ ОН ЕСТЬ — отказ, из которого он вырос: расширение действует на тот браузер, который
6
+ # считает активным сейчас, и этот выбор ПЛЫВЁТ. Выбор, сделанный в начале сессии, не держится:
7
+ # после долгого перерыва на работу без браузера следующий же вызов открыл вкладку в другом
8
+ # профиле — молча. Гард на самом выборе такого не ловит: в этот момент выбор никто не вызывает,
9
+ # а тот, что был сделан раньше, был верным.
10
+ #
11
+ # Поэтому гард про СВЕЖЕСТЬ, а не про «выбирали ли вообще»:
12
+ # - гард выбора ставит метку на каждом принятом выборе;
13
+ # - каждый прошедший здесь вызов метку обновляет, поэтому непрерывная работа идёт свободно;
14
+ # - как только метка старше окна, следующий вызов отбивается и требует выбрать заново.
15
+ # Перерыв — это ровно то, когда выбор уплывает, поэтому перерыв гард и взводит.
16
+ #
17
+ # Выбор — один дешёвый повторяемый вызов, и повторить его стоит несравнимо меньше, чем попасть
18
+ # не в тот браузер.
19
+ #
20
+ # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: помощник не назвал профиль — пропуск.
21
+
22
+ input="$(cat 2>/dev/null)"
23
+
24
+ device_id="$("${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/browser-device-id.sh" 2>/dev/null)"
25
+ [ -z "$device_id" ] && exit 0
26
+
27
+ tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
28
+ # У перечисления, переключения и самого выбора свои гарды.
29
+ case "$tool" in
30
+ *list_connected_browsers|*switch_browser|*select_browser) exit 0 ;;
31
+ esac
32
+
33
+ ttl=300
34
+
35
+ sid="$(printf '%s' "$input" | jq -r '.session_id // "nosession"' 2>/dev/null)"
36
+ marker="${TMPDIR:-/tmp}/claude-browser-guard/${sid}"
37
+
38
+ if [ ! -f "$marker" ]; then
39
+ echo "В этой сессии браузер не выбран. Вызови выбор браузера с профилем ${device_id} до любого другого вызова." >&2
40
+ exit 2
41
+ fi
42
+
43
+ now="$(date +%s)"
44
+ stamped="$(stat -f %m "$marker" 2>/dev/null || stat -c %Y "$marker" 2>/dev/null || echo 0)"
45
+ age=$(( now - stamped ))
46
+
47
+ if [ "$age" -gt "$ttl" ]; then
48
+ rm -f "$marker" 2>/dev/null
49
+ echo "Последний выбор браузера был ${age} с назад (предел ${ttl} с) — на таких перерывах активный браузер расширения уплывает, и это может быть уже не закреплённый профиль. Вызови выбор с профилем ${device_id} заново и повтори." >&2
50
+ exit 2
51
+ fi
52
+
53
+ : >"$marker" 2>/dev/null
54
+ exit 0
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env bash
2
+ # Проверка сообщения коммита. Хук самого гита, а не агента.
3
+ #
4
+ # Версионируемый шаблон: рабочая копия хуков гита не версионируется, поэтому файл ставится в
5
+ # неё отдельно — скриптом подготовки при установке зависимостей. Переустановка руками, если
6
+ # подготовка почему-то не отработала:
7
+ # cp .claude/hooks/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,43 @@
1
+ #!/usr/bin/env bash
2
+ # rt-hook: SessionStart startup|resume|compact|clear
3
+ # Вход в слой законов. SessionStart.
4
+ #
5
+ # Файл закона сам по себе не приносит в контекст ничего — его читают, только когда за ним
6
+ # пошли. Хук печатает указатель (имя файла и его заголовок) один раз за сессию: слой известен,
7
+ # что существует, и открывается, когда решение его задевает.
8
+ #
9
+ # Указатель ЧИТАЕТСЯ ИЗ КАТАЛОГА, а не выписан руками: выписанный разошёлся бы с тем, что
10
+ # разложено на самом деле, и сказать об этом было бы нечем.
11
+ #
12
+ # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: любая ошибка начинает сессию без добавленного контекста (exit 0).
13
+ # Сломанный вход не имеет права остановить сессию.
14
+
15
+ dir="${CLAUDE_PROJECT_DIR:-.}/docs/constitution"
16
+ [ -d "$dir" ] || exit 0
17
+
18
+ index=""
19
+ for law in "$dir"/*.md; do
20
+ [ -f "$law" ] || continue
21
+ title="$(grep -m1 '^# ' "$law" 2>/dev/null | sed 's/^# //')"
22
+ [ -z "$title" ] && continue
23
+ index="${index} docs/constitution/$(basename "$law") — ${title}"$'\n'
24
+ done
25
+ [ -z "$index" ] && exit 0
26
+
27
+ read -r -d '' context <<EOF
28
+ ЗАКОНЫ ПРОЕКТА (слой @rt-tools/agent-kit, разложен в docs/constitution/).
29
+
30
+ Закон говорит, ЧТО должно быть верно, и не знает ни путей, ни имён файлов. Правило — каким
31
+ приёмом это делается — живёт в .claude/skills/, а имена этого дерева при нём — в
32
+ implementation.md рядом. Закон не отменяет правила и не заменяется им: перед решением, которое
33
+ закон задевает, читается закон целиком, правило — как обычно.
34
+
35
+ ${index}
36
+ Разложенные файлы правятся не руками, а надстройкой в .claude/rt-kit/overrides/<ресурс>:
37
+ правка на месте теряется на следующем \`agent-kit sync\`, и он на неё отказывает. Сверить
38
+ разложенное с пакетом: \`agent-kit sync --check\`.
39
+ EOF
40
+
41
+ jq -n --arg c "$context" '{hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:$c}}' 2>/dev/null || exit 0
42
+
43
+ exit 0
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env bash
2
+ # rt-hook: PreToolUse Bash|mcp__webstorm__execute_terminal_command|mcp__webstorm__execute_run_configuration|mcp__webstorm__execute_tool
3
+ # Гард второго сервера разработки. PreToolUse.
4
+ #
5
+ # Приложения уже подняты владельцем, и всякая проверка через браузер идёт туда. Второй
6
+ # экземпляр занимает лишний порт, отдаёт другую сборку и уводит разбор в сторону: расхождение
7
+ # между двумя серверами читается как дефект правки. Вдобавок сборка, запущенная между делом,
8
+ # гасит уже поднятый сервер молча.
9
+ #
10
+ # Отбивается всё, что ПОДНИМАЕТ сервер. Сборка, тесты, линтеры, запросы к поднятым портам и
11
+ # осмотр слушателей проходят.
12
+ #
13
+ # Где именно подняты приложения, знает профиль проекта: .claude/rt-kit/project.sh, переменная
14
+ # RT_STANDS. Нет профиля — текст отказа остаётся общим, сам гард работает.
15
+
16
+ input="$(cat 2>/dev/null)"
17
+
18
+ tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
19
+ case "$tool" in
20
+ Bash | mcp__webstorm__execute_terminal_command | mcp__webstorm__execute_tool) ;;
21
+ # Готовая конфигурация запуска командной строки не показывает — видно только её имя.
22
+ # Отсюда правило: «serve», «dev» и «start» в имени отклоняются, потому что проверить, что
23
+ # за ними стоит, гард не может, а второй сервер стоит дороже лишнего отказа.
24
+ mcp__webstorm__execute_run_configuration)
25
+ name="$(printf '%s' "$input" | jq -r '.tool_input.configurationName // empty' 2>/dev/null)"
26
+ # Второй режим инструмента — временная конфигурация из пары «файл и строка»: имени у
27
+ # неё нет вовсе, и проверка по имени её пропускала. Именно так поднимается скрипт из
28
+ # манифеста пакета.
29
+ if [ -z "$name" ]; then
30
+ file="$(printf '%s' "$input" | jq -r '.tool_input.filePath // empty' 2>/dev/null)"
31
+ case "$file" in
32
+ */package.json|package.json)
33
+ echo "Запуск скрипта прямо из манифеста: гард видит только файл и строку, а не сам скрипт, поэтому не может отличить подъём сервера от сборки. Приложения уже подняты владельцем — если нужна сборка или тест, запусти их командой в терминале." >&2
34
+ exit 2 ;;
35
+ esac
36
+ exit 0
37
+ fi
38
+ # Слово «start» без границы ловило и «restart», который сервер не поднимает.
39
+ case "$(printf '%s' "$name" | tr '[:upper:]' '[:lower:]')" in
40
+ *serve*|*dev*|start*|*\ start*|*:start*)
41
+ echo "Конфигурация «${name}» похожа на подъём сервера разработки, а приложения уже подняты владельцем — проверяй их. Если конфигурация делает другое, запусти это командой: по имени гард содержимого не видит." >&2
42
+ exit 2 ;;
43
+ esac
44
+ exit 0 ;;
45
+ *) exit 0 ;;
46
+ esac
47
+
48
+ cmd="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null)"
49
+ [ -z "$cmd" ] && exit 0
50
+
51
+ # Универсальный исполнитель среды передаёт настоящую команду вложенной строкой. Разбирать надо
52
+ # её, а не обёртку: иначе имя раннера стоит сразу за кавычкой и ни одно правило до него не
53
+ # дотягивается.
54
+ if [ "$tool" = "mcp__webstorm__execute_tool" ] && command -v perl >/dev/null 2>&1; then
55
+ inner="$(printf '%s' "$cmd" | perl -0ne '
56
+ if (/--command(?:=|\s+)(?:"((?:[^"\\]|\\.)*)"|\x27([^\x27]*)\x27|(.+))/s) {
57
+ print defined $1 ? $1 : (defined $2 ? $2 : $3);
58
+ }
59
+ ' 2>/dev/null)"
60
+ [ -n "$inner" ] && cmd="$inner"
61
+ fi
62
+
63
+ # Гит ничего не слушает на портах, а тексты сообщений и веток свободно содержат слова вроде
64
+ # «serve» — без этой ветки гард ловит собственный коммит про себя же.
65
+ case "$cmd" in
66
+ git\ *|*/git\ *)
67
+ printf '%s' "$cmd" | grep -qE '(^|[[:space:]])git[[:space:]]+daemon([[:space:]]|$)' || exit 0 ;;
68
+ esac
69
+
70
+ # Профиль дерева: сперва умолчание пакета, поверх него — надстройка проекта, если она есть.
71
+ rt_hooks_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
72
+ for profile in "$rt_hooks_dir/../rt-kit/defaults/project.sh" "$rt_hooks_dir/../defaults/project.sh" "${CLAUDE_PROJECT_DIR:-.}/.claude/rt-kit/defaults/project.sh" "${CLAUDE_PROJECT_DIR:-.}/.claude/rt-kit/project.sh"; do
73
+ # shellcheck disable=SC1090
74
+ [ -f "$profile" ] && . "$profile" 2>/dev/null
75
+ done
76
+ stands="${RT_STANDS:-}"
77
+
78
+ deny() {
79
+ if [ -n "$stands" ]; then
80
+ echo "$1 Приложения уже подняты владельцем: ${stands} — проверяй их. Свой экземпляр не поднимай; если порт не отвечает, скажи владельцу, а не запускай второй." >&2
81
+ else
82
+ echo "$1 Приложения уже подняты владельцем — проверяй их. Свой экземпляр не поднимай; если порт не отвечает, скажи владельцу, а не запускай второй." >&2
83
+ fi
84
+ exit 2
85
+ }
86
+
87
+ # Раннеры перечисляются явно. Группа «любое слово перед именем» отклоняла даже заметку о том,
88
+ # что сервер поднимает владелец.
89
+ RUNNER='((npx|pnpm|yarn|bun|npm)([[:space:]]+(exec|run|dlx))?[[:space:]]+)?'
90
+
91
+ # Начало вызова: начало строки или разделитель команд. Кавычку в границы вносить нельзя — тогда
92
+ # поиск по тексту и снятие процесса по шаблону читаются как запуск.
93
+ BOUND='(^|[;&|(]|&&|\|\|)[[:space:]]*'
94
+
95
+ printf '%s' "$cmd" | grep -qE "${BOUND}${RUNNER}(nx|ng)[[:space:]]+(run[[:space:]]+[^[:space:]]*:)?(serve|dev)" \
96
+ && deny "Запуск ещё одного сервера разработки через каркас."
97
+
98
+ # Требование пробела сразу после имени рвало совпадение на двоеточии: скрипты вида «serve:site»
99
+ # гард пропускал — то есть ровно те команды, ради которых написан.
100
+ printf '%s' "$cmd" | grep -qE "${BOUND}(npm|pnpm|yarn|bun)([[:space:]]+run)?[[:space:]]+(dev|start|serve)([:._-][A-Za-z0-9:._-]*)?([[:space:]]|\$)" \
101
+ && deny "Запуск ещё одного сервера разработки через пакетный раннер."
102
+
103
+ # Подкоманда обязательна: пока она была необязательной, под правило попадало голое слово
104
+ # сборщика — то есть любой однострочник, где оно встречается внутри текста.
105
+ printf '%s' "$cmd" | grep -qE "${BOUND}${RUNNER}vite([[:space:]]+(dev|serve|preview))?[[:space:]]*(\$|[;&|\"'])" \
106
+ && deny "Запуск ещё одного сервера разработки."
107
+ printf '%s' "$cmd" | grep -qE "${BOUND}${RUNNER}(next|astro|nuxt)[[:space:]]+(dev|start|preview)([[:space:]]|\$)" \
108
+ && deny "Запуск ещё одного сервера разработки."
109
+
110
+ # Статика поверх сборки — тот же второй экземпляр. Каждое имя под общим якорем начала команды:
111
+ # без него перечисление пакетов и поиск по документам читались как запуск.
112
+ printf '%s' "$cmd" | grep -qE "${BOUND}(python3?[[:space:]]+-m[[:space:]]+http\.server|${RUNNER}(http-server|live-server|serve)([[:space:]]|\$))" \
113
+ && deny "Подъём статического сервера поверх сборки — тот же второй экземпляр."
114
+
115
+ exit 0