@rt-tools/agent-kit 0.8.1 → 0.8.3
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 +25 -19
- package/assets/agents/qa-engineer.md +1 -1
- package/assets/agents/rules-reviewer.md +83 -0
- package/assets/checks/board.github.mjs +56 -17
- package/assets/checks/check-board.github.mjs +49 -5
- package/assets/checks/check-dupes.mjs +66 -6
- package/assets/checks/check-specs.mjs +100 -15
- package/assets/checks/rt-kit-checks.config.mjs +9 -0
- package/assets/checks/task-new.github.mjs +33 -5
- package/assets/commands/agent-kit-digest.md +10 -5
- package/assets/commands/feedback.md +95 -0
- package/assets/commands/next-session.md +4 -4
- package/assets/commands/rules-review.md +98 -0
- package/assets/commands/skill-curator.md +44 -25
- package/assets/defaults/gate-map.sh +11 -4
- package/assets/defaults/project.sh +46 -0
- package/assets/docs/GLOSSARY.md +49 -46
- package/assets/hooks/docs-guard.sh +19 -3
- package/assets/hooks/git-guard-delivery.sh +106 -13
- package/assets/hooks/proposal-guard.sh +93 -0
- package/assets/hooks/reuse-first-guard.sh +83 -15
- package/assets/hooks/skill-gate-layers.sh +1 -1
- package/assets/hooks/skill-gate.sh +27 -1
- package/assets/hooks/task-flow-guard.sh +59 -19
- package/assets/hooks/waiting-turn-guard.sh +87 -0
- package/assets/hooks/window-fill-guard.sh +1 -1
- package/assets/laws/delivery.md +41 -7
- package/assets/laws/project-documentation.md +39 -0
- package/assets/laws/verifiability.md +6 -1
- package/assets/laws/work-conduct.md +83 -3
- package/assets/patterns/git-workflow-commit.azure.md +84 -4
- package/assets/patterns/git-workflow-commit.github.md +90 -4
- package/assets/patterns/git-workflow-commit.gitlab.md +84 -5
- package/assets/patterns/git-workflow-docker.md +30 -0
- package/assets/patterns/git-workflow-merge.md +1 -1
- package/assets/patterns/spec-driven-domain.md +7 -1
- package/assets/patterns/spec-driven-rule.md +6 -0
- package/assets/patterns/task-flow-close.md +197 -21
- package/assets/patterns/task-flow-handoff.md +28 -5
- package/assets/patterns/task-flow-resume.md +25 -7
- package/assets/patterns/task-flow-start.md +37 -6
- package/assets/rules/angular-patterns.md +22 -0
- package/assets/rules/api-layer.md +25 -0
- package/assets/rules/browser-verification.md +42 -1
- package/assets/rules/component-structure.md +21 -0
- package/assets/rules/dependencies.md +22 -0
- package/assets/rules/doc-style.md +37 -5
- package/assets/rules/{entity-conventions.md → entity-conventions.needs-admin.md} +21 -0
- package/assets/rules/entity-models.md +21 -0
- package/assets/rules/git-workflow.azure.md +62 -8
- package/assets/rules/git-workflow.github.md +78 -14
- package/assets/rules/git-workflow.gitlab.md +61 -8
- package/assets/rules/lib-layers.md +25 -0
- package/assets/rules/lists.md +27 -0
- package/assets/rules/navigation.md +21 -0
- package/assets/rules/{observability.md → observability.needs-app.md} +23 -0
- package/assets/rules/permissions.md +23 -0
- package/assets/rules/platform-access.md +21 -0
- package/assets/rules/reuse-first.md +20 -0
- package/assets/rules/seo.md +19 -0
- package/assets/rules/shared-code.md +19 -0
- package/assets/rules/spec-driven.md +36 -0
- package/assets/rules/styling-bem.md +19 -0
- package/assets/rules/task-flow.md +150 -35
- package/assets/rules/testing.md +50 -0
- package/assets/rules/translations.md +21 -0
- package/assets/rules/typescript-conventions.md +33 -0
- package/assets/samples/specs/_template/spec.md +83 -0
- package/assets/samples/tasks/_template/grill.md +28 -0
- package/assets/samples/tasks/_template/plan.md +39 -0
- package/assets/samples/tasks/_template/progress.md +23 -0
- package/assets/skills/agent-kit-extend.md +24 -0
- package/assets/skills/agent-kit.md +69 -7
- package/assets/templates/rule.md +31 -2
- package/assets/traits.json +14 -0
- package/bin/agent-kit.d.ts.map +1 -1
- package/bin/agent-kit.js +31 -16
- package/bin/agent-kit.js.map +1 -1
- package/index.d.ts +1 -0
- package/index.d.ts.map +1 -1
- package/index.js +1 -0
- package/index.js.map +1 -1
- package/lib/argv.d.ts +17 -0
- package/lib/argv.d.ts.map +1 -0
- package/lib/argv.js +44 -0
- package/lib/argv.js.map +1 -0
- package/lib/cargo.d.ts +88 -0
- package/lib/cargo.d.ts.map +1 -0
- package/lib/cargo.js +16 -0
- package/lib/cargo.js.map +1 -0
- package/lib/catalog.d.ts +18 -1
- package/lib/catalog.d.ts.map +1 -1
- package/lib/catalog.js +12 -2
- package/lib/catalog.js.map +1 -1
- package/lib/commands.d.ts +0 -26
- package/lib/commands.d.ts.map +1 -1
- package/lib/commands.js +79 -122
- package/lib/commands.js.map +1 -1
- package/lib/companion.d.ts +37 -0
- package/lib/companion.d.ts.map +1 -1
- package/lib/companion.js +42 -1
- package/lib/companion.js.map +1 -1
- package/lib/config.d.ts +38 -1
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +24 -0
- package/lib/config.js.map +1 -1
- package/lib/ship.d.ts +39 -0
- package/lib/ship.d.ts.map +1 -0
- package/lib/ship.js +87 -0
- package/lib/ship.js.map +1 -0
- package/lib/shipment.d.ts +60 -0
- package/lib/shipment.d.ts.map +1 -0
- package/lib/shipment.js +247 -0
- package/lib/shipment.js.map +1 -0
- package/lib/snapshot.d.ts +30 -0
- package/lib/snapshot.d.ts.map +1 -0
- package/lib/snapshot.js +73 -0
- package/lib/snapshot.js.map +1 -0
- package/lib/traits.d.ts +32 -0
- package/lib/traits.d.ts.map +1 -0
- package/lib/traits.js +82 -0
- package/lib/traits.js.map +1 -0
- package/package.json +6 -2
- package/rt-tools-agent-kit-0.8.3.tgz +0 -0
- package/lib/submit.d.ts +0 -24
- package/lib/submit.d.ts.map +0 -1
- package/lib/submit.js +0 -26
- package/lib/submit.js.map +0 -1
- package/rt-tools-agent-kit-0.8.1.tgz +0 -0
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# rt-hook: PreToolUse Bash|mcp__webstorm__execute_terminal_command|mcp__webstorm__execute_tool
|
|
3
3
|
# Требует: hooks/profile-check.sh
|
|
4
|
-
# Гард поставки. PreToolUse на заведении
|
|
4
|
+
# Гард поставки. PreToolUse на заведении ветки, пуше и открытии заявки на слияние.
|
|
5
5
|
#
|
|
6
6
|
# Закон о поставке требует трёх вещей, которых обычно не проверяет ничто: правка начинается с
|
|
7
|
-
# задачи, видимой в очереди работ; задача, ветка и
|
|
7
|
+
# задачи, видимой в очереди работ; задача, ветка и PR несут один номер; у задачи есть
|
|
8
8
|
# исполнитель. Держатся они памятью — и не удерживаются: задачи стоят вне очереди, исполнитель
|
|
9
9
|
# не проставлен, а большинство влитых заявок приходит с веток, за которыми задачи не стояло
|
|
10
10
|
# вовсе.
|
|
11
11
|
#
|
|
12
|
-
# Гард стоит в
|
|
12
|
+
# Гард стоит в трёх точках, и в каждой требует того, что в этот момент исправимо:
|
|
13
13
|
#
|
|
14
14
|
# заведение ветки — имя с номером разбирается на месте; имя без номера пропускается:
|
|
15
15
|
# локальная ветка под пробу законна, в главную она не поедет, потому что заявка с неё
|
|
16
16
|
# не откроется;
|
|
17
|
+
# пуш — подпись машинного коммита ещё переписывается на месте; после пуша её чинит только
|
|
18
|
+
# силовая отправка;
|
|
17
19
|
# открытие заявки — ветка обязана нести номер, заголовок обязан начинаться с того же номера,
|
|
18
20
|
# а задача — быть открытой, стоять в очереди работ и иметь исполнителя.
|
|
19
21
|
#
|
|
@@ -28,7 +30,9 @@
|
|
|
28
30
|
# numbered); молчание значит «спросить некого»;
|
|
29
31
|
# RT_TASK_NEW_CMD — чем заводится задача;
|
|
30
32
|
# RT_BOARD_CHECK_CMD — чем сверяется очередь работ;
|
|
31
|
-
# RT_TASK_BOT — учётная запись, которую ставят
|
|
33
|
+
# RT_TASK_BOT — учётная запись, которую ставят исполнителем;
|
|
34
|
+
# RT_COMMIT_EMAIL — почта, которой подписан машинный коммит; её же левой частью он и
|
|
35
|
+
# опознаётся.
|
|
32
36
|
# Отказ называет и то, что не так, и чем это чинится: отказ без действия обходят, а не исполняют.
|
|
33
37
|
#
|
|
34
38
|
# ОТКАЗ В ПОЛЬЗУ РАБОТЫ: не репозиторий, нет разборщика, битый ввод, нет профиля — пропуск.
|
|
@@ -87,6 +91,7 @@ title_re="${RT_TASK_TITLE_RE:-^\[[A-Za-z]+-[0-9]+\][[:space:]]+[^[:space:]]}"
|
|
|
87
91
|
task_new="${RT_TASK_NEW_CMD:-npm run task:new}"
|
|
88
92
|
board_check="${RT_BOARD_CHECK_CMD:-npm run check:board}"
|
|
89
93
|
task_bot="${RT_TASK_BOT:-}"
|
|
94
|
+
commit_email="${RT_COMMIT_EMAIL:-}"
|
|
90
95
|
tasks_dir="${RT_TASKS_DIR:-}"
|
|
91
96
|
archive_dir="${RT_ARCHIVE_DIR:-}"
|
|
92
97
|
main_branch="${RT_MAIN_BRANCH:-main}"
|
|
@@ -94,7 +99,13 @@ main_branch="${RT_MAIN_BRANCH:-main}"
|
|
|
94
99
|
# Обход требования: строка с причиной. Причина видна тому, кто вливает, поэтому обход законен.
|
|
95
100
|
# Без причины это просто молчаливый пропуск, поэтому она обязательна. Порог в три знака — тот
|
|
96
101
|
# же, что у гарда документа: если сделать по-разному, две формы одного обхода разойдутся.
|
|
97
|
-
|
|
102
|
+
#
|
|
103
|
+
# Строка обхода начинает строку — свою в теле PR или комментарий в конце команды — и
|
|
104
|
+
# подстановки не принимает. Без этих двух условий текст, который ОБЪЯСНЯЕТ, что обход называется
|
|
105
|
+
# так-то, от самого обхода неотличим: тело PR со строкой-примером снимало требование само
|
|
106
|
+
# собой, и тот же пример гард печатает в своём отказе ниже. Нашлось это приёмкой такого же
|
|
107
|
+
# признака — прогон обязан был покраснеть на настоящем нарушении и остался зелёным.
|
|
108
|
+
folder_skip_re='(^|#)[[:space:]]*Task-folder-skip:[[:space:]]*[^[:space:]<"'"'"'][^[:space:]"'"'"']{2,}'
|
|
98
109
|
|
|
99
110
|
deny() {
|
|
100
111
|
# Отказ гарда — наблюдение: гард, отбивающий чаще прочих, говорит, какое место поставки
|
|
@@ -109,7 +120,7 @@ deny() {
|
|
|
109
120
|
exit 0
|
|
110
121
|
}
|
|
111
122
|
|
|
112
|
-
# Подсказка вместо отказа: на открытии
|
|
123
|
+
# Подсказка вместо отказа: на открытии PR папка ещё нужна. Решения подсказка не несёт,
|
|
113
124
|
# команда идёт дальше своим ходом.
|
|
114
125
|
hint() {
|
|
115
126
|
jq -n --arg c "$1" '{hookSpecificOutput:{hookEventName:"PreToolUse",additionalContext:$c}}' 2>/dev/null
|
|
@@ -165,6 +176,66 @@ if [ -n "$branch_arg" ]; then
|
|
|
165
176
|
exit 0
|
|
166
177
|
fi
|
|
167
178
|
|
|
179
|
+
# --- подпись машинного коммита ------------------------------------------------------------
|
|
180
|
+
#
|
|
181
|
+
# Служебный адрес хостинга состоит из числа, логина и домена, а сопоставляется по числу: логин
|
|
182
|
+
# рядом с ним не сверяет никто. Коммит с чужим числом хостинг припишет постороннему человеку, и
|
|
183
|
+
# изнутри это выглядит верным — имя учётной записи в истории то самое. Так одиннадцать коммитов
|
|
184
|
+
# и уехали в главную ветку за чужой подписью; нашёл это владелец, читая историю глазами.
|
|
185
|
+
#
|
|
186
|
+
# Точка — пуш: до него подпись переписывается на месте всей веткой сразу, после — только силовой
|
|
187
|
+
# отправкой. На коммите гард не стоит: почта задаётся переменными самой команды, и разбор её из
|
|
188
|
+
# текста ловил бы ту же строку, которая и так перед глазами у набравшего её.
|
|
189
|
+
#
|
|
190
|
+
# Судится коммит, НАЗВАВШИЙСЯ машинной записью: её логин стоит именем автора либо левой частью
|
|
191
|
+
# почты. Опознавать по самой почте нельзя — она в таком коммите как раз и неверна, а требовать
|
|
192
|
+
# машинную подпись от каждого коммита значило бы отбивать работу, сделанную человеком своими
|
|
193
|
+
# руками в том же дереве.
|
|
194
|
+
#
|
|
195
|
+
# Логин читается из объявленной почты, а не объявляется вторым свойством: два объявления одного
|
|
196
|
+
# логина разошлись бы молча. Исполнителем задачи он тоже не бывает — в дереве, где хостинг
|
|
197
|
+
# ограничил машинную запись, исполнителем ставят человека, а коммит остаётся машинным.
|
|
198
|
+
#
|
|
199
|
+
# Вызов пуша узнаётся по двум признакам сразу — команда `git` в начале строки или за
|
|
200
|
+
# разделителем и слово `push` отдельным словом. Одной подстрокой «git push» его не поймать:
|
|
201
|
+
# помощник учётных данных и заголовок запроса ставятся ключами `-c` между ними.
|
|
202
|
+
# Пробный пуш не отправляет ничего, и подпись у него не спрашивают.
|
|
203
|
+
if printf '%s' "$cmd" | grep -qE '(^|[;&|(]|&&|\|\|)[[:space:]]*git([[:space:]]|$)' \
|
|
204
|
+
&& printf '%s' "$cmd" | grep -qE '(^|[[:space:]])push([[:space:]]|$)' \
|
|
205
|
+
&& ! printf '%s' "$cmd" | grep -q -- '--dry-run'; then
|
|
206
|
+
# Два условия молчания. Дерево, не назвавшее почты машинной записи, требования не получает:
|
|
207
|
+
# своей машинной записи у пакета нет, а выдуманная отбивала бы работу в чужом дереве.
|
|
208
|
+
# Вершины главной ветки нет — вклад считать не от чего.
|
|
209
|
+
#
|
|
210
|
+
# Судится вклад ветки, а не вся история: влитое в главную этой веткой уже не чинится, и
|
|
211
|
+
# отказ за него отбивал бы работу вместо промаха.
|
|
212
|
+
if [ -n "$commit_email" ] \
|
|
213
|
+
&& git rev-parse --verify --quiet "refs/remotes/origin/${main_branch}" >/dev/null 2>&1; then
|
|
214
|
+
# Левая часть служебного адреса: `<число>+<логин>` либо просто логин.
|
|
215
|
+
bot_login="${commit_email%%@*}"
|
|
216
|
+
bot_login="${bot_login##*+}"
|
|
217
|
+
|
|
218
|
+
strangers=''
|
|
219
|
+
while IFS="$(printf '\t')" read -r short author email; do
|
|
220
|
+
[ -z "$short" ] && continue
|
|
221
|
+
login="${email%%@*}"
|
|
222
|
+
login="${login##*+}"
|
|
223
|
+
[ "$author" = "$bot_login" ] || [ "$login" = "$bot_login" ] || continue
|
|
224
|
+
[ "$email" = "$commit_email" ] && continue
|
|
225
|
+
strangers="${strangers}${strangers:+, }${short} <${email}>"
|
|
226
|
+
done <<EOF
|
|
227
|
+
$(git log --format='%h%x09%an%x09%ae' "origin/${main_branch}..HEAD" 2>/dev/null)
|
|
228
|
+
EOF
|
|
229
|
+
|
|
230
|
+
[ -n "$strangers" ] \
|
|
231
|
+
&& deny "BLOCKED: машинный коммит подписан не той почтой, что объявлена деревом. Хостинг сопоставляет служебный адрес по числу в нём, и коммит с чужим числом он припишет постороннему человеку — изнутри промах не виден, потому что имя учётной записи рядом верное. Расходятся: ${strangers}. Объявлено: ${commit_email} — почта берётся оттуда, а не набирается по памяти. Перепиши подпись до пуша, после него это чинит только силовая отправка:
|
|
232
|
+
последний коммит — GIT_AUTHOR_NAME=\"${bot_login}\" GIT_AUTHOR_EMAIL=\"${commit_email}\" GIT_COMMITTER_NAME=\"${bot_login}\" GIT_COMMITTER_EMAIL=\"${commit_email}\" git commit --amend --no-edit --reset-author
|
|
233
|
+
весь вклад ветки — git filter-branch -f --env-filter 'GIT_AUTHOR_EMAIL=\"${commit_email}\"; GIT_COMMITTER_EMAIL=\"${commit_email}\"' origin/${main_branch}..HEAD"
|
|
234
|
+
fi
|
|
235
|
+
fi
|
|
236
|
+
# Своего выхода у этой точки нет: пуш бывает и составной командой, а второе её звено судят
|
|
237
|
+
# разделы ниже.
|
|
238
|
+
|
|
168
239
|
# --- слияние заявки ----------------------------------------------------------------------
|
|
169
240
|
#
|
|
170
241
|
# Папку задачи разбирают тем же PR, что и работу. После слияния этого уже никто не сделает:
|
|
@@ -243,24 +314,46 @@ fi
|
|
|
243
314
|
|
|
244
315
|
if [ -n "$title" ]; then
|
|
245
316
|
printf '%s' "$title" | grep -qE "$title_re" \
|
|
246
|
-
|| deny "BLOCKED: заголовок заявки не начинается с номера задачи. В списке заявок тела не видно, а строка связи живёт именно там — без номера в заголовке
|
|
317
|
+
|| deny "BLOCKED: заголовок заявки не начинается с номера задачи. В списке заявок тела не видно, а строка связи живёт именно там — без номера в заголовке PR с задачей не сопоставить."
|
|
247
318
|
title_number="$(printf '%s' "$title" | sed -nE 's/^\[[A-Za-z]+-([0-9]+)\].*/\1/p')"
|
|
248
319
|
if [ -n "$number" ] && [ -n "$title_number" ]; then
|
|
249
320
|
[ "$title_number" = "$number" ] \
|
|
250
|
-
|| deny "BLOCKED: в заголовке заявки номер ${title_number}, у ветки — ${number}. Задача, ветка и
|
|
321
|
+
|| deny "BLOCKED: в заголовке заявки номер ${title_number}, у ветки — ${number}. Задача, ветка и PR несут один и тот же номер."
|
|
251
322
|
fi
|
|
252
323
|
fi
|
|
253
324
|
|
|
254
|
-
# Главная ветка влита до открытия
|
|
325
|
+
# Главная ветка влита до открытия PR. PR от разошедшейся ветки показывает ревьюверу свою
|
|
255
326
|
# правку вперемешку с чужой, а проверки на нём гоняются от устаревшего основания.
|
|
256
327
|
#
|
|
257
|
-
#
|
|
258
|
-
#
|
|
259
|
-
#
|
|
328
|
+
# Ярусов два, и порядок между ними такой же, как у проверки задачи. Первый читает локальную
|
|
329
|
+
# вершину и работает без сети. Второй спрашивает удалённую ссылку — без него молчание гарда
|
|
330
|
+
# значит лишь «твоя ссылка не старше твоей ветки», а читается как «главная ветка влита»: ровно
|
|
331
|
+
# так открытый PR и оказался конфликтующим, и узнал об этом владелец.
|
|
332
|
+
#
|
|
333
|
+
# Сеть здесь допустима по той же причине, по которой её зовёт проверка очереди работ ниже:
|
|
334
|
+
# отсутствие ответа пропускается молча, и проверка, падающая в самолёте, не отбивает работу.
|
|
335
|
+
# Предел ожидания задаётся переменными самого git — внешний `timeout` есть не на всякой машине.
|
|
260
336
|
if git rev-parse --verify --quiet "refs/remotes/origin/${main_branch}" >/dev/null 2>&1 \
|
|
261
337
|
&& ! git merge-base --is-ancestor "origin/${main_branch}" HEAD 2>/dev/null; then
|
|
262
338
|
behind="$(git rev-list --count "HEAD..origin/${main_branch}" 2>/dev/null)"
|
|
263
|
-
deny "BLOCKED: «${main_branch}» ушла вперёд на ${behind:-несколько} коммитов, а в ветку не влита.
|
|
339
|
+
deny "BLOCKED: «${main_branch}» ушла вперёд на ${behind:-несколько} коммитов, а в ветку не влита. PR от разошедшейся ветки показывает ревьюверу правку вперемешку с чужой, а проверки на нём идут от устаревшего основания. Влей и повтори: git fetch origin && git merge origin/${main_branch} — порядок и разбор конфликта в паттерне git-workflow-merge."
|
|
340
|
+
fi
|
|
341
|
+
|
|
342
|
+
# Второй ярус: локальная ссылка сама могла протухнуть. Ответа нет — ярус молчит.
|
|
343
|
+
remote_main="$(GIT_HTTP_LOW_SPEED_LIMIT=1000 GIT_HTTP_LOW_SPEED_TIME=5 GIT_TERMINAL_PROMPT=0 \
|
|
344
|
+
git ls-remote origin "refs/heads/${main_branch}" 2>/dev/null | cut -f1)"
|
|
345
|
+
local_main="$(git rev-parse --verify --quiet "refs/remotes/origin/${main_branch}" 2>/dev/null)"
|
|
346
|
+
if [ -n "$remote_main" ] && [ -n "$local_main" ] && [ "$remote_main" != "$local_main" ]; then
|
|
347
|
+
# Возраст ссылки — то, чего исполнитель не видит вовсе, и именно он отличает «ветка
|
|
348
|
+
# отстала» от «я не знаю, отстала ли». Формат `stat` на BSD и на GNU разный, поэтому
|
|
349
|
+
# спрашиваются оба, а не угадывается система.
|
|
350
|
+
fetch_head="$(git rev-parse --git-dir 2>/dev/null)/FETCH_HEAD"
|
|
351
|
+
fetched_at="$(stat -f %m "$fetch_head" 2>/dev/null || stat -c %Y "$fetch_head" 2>/dev/null)"
|
|
352
|
+
age=''
|
|
353
|
+
if [ -n "$fetched_at" ]; then
|
|
354
|
+
age=" Последний git fetch — $(( ( $(date +%s) - fetched_at ) / 60 )) мин. назад."
|
|
355
|
+
fi
|
|
356
|
+
deny "BLOCKED: твоя ссылка origin/${main_branch} отстала от удалённой — ${local_main:0:8} против ${remote_main:0:8}.${age} Гард сравнивает ветку с тем, что лежит в дереве, поэтому молчание первого яруса значит «ссылка не старше ветки», а не «главная ветка влита». Влей и повтори: git fetch origin && git merge origin/${main_branch}."
|
|
264
357
|
fi
|
|
265
358
|
|
|
266
359
|
check_task "$number" "заявка с ветки «${branch}»"
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# rt-hook: Stop
|
|
3
|
+
# Гард предложения: ход, в котором владелец сказал завести или отправить предложение слою правил,
|
|
4
|
+
# не заканчивается, пока отправки не было. Stop.
|
|
5
|
+
#
|
|
6
|
+
# Зачем именно так. Написанное и не отправленное лежит в дереве неотличимо от отправленного:
|
|
7
|
+
# своей записи в слое правил у него нет, и владелец читает работу сделанной, пока не спросит
|
|
8
|
+
# прямо. Отправка при этом неудобна ровно в одном месте — она пишет отметки в файлы предложений и
|
|
9
|
+
# делает дерево грязным, — и этим доводом исполнитель закрывает молчание правил.
|
|
10
|
+
#
|
|
11
|
+
# Ловится просьба образцами, а не пониманием смысла: оценку «владелец просил отправить» назначал
|
|
12
|
+
# бы тот, кому она мешает. Глагол обязателен: «разбери пропозалы» — работа над уже приехавшими, и
|
|
13
|
+
# отправкой она не кончается. Слово «предложение» само по себе не считается тоже — оно ходит в
|
|
14
|
+
# каждом втором ходе о другом, и рядом с ним нужно слово о слое правил или о пакете.
|
|
15
|
+
#
|
|
16
|
+
# Отправкой считается вызов команды пакета без сухого прогона, сделанный в том же ходе: сухой
|
|
17
|
+
# прогон показывает, что уехало бы, ничего не заводит и следа наружу не оставляет.
|
|
18
|
+
#
|
|
19
|
+
# ОТКАЗ В ПОЛЬЗУ РАБОТЫ: при любой ошибке, нехватке `jq`, отсутствии записи хода и повторном
|
|
20
|
+
# заходе ход РАЗРЕШАЕТСЯ (exit 0). Сломанный гард не имеет права заклинить разговор.
|
|
21
|
+
|
|
22
|
+
input="$(cat 2>/dev/null)"
|
|
23
|
+
[ -z "$input" ] && exit 0
|
|
24
|
+
|
|
25
|
+
command -v jq >/dev/null 2>&1 || exit 0
|
|
26
|
+
|
|
27
|
+
# Повторный заход по тому же ходу не судится: гард сказал своё один раз и отпускает.
|
|
28
|
+
active="$(printf '%s' "$input" | jq -r '.stop_hook_active // false' 2>/dev/null)"
|
|
29
|
+
[ "$active" = "true" ] && exit 0
|
|
30
|
+
|
|
31
|
+
transcript="$(printf '%s' "$input" | jq -r '.transcript_path // empty' 2>/dev/null)"
|
|
32
|
+
[ -z "$transcript" ] && exit 0
|
|
33
|
+
[ -f "$transcript" ] || exit 0
|
|
34
|
+
|
|
35
|
+
# Каталог предложений у дерева свой. Заданный пустым — отказ дерева от требования: дереву, не
|
|
36
|
+
# берущему переносимый слой правил, гард не навязывается.
|
|
37
|
+
proposals_dir="${RT_PROPOSALS_DIR-.claude/rt-kit/proposals}"
|
|
38
|
+
[ -z "$proposals_dir" ] && exit 0
|
|
39
|
+
|
|
40
|
+
# Просьба владельца. Набор открыт и пополняется правкой: полнота его — открытый вопрос
|
|
41
|
+
# договорённости, а не обещание.
|
|
42
|
+
asked_re='(завед|напиш|отправ|пошл|зашл|отошл|выгруз)[а-яё]*[^.!?]{0,40}(пропозал|предложени)|(пропозал|предложени)[а-яё]*[^.!?]{0,40}(завед|напиш|отправ|пошл|зашл|отошл|выгруз)'
|
|
43
|
+
|
|
44
|
+
# Сосед, без которого слово «предложение» не считается просьбой о слое правил.
|
|
45
|
+
context_re='пропозал|слою правил|слоя правил|слой правил|пакет|agent-kit|наверх'
|
|
46
|
+
|
|
47
|
+
# Ход — это всё, что записано после последнего настоящего ввода владельца. Ответ инструмента
|
|
48
|
+
# приходит той же ролью, поэтому строки с `tool_result` вводом не считаются.
|
|
49
|
+
#
|
|
50
|
+
# Хвост в 400 строк: запись хода растёт всю сессию, а судится только последний ход.
|
|
51
|
+
verdict="$(tail -n 400 "$transcript" 2>/dev/null | jq -s -r --arg asked "$asked_re" --arg ctx "$context_re" '
|
|
52
|
+
def is_input:
|
|
53
|
+
.type == "user"
|
|
54
|
+
and (((.message.content // []) | if type == "array"
|
|
55
|
+
then ([.[] | select(.type == "tool_result")] | length)
|
|
56
|
+
else 0 end) == 0);
|
|
57
|
+
|
|
58
|
+
def text_of:
|
|
59
|
+
(.message.content // []) | if type == "array"
|
|
60
|
+
then ([.[] | select(.type == "text") | .text] | join("\n"))
|
|
61
|
+
else (. // "") end;
|
|
62
|
+
|
|
63
|
+
(map(is_input) | rindex(true)) as $i
|
|
64
|
+
| (if $i == null then [] else .[$i:] end) as $turn
|
|
65
|
+
| (if $i == null then "" else ($turn[0] | text_of) end) as $said
|
|
66
|
+
| [$turn[] | select(.type == "assistant") | (.message.content // [])[] | select(.type == "tool_use")] as $uses
|
|
67
|
+
# Признак нечувствителен к регистру флагом, а не приведением: приведение знает только
|
|
68
|
+
# латиницу, и «Отправь Пропозал» с большой буквы проходило бы мимо набора образцов молча.
|
|
69
|
+
| (($said | test($asked; "i")) and ($said | test($ctx; "i"))) as $wanted
|
|
70
|
+
| ($uses | map(
|
|
71
|
+
((.name // "") | test("^Bash$"))
|
|
72
|
+
and ((.input.command // "") | test("agent-kit[^|;&]*propose"))
|
|
73
|
+
and ((.input.command // "") | test("--dry-run") | not)
|
|
74
|
+
) | any) as $sent
|
|
75
|
+
| if $wanted and ($sent | not) then "owe" else "pass" end
|
|
76
|
+
' 2>/dev/null)"
|
|
77
|
+
|
|
78
|
+
[ "$verdict" = "owe" ] || exit 0
|
|
79
|
+
|
|
80
|
+
reason="BLOCKED by proposal-guard: владелец сказал завести или отправить предложение слою правил, а отправки в этом ходе не было. Написанное и не отправленное лежит в дереве неотличимо от отправленного: своей записи в слое правил у него нет, и владелец читает работу сделанной, пока не спросит прямо.
|
|
81
|
+
|
|
82
|
+
Предложение пишется файлом в \`$proposals_dir/\` и уезжает в тот же ход:
|
|
83
|
+
|
|
84
|
+
npx agent-kit propose
|
|
85
|
+
|
|
86
|
+
Сухой прогон отправкой не является: он показывает, что уехало бы, и следа наружу не оставляет. Отправка пишет отметки в файлы предложений и делает дерево грязным — при открытом PR они ложатся вторым коммитом в ту же ветку, и это их место, а не повод отложить.
|
|
87
|
+
|
|
88
|
+
Гард судит один ход: следующий заход не отбивается."
|
|
89
|
+
|
|
90
|
+
jq -n --arg r "$reason" '{decision:"block",reason:$r}' 2>/dev/null \
|
|
91
|
+
|| printf '{"decision":"block","reason":"proposal-guard: владелец просил предложение — отправь его командой пакета."}\n'
|
|
92
|
+
|
|
93
|
+
exit 0
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# rt-hook: PreToolUse Edit|Write|MultiEdit|mcp__webstorm__create_new_file
|
|
2
|
+
# rt-hook: PreToolUse Edit|Write|MultiEdit|Bash|mcp__webstorm__create_new_file|mcp__webstorm__execute_terminal_command|mcp__webstorm__execute_tool
|
|
3
3
|
# Требует: hooks/profile-check.sh
|
|
4
4
|
# Гард «ничего не пишется с нуля». PreToolUse на правке кода и разметки.
|
|
5
5
|
#
|
|
@@ -38,14 +38,60 @@ command -v jq >/dev/null 2>&1 || exit 0
|
|
|
38
38
|
command -v perl >/dev/null 2>&1 || exit 0
|
|
39
39
|
|
|
40
40
|
tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
|
|
41
|
+
shell_cmd=""
|
|
41
42
|
case "$tool" in
|
|
42
43
|
# Инструмент среды заводит файл теми же двумя данными, только называет их иначе — без этой
|
|
43
44
|
# ветки файл заводился мимо всех проверок.
|
|
44
45
|
Edit | Write | MultiEdit | mcp__webstorm__create_new_file) ;;
|
|
46
|
+
# Команда оболочки, которая пишет файл, — та же правка. Без этой ветки гард обходится
|
|
47
|
+
# сменой не инструмента, а способа записи; текстом правки тогда служит сама команда, и
|
|
48
|
+
# заведённое ею в heredoc читается наравне с телом правки. Разбор —
|
|
49
|
+
# `2026-08-15-guard-denied-shell-wrote-anyway.md`.
|
|
50
|
+
#
|
|
51
|
+
# Терминал среды исполняет ту же командную строку и кладёт её в то же поле: без этих двух
|
|
52
|
+
# имён гард стоял бы объявленным на них и молча пропускал — состояние хуже необъявленного,
|
|
53
|
+
# потому что снаружи выглядит закрытым.
|
|
54
|
+
Bash | mcp__webstorm__execute_terminal_command | mcp__webstorm__execute_tool)
|
|
55
|
+
shell_cmd="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null)"
|
|
56
|
+
[ -z "$shell_cmd" ] && exit 0
|
|
57
|
+
# Универсальный исполнитель прячет настоящую команду во вложенной строке: без её разбора
|
|
58
|
+
# путь стоит за кавычкой, и до него не дотягивается ни один образец.
|
|
59
|
+
if [ "$tool" = "mcp__webstorm__execute_tool" ] && command -v perl >/dev/null 2>&1; then
|
|
60
|
+
inner="$(printf '%s' "$shell_cmd" | perl -0ne '
|
|
61
|
+
if (/--command(?:=|\s+)(?:"((?:[^"\\]|\\.)*)"|\x27([^\x27]*)\x27|(.+))/s) {
|
|
62
|
+
print defined $1 ? $1 : (defined $2 ? $2 : $3);
|
|
63
|
+
}
|
|
64
|
+
' 2>/dev/null)"
|
|
65
|
+
[ -n "$inner" ] && shell_cmd="$inner"
|
|
66
|
+
fi
|
|
67
|
+
;;
|
|
45
68
|
*) exit 0 ;;
|
|
46
69
|
esac
|
|
47
70
|
|
|
48
|
-
|
|
71
|
+
# Профиль дерева: сперва умолчание пакета, поверх него — надстройка проекта, если она есть.
|
|
72
|
+
# Читается до разбора пути: пути из команды оболочки вынимает как раз профиль.
|
|
73
|
+
rt_hooks_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
74
|
+
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
|
|
75
|
+
# shellcheck disable=SC1090
|
|
76
|
+
[ -f "$profile" ] && . "$profile" 2>/dev/null
|
|
77
|
+
done
|
|
78
|
+
|
|
79
|
+
if [ -n "$shell_cmd" ]; then
|
|
80
|
+
command -v rt_shell_writes >/dev/null 2>&1 && command -v rt_shell_paths >/dev/null 2>&1 || exit 0
|
|
81
|
+
rt_shell_writes "$shell_cmd" || exit 0
|
|
82
|
+
# Из команды берётся первый путь, чьё расширение гарду интересно: остальные ему безразличны.
|
|
83
|
+
path=""
|
|
84
|
+
while IFS= read -r candidate; do
|
|
85
|
+
case "$candidate" in
|
|
86
|
+
*.html | *.scss | *.ts) path="$candidate"; break ;;
|
|
87
|
+
esac
|
|
88
|
+
done <<EOF
|
|
89
|
+
$(rt_shell_paths "$shell_cmd")
|
|
90
|
+
EOF
|
|
91
|
+
[ -z "$path" ] && exit 0
|
|
92
|
+
else
|
|
93
|
+
path="$(printf '%s' "$input" | jq -r '.tool_input.file_path // .tool_input.pathInProject // empty' 2>/dev/null)"
|
|
94
|
+
fi
|
|
49
95
|
# Путь от корня дерева приводится к абсолютному один раз, чтобы образцы не двоились.
|
|
50
96
|
case "$path" in
|
|
51
97
|
/*) ;;
|
|
@@ -56,13 +102,6 @@ case "$path" in
|
|
|
56
102
|
*) exit 0 ;;
|
|
57
103
|
esac
|
|
58
104
|
|
|
59
|
-
# Профиль дерева: сперва умолчание пакета, поверх него — надстройка проекта, если она есть.
|
|
60
|
-
rt_hooks_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
61
|
-
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
|
|
62
|
-
# shellcheck disable=SC1090
|
|
63
|
-
[ -f "$profile" ] && . "$profile" 2>/dev/null
|
|
64
|
-
done
|
|
65
|
-
|
|
66
105
|
# Слово о нехватке функции профиля: хук, вышедший молча, неотличим от работающего. Файл может
|
|
67
106
|
# быть не разложен — тогда остаётся прежнее поведение, молчаливое.
|
|
68
107
|
# shellcheck disable=SC1090
|
|
@@ -82,11 +121,16 @@ if [ -n "${RT_REUSE_SKIP_RE:-}" ] && printf '%s' "$path" | grep -qE "$RT_REUSE_S
|
|
|
82
121
|
exit 0
|
|
83
122
|
fi
|
|
84
123
|
|
|
85
|
-
# Только новый текст: строка, уже лежавшая в файле, этой правкой не заводилась.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
124
|
+
# Только новый текст: строка, уже лежавшая в файле, этой правкой не заводилась. У команды
|
|
125
|
+
# оболочки таким текстом служит она сама: что она кладёт в файл, лежит в ней же.
|
|
126
|
+
if [ -n "$shell_cmd" ]; then
|
|
127
|
+
added="$shell_cmd"
|
|
128
|
+
else
|
|
129
|
+
added="$(printf '%s' "$input" | jq -r '
|
|
130
|
+
[ .tool_input.content?, .tool_input.text?, .tool_input.new_string?, (.tool_input.edits[]?.new_string) ]
|
|
131
|
+
| map(select(. != null)) | join("\n")
|
|
132
|
+
' 2>/dev/null)"
|
|
133
|
+
fi
|
|
90
134
|
[ -z "$added" ] && exit 0
|
|
91
135
|
|
|
92
136
|
# Явный отказ от правила: готового такого нет, автор это осознал и пометил. Считается по
|
|
@@ -138,11 +182,19 @@ has_re() {
|
|
|
138
182
|
rt_checks_json="${CLAUDE_PROJECT_DIR:-.}/.claude/rt-kit/checks.json"
|
|
139
183
|
rt_bundles=''
|
|
140
184
|
rt_own_signals=''
|
|
185
|
+
rt_backend_roots=''
|
|
141
186
|
if [ -f "$rt_checks_json" ]; then
|
|
142
187
|
rt_bundles="$(jq -r '.reuse.bundles[]? // empty' "$rt_checks_json" 2>/dev/null | tr '\n' ' ')"
|
|
143
188
|
own="$(jq -r '.reuse.signals // empty' "$rt_checks_json" 2>/dev/null)"
|
|
144
189
|
[ -n "$own" ] && rt_own_signals="${CLAUDE_PROJECT_DIR:-.}/$own"
|
|
190
|
+
# Тем же ключом, каким их читает сплошная проверка: второе объявление тех же корней
|
|
191
|
+
# разошлось бы с первым молча.
|
|
192
|
+
rt_backend_roots="$(jq -r '.backendRoots[]? // empty' "$rt_checks_json" 2>/dev/null | tr '\n' ' ')"
|
|
145
193
|
fi
|
|
194
|
+
|
|
195
|
+
# Путь от корня дерева: образец в признаке пишет дерево, и писать его от корня машины оно не
|
|
196
|
+
# может. Абсолютный путь нужен только для чтения файла с диска.
|
|
197
|
+
rel_path="${path#"${CLAUDE_PROJECT_DIR:-.}"/}"
|
|
146
198
|
rt_signals_dir="${CLAUDE_PROJECT_DIR:-.}/${RT_REUSE_SIGNALS_DIR:-tools/signals}"
|
|
147
199
|
|
|
148
200
|
# Признаки объявленных наборов: те же файлы читает сплошная проверка. Ключ признака совпал с
|
|
@@ -172,9 +224,25 @@ while IFS= read -r signal; do
|
|
|
172
224
|
'') ;;
|
|
173
225
|
*) case "$path" in *"$ext") ;; *) continue ;; esac ;;
|
|
174
226
|
esac
|
|
227
|
+
# Пропуск корней бэкенда: признак, объявленный с ним, на бэкенде не действует вовсе — там
|
|
228
|
+
# принят другой способ, и базового класса, которого признак требует, у бэкенда нет. Поле
|
|
229
|
+
# читает и сплошная проверка; читать его одному из двоих значит отбивать гардом ту самую
|
|
230
|
+
# правку, которую проверка пропускает, — а провести её больше нечем: маркер отступления
|
|
231
|
+
# объявляет обход готового, а обхода тут не было.
|
|
232
|
+
if [ "$(field "$signal" '.skipBackendRoots')" = 'true' ] && [ -n "$rt_backend_roots" ]; then
|
|
233
|
+
skip_backend=''
|
|
234
|
+
for backend_root in $rt_backend_roots; do
|
|
235
|
+
case "$rel_path" in "$backend_root"*) skip_backend=1 ;; esac
|
|
236
|
+
done
|
|
237
|
+
[ -n "$skip_backend" ] && continue
|
|
238
|
+
fi
|
|
239
|
+
|
|
240
|
+
# Образец имени сверяется с путём от корня дерева, а не с именем файла: слои, которые
|
|
241
|
+
# признак и разделяет, зовут свои файлы одинаково, и по имени они неразличимы. Сплошная
|
|
242
|
+
# проверка сверяет с путём — расходиться им нельзя.
|
|
175
243
|
only_named="$(field "$signal" '.onlyNamed')"
|
|
176
244
|
if [ -n "$only_named" ]; then
|
|
177
|
-
printf '%s' "$
|
|
245
|
+
printf '%s' "$rel_path" | grep -qE "$only_named" || continue
|
|
178
246
|
fi
|
|
179
247
|
|
|
180
248
|
case "$(field "$signal" '.scope')" in
|
|
@@ -124,7 +124,7 @@ esac
|
|
|
124
124
|
|
|
125
125
|
# --- слой по пути: ведение работы -------------------------------------------------------------
|
|
126
126
|
#
|
|
127
|
-
# Папка задачи, договорённость о продукте до кода и
|
|
127
|
+
# Папка задачи, договорённость о продукте до кода и замысел эпика — первые файлы, которые
|
|
128
128
|
# заводятся в работе. Требование ловит на них того, кто пошёл мимо порядка: «пришла новая
|
|
129
129
|
# задача» инструментом не является, и поймать это больше нечем.
|
|
130
130
|
case "$target" in
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# rt-hook: PreToolUse Edit|Write|MultiEdit|Bash
|
|
2
|
+
# rt-hook: PreToolUse Edit|Write|MultiEdit|Bash|mcp__webstorm__create_new_file|mcp__webstorm__execute_terminal_command|mcp__webstorm__execute_tool
|
|
3
3
|
# Гейт правил: не даёт править файл, пока не загружено правило, под которое он подпадает.
|
|
4
4
|
#
|
|
5
5
|
# Закон и правило, которых никто не открывает, не действуют. Напоминание в подсказке помогает
|
|
@@ -90,6 +90,32 @@ case "$tool" in
|
|
|
90
90
|
fi
|
|
91
91
|
req="$(skill_for bash "$target" "" 2>/dev/null)"
|
|
92
92
|
kind="command"
|
|
93
|
+
# Команда оболочки, которая пишет файл, — та же правка, и правило ей нужно то же.
|
|
94
|
+
# Без этого яруса гейт обходится сменой не инструмента, а способа записи: отбитая
|
|
95
|
+
# правка легла командой дважды за один заход. Разбор —
|
|
96
|
+
# `2026-08-15-guard-denied-shell-wrote-anyway.md`.
|
|
97
|
+
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
|
|
98
|
+
# shellcheck disable=SC1090
|
|
99
|
+
[ -f "$profile" ] && . "$profile" 2>/dev/null
|
|
100
|
+
done
|
|
101
|
+
if command -v rt_shell_writes >/dev/null 2>&1 && command -v rt_shell_paths >/dev/null 2>&1 \
|
|
102
|
+
&& rt_shell_writes "$target"; then
|
|
103
|
+
while IFS= read -r written_path; do
|
|
104
|
+
[ -z "$written_path" ] && continue
|
|
105
|
+
case "$written_path" in
|
|
106
|
+
/*) ;;
|
|
107
|
+
*) written_path="${CLAUDE_PROJECT_DIR:-.}/$written_path" ;;
|
|
108
|
+
esac
|
|
109
|
+
case "$written_path" in
|
|
110
|
+
"${CLAUDE_PROJECT_DIR:-.}"/*) ;;
|
|
111
|
+
*) continue ;;
|
|
112
|
+
esac
|
|
113
|
+
more="$(skill_for edit "$written_path" "" 2>/dev/null)"
|
|
114
|
+
[ -n "$more" ] && req="$req $more"
|
|
115
|
+
done <<EOF
|
|
116
|
+
$(rt_shell_paths "$target")
|
|
117
|
+
EOF
|
|
118
|
+
fi
|
|
93
119
|
;;
|
|
94
120
|
mcp__claude-in-chrome__*)
|
|
95
121
|
req="$(skill_for browser "$tool" "" 2>/dev/null)"
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# rt-hook: PreToolUse Edit|Write|MultiEdit
|
|
2
|
+
# rt-hook: PreToolUse Edit|Write|MultiEdit|Bash|mcp__webstorm__create_new_file|mcp__webstorm__execute_terminal_command|mcp__webstorm__execute_tool
|
|
3
3
|
# Требует: hooks/profile-check.sh
|
|
4
4
|
# PreToolUse guard for Edit|Write|MultiEdit: код не пишется раньше замысла.
|
|
5
5
|
#
|
|
6
6
|
# Работа идёт много заходов, и между ними исполнитель не помнит ничего. Замысел, лежащий на
|
|
7
7
|
# диске, — единственное, что переживает перерыв: изменения к этому моменту бывают не
|
|
8
|
-
# закоммичены,
|
|
8
|
+
# закоммичены, PR не открыт, а очередь работ показывает задачу начатой и молчит о том, что
|
|
9
9
|
# внутри неё сделано.
|
|
10
10
|
#
|
|
11
11
|
# Гард требует три вещи и ровно их: папку задачи по имени ветки, замысел в ней и названную в
|
|
@@ -24,22 +24,8 @@ input="$(cat 2>/dev/null)"
|
|
|
24
24
|
[ -z "$input" ] && exit 0
|
|
25
25
|
command -v jq >/dev/null 2>&1 || exit 0
|
|
26
26
|
|
|
27
|
-
tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
|
|
28
|
-
case "$tool" in
|
|
29
|
-
# Инструмент редактора заводит файл теми же двумя данными, только называет их иначе —
|
|
30
|
-
# без этой ветки правка шла бы мимо гарда сменой инструмента.
|
|
31
|
-
Edit | Write | MultiEdit | mcp__webstorm__create_new_file) ;;
|
|
32
|
-
*) exit 0 ;;
|
|
33
|
-
esac
|
|
34
|
-
|
|
35
|
-
path="$(printf '%s' "$input" | jq -r '.tool_input.file_path // .tool_input.pathInProject // empty' 2>/dev/null)"
|
|
36
|
-
[ -z "$path" ] && exit 0
|
|
37
|
-
case "$path" in
|
|
38
|
-
/*) ;;
|
|
39
|
-
?*) path="${CLAUDE_PROJECT_DIR:-.}/$path" ;;
|
|
40
|
-
esac
|
|
41
|
-
|
|
42
27
|
# Профиль дерева: сперва умолчание пакета, поверх него — надстройка проекта, если она есть.
|
|
28
|
+
# Читается до разбора пути: пути из команды оболочки вынимает как раз профиль.
|
|
43
29
|
rt_hooks_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
44
30
|
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
|
|
45
31
|
# shellcheck disable=SC1090
|
|
@@ -52,12 +38,66 @@ done
|
|
|
52
38
|
[ -f "$rt_hooks_dir/profile-check.sh" ] && . "$rt_hooks_dir/profile-check.sh"
|
|
53
39
|
command -v rt_needs >/dev/null 2>&1 || rt_needs() { command -v "$1" >/dev/null 2>&1; }
|
|
54
40
|
|
|
41
|
+
tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
|
|
42
|
+
candidates=""
|
|
43
|
+
case "$tool" in
|
|
44
|
+
# Инструмент редактора заводит файл теми же двумя данными, только называет их иначе —
|
|
45
|
+
# без этой ветки правка шла бы мимо гарда сменой инструмента.
|
|
46
|
+
Edit | Write | MultiEdit | mcp__webstorm__create_new_file)
|
|
47
|
+
candidates="$(printf '%s' "$input" | jq -r '.tool_input.file_path // .tool_input.pathInProject // empty' 2>/dev/null)"
|
|
48
|
+
;;
|
|
49
|
+
# Второй ярус: та же правка, положенная командой оболочки. Без него отказ гарда обходится
|
|
50
|
+
# сменой не инструмента, а способа записи — перенаправлением, `sed -i`, интерпретатором с
|
|
51
|
+
# heredoc. Разбор — `2026-08-15-guard-denied-shell-wrote-anyway.md`.
|
|
52
|
+
#
|
|
53
|
+
# Терминал среды исполняет ту же командную строку и кладёт её в то же поле: без этих двух
|
|
54
|
+
# имён гард стоял бы объявленным на них и молча пропускал — состояние хуже необъявленного,
|
|
55
|
+
# потому что снаружи выглядит закрытым.
|
|
56
|
+
Bash | mcp__webstorm__execute_terminal_command | mcp__webstorm__execute_tool)
|
|
57
|
+
cmd="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null)"
|
|
58
|
+
[ -z "$cmd" ] && exit 0
|
|
59
|
+
# Универсальный исполнитель прячет настоящую команду во вложенной строке: без её разбора
|
|
60
|
+
# путь стоит за кавычкой, и до него не дотягивается ни один образец.
|
|
61
|
+
if [ "$tool" = "mcp__webstorm__execute_tool" ] && command -v perl >/dev/null 2>&1; then
|
|
62
|
+
inner="$(printf '%s' "$cmd" | perl -0ne '
|
|
63
|
+
if (/--command(?:=|\s+)(?:"((?:[^"\\]|\\.)*)"|\x27([^\x27]*)\x27|(.+))/s) {
|
|
64
|
+
print defined $1 ? $1 : (defined $2 ? $2 : $3);
|
|
65
|
+
}
|
|
66
|
+
' 2>/dev/null)"
|
|
67
|
+
[ -n "$inner" ] && cmd="$inner"
|
|
68
|
+
fi
|
|
69
|
+
rt_needs rt_shell_writes task-flow-guard || exit 0
|
|
70
|
+
rt_needs rt_shell_paths task-flow-guard || exit 0
|
|
71
|
+
rt_shell_writes "$cmd" || exit 0
|
|
72
|
+
candidates="$(rt_shell_paths "$cmd")"
|
|
73
|
+
;;
|
|
74
|
+
*) exit 0 ;;
|
|
75
|
+
esac
|
|
76
|
+
[ -z "$candidates" ] && exit 0
|
|
77
|
+
|
|
55
78
|
# Признак «правка меняет поведение» — путь, а не оценка на глаз: оценку назначает тот, кому
|
|
56
79
|
# она мешает, и порог плывёт. Где живёт код приложения, знает профиль: правила, тексты, обвязка
|
|
57
80
|
# и зависимости под требование не попадают — иначе разбор задачи нельзя было бы вести до
|
|
58
81
|
# заведения ветки.
|
|
59
82
|
rt_needs rt_is_app_code task-flow-guard || exit 0
|
|
60
|
-
|
|
83
|
+
|
|
84
|
+
# Судится каждый названный путь: команда пишет столько файлов, сколько в ней стоит, и одного
|
|
85
|
+
# под требованием довольно, чтобы отбить её целиком.
|
|
86
|
+
path=""
|
|
87
|
+
while IFS= read -r candidate; do
|
|
88
|
+
[ -z "$candidate" ] && continue
|
|
89
|
+
case "$candidate" in
|
|
90
|
+
/*) ;;
|
|
91
|
+
*) candidate="${CLAUDE_PROJECT_DIR:-.}/$candidate" ;;
|
|
92
|
+
esac
|
|
93
|
+
if rt_is_app_code "$candidate"; then
|
|
94
|
+
path="$candidate"
|
|
95
|
+
break
|
|
96
|
+
fi
|
|
97
|
+
done <<EOF
|
|
98
|
+
$candidates
|
|
99
|
+
EOF
|
|
100
|
+
[ -z "$path" ] && exit 0
|
|
61
101
|
|
|
62
102
|
# Каталог папок задач: у дерева он свой, но имя обычно общее.
|
|
63
103
|
tasks_dir="${RT_TASKS_DIR:-docs/tasks}"
|
|
@@ -113,7 +153,7 @@ fi
|
|
|
113
153
|
|
|
114
154
|
# Договорённость, влитая в спек домена, с диска уходит — так и задумано: в главной ветке
|
|
115
155
|
# директории «предложено» быть не должно. Но замысел на неё ссылается до конца работы, и без
|
|
116
|
-
# этой развилки последний коммит
|
|
156
|
+
# этой развилки последний коммит PR запирал бы ветку: ни правки по замечаниям разбора, ни
|
|
117
157
|
# записи в журнал изменений после вливания уже не сделать.
|
|
118
158
|
#
|
|
119
159
|
# Влитое от незаведённого отличает история ветки: путь, которого в ней никогда не было,
|