@rt-tools/agent-kit 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/checks/board-runs.github.mjs +87 -0
- package/assets/checks/board.github.mjs +0 -40
- package/assets/checks/check-board.github.mjs +39 -8
- package/assets/checks/check-schema-drift.mjs +28 -5
- package/assets/checks/rt-kit-checks.config.mjs +12 -0
- package/assets/commands/feedback.md +8 -0
- package/assets/defaults/project.sh +50 -5
- package/assets/hooks/browser-guard-no-other-drivers.sh +1 -1
- package/assets/hooks/dev-server-guard.sh +1 -1
- package/assets/hooks/git-guard-delivery-folder.sh +99 -0
- package/assets/hooks/git-guard-delivery-signature.sh +10 -4
- package/assets/hooks/git-guard-delivery.sh +21 -69
- package/assets/hooks/git-guard-push-tests.sh +2 -2
- package/assets/hooks/hook-input.sh +23 -0
- package/assets/hooks/override-write-guard.sh +107 -0
- package/assets/hooks/rerun-guard.sh +1 -1
- package/assets/hooks/rule-source-guard.sh +126 -0
- package/assets/hooks/task-flow-guard.sh +18 -0
- package/assets/hooks/turn-exit-guard.sh +17 -0
- package/assets/patterns/git-workflow-pr.azure.md +4 -4
- package/assets/patterns/git-workflow-pr.github.md +4 -4
- package/assets/patterns/git-workflow-pr.gitlab.md +4 -4
- package/assets/patterns/task-flow-archive.md +23 -21
- package/assets/patterns/task-flow-close.md +98 -96
- package/assets/patterns/task-flow-resume.md +5 -3
- package/assets/pitfalls/agent-kit.md +80 -0
- package/assets/rules/deploy-flow.azure.md +15 -7
- package/assets/rules/deploy-flow.github.md +15 -6
- package/assets/rules/deploy-flow.gitlab.md +15 -7
- package/assets/rules/git-workflow.github.md +6 -1
- package/assets/rules/task-flow.md +41 -25
- package/assets/rules/turn-conduct.md +4 -0
- package/assets/skills/agent-kit.md +32 -77
- package/assets/templates/proposal.md +16 -1
- package/lib/proposals.d.ts +5 -1
- package/lib/proposals.d.ts.map +1 -1
- package/lib/proposals.js +74 -5
- package/lib/proposals.js.map +1 -1
- package/lib/shipment.d.ts.map +1 -1
- package/lib/shipment.fixture.d.ts +39 -0
- package/lib/shipment.fixture.d.ts.map +1 -0
- package/lib/shipment.fixture.js +99 -0
- package/lib/shipment.fixture.js.map +1 -0
- package/lib/shipment.js +59 -7
- package/lib/shipment.js.map +1 -1
- package/package.json +1 -1
- package/rt-tools-agent-kit-0.13.0.tgz +0 -0
- package/rt-tools-agent-kit-0.12.0.tgz +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# rt-hook: PreToolUse Bash|mcp__webstorm__execute_terminal_command|mcp__webstorm__execute_tool
|
|
3
|
-
# Требует: hooks/profile-check.sh, hooks/deny-tail.sh
|
|
3
|
+
# Требует: hooks/git-guard-delivery-folder.sh, hooks/profile-check.sh, hooks/deny-tail.sh
|
|
4
4
|
# Гард поставки. PreToolUse на заведении ветки, пуше и открытии заявки на слияние.
|
|
5
5
|
#
|
|
6
6
|
# Закон о поставке требует трёх вещей, которых обычно не проверяет ничто: правка начинается с
|
|
@@ -152,6 +152,12 @@ fault() {
|
|
|
152
152
|
}— $1"
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
# Условия о папке задачи вынесены соседним файлом: у папки свой предмет, свой обход и своя
|
|
156
|
+
# запись в архиве, а вместе с разбором команды и очередью работ они переросли предел длины.
|
|
157
|
+
# Подключается он после `deny` и `fault` — обе зовутся изнутри.
|
|
158
|
+
# shellcheck disable=SC1090
|
|
159
|
+
[ -f "$rt_hooks_dir/git-guard-delivery-folder.sh" ] && . "$rt_hooks_dir/git-guard-delivery-folder.sh" 2>/dev/null
|
|
160
|
+
|
|
155
161
|
# Отказ по накопленному. Пусто — вызывающая сторона идёт дальше.
|
|
156
162
|
deny_faults() {
|
|
157
163
|
[ -z "$faults" ] && return 0
|
|
@@ -161,21 +167,6 @@ deny_faults() {
|
|
|
161
167
|
${faults}"
|
|
162
168
|
}
|
|
163
169
|
|
|
164
|
-
# Подсказка вместо отказа: на открытии PR папка ещё нужна. Решения подсказка не несёт,
|
|
165
|
-
# команда идёт дальше своим ходом.
|
|
166
|
-
hint() {
|
|
167
|
-
jq -n --arg c "$1" '{hookSpecificOutput:{hookEventName:"PreToolUse",additionalContext:$c}}' 2>/dev/null
|
|
168
|
-
exit 0
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
# Есть ли папка задачи в ветке. Смотрим содержимое ветки, а не рабочее дерево: если папку
|
|
172
|
-
# удалили, но не закоммитили, проверка прошла бы, а папка всё равно уехала бы в main. Имя
|
|
173
|
-
# ветки подставляем целиком, вместе с косой: у ветки вида `chore/312-slug` папка лежит во
|
|
174
|
-
# вложенном каталоге.
|
|
175
|
-
folder_in_branch() {
|
|
176
|
-
git ls-tree -d --name-only HEAD -- "$1" 2>/dev/null | head -1
|
|
177
|
-
}
|
|
178
|
-
|
|
179
170
|
check_task() {
|
|
180
171
|
number="$1"
|
|
181
172
|
where="$2"
|
|
@@ -292,7 +283,7 @@ command -v rt_delivery_signature >/dev/null 2>&1 && rt_delivery_signature
|
|
|
292
283
|
#
|
|
293
284
|
# Возврат заявки в черновик под требование не подпадает: он делает ровно то, чего гард и
|
|
294
285
|
# добивается, — снимает с работы вид готовой.
|
|
295
|
-
if printf '%s' "$cmd" | grep -qE
|
|
286
|
+
if printf '%s' "$cmd" | grep -qE "${RT_CMD_BOUND}(gh[[:space:]]+pr[[:space:]]+ready|glab[[:space:]]+mr[[:space:]]+update[^|;&]*--ready)([[:space:]]|\$)" \
|
|
296
287
|
&& ! printf '%s' "$cmd" | grep -q -- '--undo'; then
|
|
297
288
|
# Ссылка на заявку необязательна: без неё клиент берёт заявку текущей ветки, и это самая
|
|
298
289
|
# короткая форма вызова. Требовать номер значило бы снимать всё требование одним пробелом.
|
|
@@ -341,59 +332,20 @@ fi
|
|
|
341
332
|
|
|
342
333
|
# --- слияние заявки ----------------------------------------------------------------------
|
|
343
334
|
#
|
|
344
|
-
#
|
|
345
|
-
# работа перешла к следующей задаче, а PR закрыт. Раньше слияния требовать нельзя — пока идёт
|
|
346
|
-
# ревью, plan.md нужен на диске, иначе гард хода работы не даст править код.
|
|
335
|
+
# Второй рубеж того же условия, что стоит на открытии заявки: он ловит слияние, идущее командой.
|
|
347
336
|
# Команду ищем от начала строки или после разделителя, а не где угодно в тексте. Иначе гард
|
|
348
337
|
# отбивает сообщение, где `gh pr merge` просто упомянут в кавычках, — так он и сработал на
|
|
349
338
|
# правке этого же текста. Полностью подстроку в кавычках так не отсечь, но случайное упоминание
|
|
350
339
|
# внутри слова или пути мимо уже не пройдёт.
|
|
351
|
-
if printf '%s' "$cmd" | grep -qE
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
merge_branch="$(git branch --show-current 2>/dev/null)"
|
|
355
|
-
[ -z "$merge_branch" ] && exit 0
|
|
356
|
-
rt_task_branch_ok "$merge_branch" || exit 0 # за беззадачной веткой папки не стоит
|
|
357
|
-
|
|
358
|
-
folder="$tasks_dir/$merge_branch"
|
|
359
|
-
|
|
360
|
-
# Сначала ищем обход в самой команде — это работает и без сети. Если читать только
|
|
361
|
-
# тело PR, то без сети гард отбил бы слияние, причина которого в этом теле и написана.
|
|
362
|
-
printf '%s' "$cmd" | grep -qiE "$folder_skip_re" && exit 0
|
|
363
|
-
|
|
364
|
-
merge_number="$(printf '%s' "$cmd" | sed -nE 's/.*(pr|mr)[[:space:]]+(merge|update)[[:space:]]+([0-9]+).*/\3/p' | head -1)"
|
|
365
|
-
if [ -n "$merge_number" ] && rt_needs rt_report_body git-guard-delivery; then
|
|
366
|
-
body="$(cd "$root" && rt_report_body "$merge_number" 2>/dev/null)"
|
|
367
|
-
[ -n "$body" ] && printf '%s' "$body" | grep -qiE "$folder_skip_re" && exit 0
|
|
368
|
-
fi
|
|
369
|
-
|
|
370
|
-
lying="$(folder_in_branch "$folder")"
|
|
371
|
-
[ -n "$lying" ] \
|
|
372
|
-
&& deny "BLOCKED: в ветке осталась папка задачи «${lying}» — она уедет в главную. Разобрать её потом будет некому: работа перейдёт к следующей задаче, а этот PR закроется. Перенеси в «${archive_dir:-архив}» то, что объясняет принятые решения, остальное удали и повтори. Если работа вливается частями, поставь в тело PR строку «Task-folder-skip: <причина>»."
|
|
373
|
-
|
|
374
|
-
# Запись в архиве спрашиваем только у ветки, которая папку удалила. Иначе проверка
|
|
375
|
-
# цеплялась бы к работе, у которой папки и не было. Без общего предка с главной веткой
|
|
376
|
-
# сравнивать не с чем — тогда молчим.
|
|
377
|
-
[ -n "$archive_dir" ] || exit 0
|
|
378
|
-
base="$(git merge-base "$main_branch" HEAD 2>/dev/null)"
|
|
379
|
-
[ -z "$base" ] && exit 0
|
|
380
|
-
|
|
381
|
-
had="$(git ls-tree -d --name-only "$base" -- "$folder" 2>/dev/null | head -1)"
|
|
382
|
-
[ -z "$had" ] && had="$(git log "$base..HEAD" --diff-filter=A --name-only --pretty=format: -- "$folder" 2>/dev/null | head -1)"
|
|
383
|
-
[ -z "$had" ] && exit 0
|
|
384
|
-
|
|
385
|
-
gained="$(git diff --name-only --diff-filter=A "$base" HEAD -- "$archive_dir" 2>/dev/null | head -1)"
|
|
386
|
-
[ -z "$gained" ] \
|
|
387
|
-
&& deny "BLOCKED: папку задачи удалили, но в «${archive_dir}» ветка ничего не добавила. Удалить проще, чем разобрать, — и вместе с папкой пропадает разбор просьбы, единственная запись слов владельца. Перенеси то, что объясняет принятые решения, одним файлом с понятным именем и повтори."
|
|
388
|
-
|
|
389
|
-
exit 0
|
|
340
|
+
if printf '%s' "$cmd" | grep -qE "${RT_CMD_BOUND}(gh[[:space:]]+pr[[:space:]]+merge|glab[[:space:]]+mr[[:space:]]+merge|az[[:space:]]+repos[[:space:]]+pr[[:space:]]+update)([[:space:]]|\$)"; then
|
|
341
|
+
rt_delivery_merge_folder
|
|
390
342
|
fi
|
|
391
343
|
|
|
392
344
|
# --- открытие заявки на слияние ----------------------------------------------------------
|
|
393
345
|
# Команду ищем от начала строки или после разделителя — по той же причине, что и слияние:
|
|
394
346
|
# упоминание в кавычках командой не является.
|
|
395
347
|
printf '%s' "$cmd" \
|
|
396
|
-
| grep -qE
|
|
348
|
+
| grep -qE "${RT_CMD_BOUND}(gh[[:space:]]+pr[[:space:]]+create|glab[[:space:]]+mr[[:space:]]+create|az[[:space:]]+repos[[:space:]]+pr[[:space:]]+create)([[:space:]]|\$)" \
|
|
397
349
|
|| exit 0
|
|
398
350
|
|
|
399
351
|
branch="$(git branch --show-current 2>/dev/null)"
|
|
@@ -474,17 +426,17 @@ fi
|
|
|
474
426
|
|
|
475
427
|
check_task "$number" "заявка с ветки «${branch}»" да
|
|
476
428
|
|
|
429
|
+
# Папка задачи разбирается до открытия заявки, а не после одобрения. Прежде здесь стояло
|
|
430
|
+
# напоминание, а требование — на слиянии: считалось, что папка нужна на диске всё время
|
|
431
|
+
# разбора, потому что правку по замечаниям без замысла отбивает гард хода работы. Владелец же
|
|
432
|
+
# вливает, как только видит зелёное, и закрывающему коммиту места не остаётся — трижды подряд
|
|
433
|
+
# папка уехала в главную неразобранной, а гард слияния этого не видел вовсе: кнопку нажимает
|
|
434
|
+
# человек на хостинге, и до него гард не достаёт. Разбор переехал сюда, а замысел после уборки
|
|
435
|
+
# гард хода работы берёт из истории ветки, а не с диска.
|
|
436
|
+
rt_delivery_open_folder
|
|
437
|
+
|
|
477
438
|
# Всё несошедшееся названо здесь, разом: до этой строки собирались условия, каждое из которых
|
|
478
439
|
# прежде отбивало вызов в одиночку.
|
|
479
440
|
deny_faults
|
|
480
441
|
|
|
481
|
-
# Сейчас папка ещё нужна: правки по замечаниям ревью идут в эту же ветку, а без plan.md их не
|
|
482
|
-
# пропустит гард хода работы. Поэтому здесь только напоминание. Требование стоит на слиянии —
|
|
483
|
-
# там папка уже не нужна, а вред от неё как раз и наступает.
|
|
484
|
-
if [ -n "$tasks_dir" ]; then
|
|
485
|
-
lying="$(folder_in_branch "$tasks_dir/$branch")"
|
|
486
|
-
[ -n "$lying" ] \
|
|
487
|
-
&& hint "В ветке лежит папка задачи «${lying}». Разбери её до слияния, этим же PR: потом за неё уже никто не возьмётся. На слиянии это будет отказ, а не напоминание."
|
|
488
|
-
fi
|
|
489
|
-
|
|
490
442
|
exit 0
|
|
@@ -46,7 +46,7 @@ cmd="$(rt_hook_cmd)"
|
|
|
46
46
|
# подстрокой «git push» пуш не поймать — помощник учётных данных и заголовок запроса ставятся
|
|
47
47
|
# ключами `-c` между ними, и ровно этой формой здесь и пушат. Пока признаком была подстрока,
|
|
48
48
|
# весь набор гейта на таком пуше не гонялся вовсе, а молчание гарда читалось как «зелено».
|
|
49
|
-
printf '%s' "$cmd" | grep -qE
|
|
49
|
+
printf '%s' "$cmd" | grep -qE "${RT_CMD_BOUND}git([[:space:]]|\$)" || exit 0
|
|
50
50
|
|
|
51
51
|
# Отложенная правка пушем не бывает: `git stash push` кладёт правку в тайник этой же машины и
|
|
52
52
|
# наружу не отправляет ничего. Слово `push` в ней стоит отдельным, и без этой строки гард гонял
|
|
@@ -71,7 +71,7 @@ esac
|
|
|
71
71
|
#
|
|
72
72
|
# Судится переключение на существующую ветку. Заведение новой (`checkout -b`, `switch -c`)
|
|
73
73
|
# сюда не попадает: у свежей ветки дерево то же самое, что и было.
|
|
74
|
-
if printf '%s' "$cmd" | grep -qE
|
|
74
|
+
if printf '%s' "$cmd" | grep -qE "${RT_CMD_BOUND}git[[:space:]]+(checkout|switch)[[:space:]]+" &&
|
|
75
75
|
! printf '%s' "$cmd" | grep -qE 'git[[:space:]]+(checkout[[:space:]]+-b|switch[[:space:]]+-c)([[:space:]]|$)'; then
|
|
76
76
|
reason="BLOCKED: переключение ветки и пуш одной командой. Набор гейта гоняется в том дереве, какое лежит на момент разбора команды, — то есть по ПРЕЖНЕЙ ветке, а не по той, что уходит на хостинг. Зелёный набор при этом читается как проверка ушедшего, хотя проверял он другое. Раздели вызовы: сперва переключись, затем отдельной командой пушь."
|
|
77
77
|
# Общий хвост отказа: два законных хода и законная форма обхода, если она у отказа есть.
|
|
@@ -47,6 +47,29 @@ rt_hook_field() {
|
|
|
47
47
|
printf '%s' "$RT_HOOK_INPUT" | jq -r "${path} // empty" 2>/dev/null
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
# НАЧАЛО ВЫЗОВА В КОМАНДНОЙ СТРОКЕ. Приставка образца, которым гард узнаёт свой вызов: команда
|
|
51
|
+
# стоит в начале строки, за разделителем — и после любого числа присваиваний переменных
|
|
52
|
+
# окружения перед её именем.
|
|
53
|
+
#
|
|
54
|
+
# Присваивания входят сюда потому, что вызов с ними — обычная форма, и часть набора требует её
|
|
55
|
+
# прямо: личность вызова, открывающего заявку, видна из команды только явной подстановкой
|
|
56
|
+
# токена. Признак, не знавший о присваиваниях, на этой форме слепнул молча — он не отказывал и
|
|
57
|
+
# не предупреждал, он не считал вызов вызовом, и весь предмет гарда оставался несудимым. Так
|
|
58
|
+
# прошли мимо своих гардов пуш, открытие заявки, снятие черновика и слияние: коммит с чужой
|
|
59
|
+
# подписью уехал в главную ветку, а запрет на слияние агентом снимался подстановкой токена.
|
|
60
|
+
#
|
|
61
|
+
# Значение присваивания берётся без пробелов: `ИМЯ="a b"` в кавычках сюда не подпадает, и вызов
|
|
62
|
+
# за ним останется несудимым. Пробел внутри значения перед вызовом встречается редко, а
|
|
63
|
+
# образец, разбирающий кавычки, ошибается в другую сторону — считает вызовом то, что им не
|
|
64
|
+
# является. Признак обязан ошибаться в сторону лишнего срабатывания: гард, не узнавший вызов,
|
|
65
|
+
# молчит и выглядит исправным, а сработавший на лишнем виден сразу и правится.
|
|
66
|
+
#
|
|
67
|
+
# Само по себе присваивание вызовом не считается: за ним обязано стоять имя команды.
|
|
68
|
+
#
|
|
69
|
+
# Объявлено это одним местом, а не литералом в каждом гарде: разойдясь, копии чинятся по одной
|
|
70
|
+
# и молчат о том, что остальные остались слепыми.
|
|
71
|
+
RT_CMD_BOUND='(^|[;&|(]|&&|\|\|)[[:space:]]*([A-Za-z_][A-Za-z0-9_]*=[^[:space:]]*[[:space:]]+)*'
|
|
72
|
+
|
|
50
73
|
# Поля, которые спрашивают все гарды.
|
|
51
74
|
rt_hook_tool() { rt_hook_field RT_HOOK_TOOL '.tool_name'; }
|
|
52
75
|
rt_hook_cmd() { rt_hook_field RT_HOOK_CMD '.tool_input.command'; }
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# rt-hook: PreToolUse Write|Bash|mcp__webstorm__create_new_file|mcp__webstorm__execute_terminal_command|mcp__webstorm__execute_tool
|
|
3
|
+
# Требует: hooks/deny-tail.sh
|
|
4
|
+
# Гард затирания надстройки: запись поверх — не то же самое, что правка.
|
|
5
|
+
#
|
|
6
|
+
# Надстройка дерева сливается с ресурсом пакета по разделу «## »: раздел, который дерево
|
|
7
|
+
# переписало, замещает пакетный, а остальные приходят из пакета. Файл надстройки поэтому
|
|
8
|
+
# накапливается — разделы в него дописывают разные ветки и разные заходы, — и выглядит он как
|
|
9
|
+
# обычный текст, который можно положить целиком.
|
|
10
|
+
#
|
|
11
|
+
# Положенный целиком, он уносит с собой все разделы, которых эта правка не касалась. Пропажу не
|
|
12
|
+
# видно ничем: раскладка сходится, проверки зелёные, а пакетный раздел молча вернулся на место
|
|
13
|
+
# того, что дерево о себе говорило. Заметит это тот, кто через месяц удивится, почему правило
|
|
14
|
+
# снова требует чужих имён.
|
|
15
|
+
#
|
|
16
|
+
# ЧТО СУДИТСЯ. Запись файла целиком поверх существующей непустой надстройки: инструмент записи,
|
|
17
|
+
# перенаправление `>`, `tee` без дописывания, копирование поверх. Дописывание в конец — `>>`,
|
|
18
|
+
# `tee -a` — и правка по месту проходят: они ничего не уносят.
|
|
19
|
+
#
|
|
20
|
+
# ОТКАЗ В ПОЛЬЗУ РАБОТЫ: нет `jq`, битый ввод, чужой инструмент, файла нет, файл пуст —
|
|
21
|
+
# правка РАЗРЕШАЕТСЯ. Сломанный гард не имеет права заклинить работу.
|
|
22
|
+
|
|
23
|
+
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
|
|
24
|
+
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
|
|
25
|
+
|
|
26
|
+
rt_hook_read
|
|
27
|
+
input="$RT_HOOK_INPUT"
|
|
28
|
+
[ -z "$input" ] && exit 0
|
|
29
|
+
command -v jq >/dev/null 2>&1 || exit 0
|
|
30
|
+
|
|
31
|
+
rt_hooks_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
32
|
+
|
|
33
|
+
# shellcheck disable=SC1090
|
|
34
|
+
[ -f "$rt_hooks_dir/deny-tail.sh" ] && . "$rt_hooks_dir/deny-tail.sh" 2>/dev/null
|
|
35
|
+
command -v rt_deny_tail >/dev/null 2>&1 || rt_deny_tail() { printf ''; }
|
|
36
|
+
|
|
37
|
+
deny() {
|
|
38
|
+
# shellcheck disable=SC1090
|
|
39
|
+
[ -f "$rt_hooks_dir/observe.sh" ] && . "$rt_hooks_dir/observe.sh" 2>/dev/null
|
|
40
|
+
command -v rt_note >/dev/null 2>&1 && rt_note guard-deny res=override-write-guard
|
|
41
|
+
|
|
42
|
+
reason="$1 $(rt_deny_tail "$2")"
|
|
43
|
+
jq -n --arg r "$reason" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$r}}' 2>/dev/null \
|
|
44
|
+
|| printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Гард затирания надстройки."}}\n'
|
|
45
|
+
exit 0
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# Цели записи поверх, названные командой прямо. Дописывание сюда не входит: `>>` прячется до
|
|
49
|
+
# разбора и обратно не разворачивается, `tee` берётся только без довода о дописывании.
|
|
50
|
+
rt_overwrite_targets() {
|
|
51
|
+
tr "\"'\`" ' ' \
|
|
52
|
+
| sed -E 's/>>/\
|
|
53
|
+
APPEND/g' \
|
|
54
|
+
| sed -E 's/>/\
|
|
55
|
+
>/g' \
|
|
56
|
+
| sed -nE '
|
|
57
|
+
s/^>[[:space:]]*([^[:space:]|&;]+).*/\1/p
|
|
58
|
+
s/(^|.*[[:space:]])tee[[:space:]]+([^-[:space:]][^[:space:]|&;]*).*/\2/p
|
|
59
|
+
s/(^|.*[[:space:]])(cp|mv|install)[[:space:]]+([^[:space:]]+[[:space:]]+)+([^[:space:]|&;]+).*/\4/p
|
|
60
|
+
' \
|
|
61
|
+
| sort -u
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
tool="$(rt_hook_tool)"
|
|
65
|
+
candidates=""
|
|
66
|
+
case "$tool" in
|
|
67
|
+
Write | mcp__webstorm__create_new_file)
|
|
68
|
+
candidates="$(printf '%s' "$input" | jq -r '.tool_input.file_path // .tool_input.pathInProject // empty' 2>/dev/null)"
|
|
69
|
+
;;
|
|
70
|
+
Bash | mcp__webstorm__execute_terminal_command | mcp__webstorm__execute_tool)
|
|
71
|
+
cmd="$(rt_hook_cmd)"
|
|
72
|
+
[ -z "$cmd" ] && exit 0
|
|
73
|
+
candidates="$(printf '%s' "$cmd" | rt_overwrite_targets)"
|
|
74
|
+
;;
|
|
75
|
+
*) exit 0 ;;
|
|
76
|
+
esac
|
|
77
|
+
[ -z "$candidates" ] && exit 0
|
|
78
|
+
|
|
79
|
+
root="${CLAUDE_PROJECT_DIR:-.}"
|
|
80
|
+
overrides_dir="${RT_OVERRIDES_DIR:-.claude/rt-kit/overrides}"
|
|
81
|
+
|
|
82
|
+
while IFS= read -r candidate; do
|
|
83
|
+
[ -z "$candidate" ] && continue
|
|
84
|
+
case "$candidate" in
|
|
85
|
+
/*) relative="${candidate#"$root"/}" ;;
|
|
86
|
+
*)
|
|
87
|
+
relative="$candidate"
|
|
88
|
+
candidate="$root/$candidate"
|
|
89
|
+
;;
|
|
90
|
+
esac
|
|
91
|
+
case "$relative" in "$overrides_dir"/*) ;; *) continue ;; esac
|
|
92
|
+
[ -f "$candidate" ] || continue
|
|
93
|
+
|
|
94
|
+
lines="$(wc -l <"$candidate" 2>/dev/null | tr -d ' ')"
|
|
95
|
+
[ -z "$lines" ] && continue
|
|
96
|
+
[ "$lines" -eq 0 ] && continue
|
|
97
|
+
|
|
98
|
+
sections="$(grep -c '^## ' "$candidate" 2>/dev/null || printf '0')"
|
|
99
|
+
resource="${relative#"$overrides_dir"/}"
|
|
100
|
+
|
|
101
|
+
deny "BLOCKED by override-write-guard: «${relative}» — надстройка этого дерева, и в ней уже лежит ${lines} строк, разделов «## » — ${sections}. Запись целиком уносит все разделы, которых эта правка не касалась, и на их место молча возвращается пакетный текст ресурса «${resource}»: раскладка после этого сходится, проверки зелёные, а сказанного деревом о себе больше нет. Правь по месту — правкой раздела, а не записью файла." \
|
|
102
|
+
"новый раздел дописывается в конец: надстройка сливается с пакетным ресурсом по заголовку «## », и пакетный текст замещается только теми разделами, которые дерево назвало"
|
|
103
|
+
done <<EOF
|
|
104
|
+
$candidates
|
|
105
|
+
EOF
|
|
106
|
+
|
|
107
|
+
exit 0
|
|
@@ -49,7 +49,7 @@ done
|
|
|
49
49
|
|
|
50
50
|
# Вызов перезапуска: слово перезапуска отдельным словом рядом со словом прогона.
|
|
51
51
|
host_cli="${RT_HOST_CLI:-gh}"
|
|
52
|
-
printf '%s' "$cmd" | grep -qE "
|
|
52
|
+
printf '%s' "$cmd" | grep -qE "${RT_CMD_BOUND}${host_cli}([[:space:]]|\$)" || exit 0
|
|
53
53
|
printf '%s' "$cmd" | grep -qE '(^|[[:space:]])(run|workflow)([[:space:]]|$)' || exit 0
|
|
54
54
|
printf '%s' "$cmd" | grep -qE '(^|[[:space:]])rerun([[:space:]]|$)' || exit 0
|
|
55
55
|
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# rt-hook: PreToolUse Edit|Write|MultiEdit|Bash|mcp__webstorm__create_new_file|mcp__webstorm__execute_terminal_command|mcp__webstorm__execute_tool
|
|
3
|
+
# Требует: hooks/profile-check.sh, hooks/deny-tail.sh
|
|
4
|
+
# Гард места правки: слой правил чинится там, где сломано, а не там, где виден.
|
|
5
|
+
#
|
|
6
|
+
# Слой правил правит тот же исполнитель, которым слой правил управляет, и разницы между
|
|
7
|
+
# «исполняю правило» и «правлю правило» в дереве не видно ничем: разложенная копия лежит рядом
|
|
8
|
+
# с обычными файлами и правится так же. Правка доходит до диска и не доходит до места, где
|
|
9
|
+
# промах чинится: в дереве она видна, в пакете её нет, а через месяц раскладка отказывает по
|
|
10
|
+
# правленому файлу целиком — и цену платит тот, кто в этот день правил соседний ресурс.
|
|
11
|
+
#
|
|
12
|
+
# Знал об этом только `sync --check`, и говорил он на следующей раскладке, то есть в чужой
|
|
13
|
+
# ветке и чужим ходом. Гард говорит в минуту правки и называет адрес: источник пакета, если
|
|
14
|
+
# дерево его держит, иначе — надстройку.
|
|
15
|
+
#
|
|
16
|
+
# ЧТО СУДИТСЯ. Файл, который правят, несёт шапку раскладки: `rt-kit v<версия> · <ресурс> ·
|
|
17
|
+
# <дайджест>`. Ресурс из неё и есть адрес: `<источники>/<ресурс>` в дереве пакета,
|
|
18
|
+
# `.claude/rt-kit/overrides/<ресурс>` у потребителя.
|
|
19
|
+
#
|
|
20
|
+
# ОТКАЗ В ПОЛЬЗУ РАБОТЫ: нет `jq`, битый ввод, чужой инструмент, файла нет, шапки в нём нет,
|
|
21
|
+
# не git-репозиторий — правка РАЗРЕШАЕТСЯ. Сломанный гард не имеет права заклинить работу.
|
|
22
|
+
|
|
23
|
+
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
|
|
24
|
+
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
|
|
25
|
+
|
|
26
|
+
rt_hook_read
|
|
27
|
+
input="$RT_HOOK_INPUT"
|
|
28
|
+
[ -z "$input" ] && exit 0
|
|
29
|
+
command -v jq >/dev/null 2>&1 || exit 0
|
|
30
|
+
|
|
31
|
+
rt_hooks_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
32
|
+
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
|
|
33
|
+
# shellcheck disable=SC1090
|
|
34
|
+
[ -f "$profile" ] && . "$profile" 2>/dev/null
|
|
35
|
+
done
|
|
36
|
+
|
|
37
|
+
# shellcheck disable=SC1090
|
|
38
|
+
[ -f "$rt_hooks_dir/profile-check.sh" ] && . "$rt_hooks_dir/profile-check.sh"
|
|
39
|
+
command -v rt_needs >/dev/null 2>&1 || rt_needs() { command -v "$1" >/dev/null 2>&1; }
|
|
40
|
+
|
|
41
|
+
# shellcheck disable=SC1090
|
|
42
|
+
[ -f "$rt_hooks_dir/deny-tail.sh" ] && . "$rt_hooks_dir/deny-tail.sh" 2>/dev/null
|
|
43
|
+
command -v rt_deny_tail >/dev/null 2>&1 || rt_deny_tail() { printf ''; }
|
|
44
|
+
|
|
45
|
+
deny() {
|
|
46
|
+
# shellcheck disable=SC1090
|
|
47
|
+
[ -f "$rt_hooks_dir/observe.sh" ] && . "$rt_hooks_dir/observe.sh" 2>/dev/null
|
|
48
|
+
command -v rt_note >/dev/null 2>&1 && rt_note guard-deny res=rule-source-guard
|
|
49
|
+
|
|
50
|
+
reason="$1 $(rt_deny_tail "$2")"
|
|
51
|
+
jq -n --arg r "$reason" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$r}}' 2>/dev/null \
|
|
52
|
+
|| printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Гард места правки."}}\n'
|
|
53
|
+
exit 0
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
# Цели записи, названные командой прямо. Печатает по одной в строке.
|
|
57
|
+
rt_write_targets() {
|
|
58
|
+
tr "\"'\`" ' ' \
|
|
59
|
+
| sed -E 's/>>?/\n>/g' \
|
|
60
|
+
| sed -nE '
|
|
61
|
+
s/^>[[:space:]]*([^[:space:]|&;]+).*/\1/p
|
|
62
|
+
s/(^|.*[[:space:]])tee[[:space:]]+(-a[[:space:]]+)?([^[:space:]|&;]+).*/\3/p
|
|
63
|
+
s/(^|.*[[:space:]])sed[[:space:]]+-i[[:space:]]+([^[:space:]]+[[:space:]]+)*([^[:space:]|&;]+)$/\3/p
|
|
64
|
+
s/(^|.*[[:space:]])(cp|mv|install)[[:space:]]+([^[:space:]]+[[:space:]]+)+([^[:space:]|&;]+).*/\4/p
|
|
65
|
+
' \
|
|
66
|
+
| sort -u
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
tool="$(rt_hook_tool)"
|
|
70
|
+
candidates=""
|
|
71
|
+
case "$tool" in
|
|
72
|
+
Edit | Write | MultiEdit | mcp__webstorm__create_new_file)
|
|
73
|
+
candidates="$(printf '%s' "$input" | jq -r '.tool_input.file_path // .tool_input.pathInProject // empty' 2>/dev/null)"
|
|
74
|
+
;;
|
|
75
|
+
Bash | mcp__webstorm__execute_terminal_command | mcp__webstorm__execute_tool)
|
|
76
|
+
cmd="$(rt_hook_cmd)"
|
|
77
|
+
[ -z "$cmd" ] && exit 0
|
|
78
|
+
# Здесь берётся не общий признак записи, а цель, названная в команде прямо:
|
|
79
|
+
# перенаправление, `tee`, правка на месте, копирование поверх. Общий признак широк
|
|
80
|
+
# намеренно — в нём и имя интерпретатора, — и запуск разложенной проверки читался бы
|
|
81
|
+
# как правка её самой. Отбитие на чтении стоит дороже пропуска: гард, мешающий читать,
|
|
82
|
+
# выключают в первый же день.
|
|
83
|
+
#
|
|
84
|
+
# Снятие копии сюда не входит намеренно: снятый файл раскладка кладёт заново, и так
|
|
85
|
+
# чинят копию, которую переписал форматтер.
|
|
86
|
+
candidates="$(printf '%s' "$cmd" | rt_write_targets)"
|
|
87
|
+
;;
|
|
88
|
+
*) exit 0 ;;
|
|
89
|
+
esac
|
|
90
|
+
[ -z "$candidates" ] && exit 0
|
|
91
|
+
|
|
92
|
+
root="${CLAUDE_PROJECT_DIR:-.}"
|
|
93
|
+
|
|
94
|
+
# Каталог источников пакета в этом дереве. Есть он только у дерева, которое пакет и везёт; у
|
|
95
|
+
# потребителя источника нет вовсе, и адрес правки у него один — надстройка.
|
|
96
|
+
sources=""
|
|
97
|
+
if rt_needs rt_kit_sources_dir rule-source-guard; then
|
|
98
|
+
sources="$(rt_kit_sources_dir 2>/dev/null)"
|
|
99
|
+
fi
|
|
100
|
+
|
|
101
|
+
while IFS= read -r candidate; do
|
|
102
|
+
[ -z "$candidate" ] && continue
|
|
103
|
+
case "$candidate" in
|
|
104
|
+
/*) ;;
|
|
105
|
+
*) candidate="$root/$candidate" ;;
|
|
106
|
+
esac
|
|
107
|
+
[ -f "$candidate" ] || continue
|
|
108
|
+
|
|
109
|
+
# Шапка стоит в начале файла, но не первой строкой: у сценария её отодвигает `#!`, у
|
|
110
|
+
# правила — заголовок с именем и родом ресурса. Десяти строк хватает обоим, а читать файл
|
|
111
|
+
# целиком нельзя: гард стоит на каждой правке.
|
|
112
|
+
resource="$(head -12 "$candidate" 2>/dev/null | sed -nE 's/.*rt-kit v[^ ]+ · ([^ ]+) · [0-9a-f]+.*/\1/p' | head -1)"
|
|
113
|
+
[ -z "$resource" ] && continue
|
|
114
|
+
|
|
115
|
+
if [ -n "$sources" ] && [ -f "$root/$sources/$resource" ]; then
|
|
116
|
+
deny "BLOCKED by rule-source-guard: «${candidate#"$root"/}» разложен пакетом, и правка на его месте теряется на следующей раскладке — а до тех пор раскладка отказывает по этому файлу целиком, и цену платит тот, кто в этот день правит соседний ресурс. Ресурс — «${resource}». Правь источник: ${sources}/${resource} — потом собери пакет и разложи." \
|
|
117
|
+
"правка, верная только этому дереву, идёт в надстройку .claude/rt-kit/overrides/${resource} — она сливается по разделу «## » и раскладку переживает"
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
deny "BLOCKED by rule-source-guard: «${candidate#"$root"/}» разложен пакетом @rt-tools/agent-kit, и правка на его месте теряется на следующей раскладке. Ресурс — «${resource}». Правь надстройку: .claude/rt-kit/overrides/${resource} — она сливается по разделу «## » и раскладку переживает." \
|
|
121
|
+
"то, что верно любому дереву, правится в самом пакете и приезжает сюда новой редакцией"
|
|
122
|
+
done <<EOF
|
|
123
|
+
$candidates
|
|
124
|
+
EOF
|
|
125
|
+
|
|
126
|
+
exit 0
|
|
@@ -110,6 +110,7 @@ EOF
|
|
|
110
110
|
|
|
111
111
|
# Каталог папок задач: у дерева он свой, но имя обычно общее.
|
|
112
112
|
tasks_dir="${RT_TASKS_DIR:-docs/tasks}"
|
|
113
|
+
main_branch="${RT_MAIN_BRANCH:-main}"
|
|
113
114
|
|
|
114
115
|
# Общий хвост отказа: два законных хода и законная форма обхода, если она у отказа есть. Файл
|
|
115
116
|
# может быть не разложен — тогда хвоста нет, а причина отказа остаётся прежней.
|
|
@@ -146,6 +147,23 @@ root="$(git rev-parse --show-toplevel 2>/dev/null)"
|
|
|
146
147
|
dir="$root/$tasks_dir/$branch"
|
|
147
148
|
plan="$dir/plan.md"
|
|
148
149
|
|
|
150
|
+
# Папка, разобранная коммитом этой ветки, — признак того, что работа отдана. Замысел с диска к
|
|
151
|
+
# этой минуте снят намеренно: уборка стоит до открытия заявки, потому что кнопку слияния
|
|
152
|
+
# нажимает человек на хостинге и закрывающему коммиту места после одобрения не остаётся.
|
|
153
|
+
# Правка после уборки — это правка по замечаниям разбора, и требовать под неё замысла значило
|
|
154
|
+
# бы запирать ветку собственным порядком. Признак берётся из истории ветки, а не с диска:
|
|
155
|
+
# снесённая, но не закоммиченная папка отданной работы не означает.
|
|
156
|
+
folder_archived() {
|
|
157
|
+
[ -n "$(git ls-tree -d --name-only HEAD -- "$tasks_dir/$branch" 2>/dev/null | head -1)" ] && return 1
|
|
158
|
+
base="$(git merge-base "$main_branch" HEAD 2>/dev/null)"
|
|
159
|
+
[ -z "$base" ] && return 1
|
|
160
|
+
had="$(git ls-tree -d --name-only "$base" -- "$tasks_dir/$branch" 2>/dev/null | head -1)"
|
|
161
|
+
[ -z "$had" ] && had="$(git log "$base..HEAD" --diff-filter=A --name-only --pretty=format: -- "$tasks_dir/$branch" 2>/dev/null | head -1)"
|
|
162
|
+
[ -n "$had" ]
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
folder_archived && exit 0
|
|
166
|
+
|
|
149
167
|
if [ ! -f "$plan" ]; then
|
|
150
168
|
deny "BLOCKED by task-flow: нет замысла — '${tasks_dir}/${branch}/plan.md'. Собери папку задачи с образца (cp -r ${tasks_dir}/_template ${tasks_dir}/${branch}) и заполни шапку, след задачи и этапы, затем повтори. Правило — скил task-flow."
|
|
151
169
|
fi
|
|
@@ -74,6 +74,23 @@ case "$state" in
|
|
|
74
74
|
работа-отдана | влито) exit 0 ;;
|
|
75
75
|
esac
|
|
76
76
|
|
|
77
|
+
# То же самое, но объявить это на диске уже нечем: папка задачи разбирается до открытия заявки,
|
|
78
|
+
# и ход работы уезжает вместе с ней. Признак берётся из истории ветки — папка, снятая её
|
|
79
|
+
# коммитом. Без этой ветки хвост работы судился бы вторым признаком, то есть отбивался бы за
|
|
80
|
+
# ход, в котором исполнитель ждёт чужого прогона и ничего в дереве не двигает.
|
|
81
|
+
folder_archived() {
|
|
82
|
+
[ -n "$branch" ] || return 1
|
|
83
|
+
[ -n "$(git ls-tree -d --name-only HEAD -- "$tasks_dir/$branch" 2>/dev/null | head -1)" ] && return 1
|
|
84
|
+
main_branch="${RT_MAIN_BRANCH:-main}"
|
|
85
|
+
base="$(git merge-base "$main_branch" HEAD 2>/dev/null)"
|
|
86
|
+
[ -z "$base" ] && return 1
|
|
87
|
+
had="$(git ls-tree -d --name-only "$base" -- "$tasks_dir/$branch" 2>/dev/null | head -1)"
|
|
88
|
+
[ -z "$had" ] && had="$(git log "$base..HEAD" --diff-filter=A --name-only --pretty=format: -- "$tasks_dir/$branch" 2>/dev/null | head -1)"
|
|
89
|
+
[ -n "$had" ]
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
[ -z "$progress" ] && folder_archived && exit 0
|
|
93
|
+
|
|
77
94
|
# Следующий шаг из хода работы — его страж и называет в отказе: исполнитель, которому сказано
|
|
78
95
|
# только «работа не кончена», перечитывает ту же строку сам.
|
|
79
96
|
next_step=""
|
|
@@ -114,17 +114,17 @@ az repos pr update --id 205 --description "$(cat тело.md)"
|
|
|
114
114
|
|
|
115
115
|
## Оставшийся шаг
|
|
116
116
|
|
|
117
|
-
|
|
118
|
-
|
|
117
|
+
Папка задачи разобрана коммитом `<sha>` — за работой убрано. Осталось дождаться прогона и снять
|
|
118
|
+
черновик; до этого кнопка слияния заблокирована хостингом.
|
|
119
119
|
```
|
|
120
120
|
|
|
121
121
|
Раздел «Оставшийся шаг» стоит последним и переписывается тем же вызовом, что и остальное тело,
|
|
122
|
-
— в тот ход, которым
|
|
122
|
+
— в тот ход, которым снимается черновик:
|
|
123
123
|
|
|
124
124
|
```markdown
|
|
125
125
|
## Оставшийся шаг
|
|
126
126
|
|
|
127
|
-
Не осталось:
|
|
127
|
+
Не осталось: прогон зелёный, черновик снят. Можно вливать.
|
|
128
128
|
```
|
|
129
129
|
|
|
130
130
|
Стоит он там потому, что решение о слиянии принимается на этой странице, а не в переписке:
|
|
@@ -141,17 +141,17 @@ Closes #86
|
|
|
141
141
|
|
|
142
142
|
## Оставшийся шаг
|
|
143
143
|
|
|
144
|
-
|
|
145
|
-
|
|
144
|
+
Папка задачи разобрана коммитом `<sha>` — за работой убрано. Осталось дождаться прогона и снять
|
|
145
|
+
черновик; до этого кнопка слияния заблокирована хостингом.
|
|
146
146
|
```
|
|
147
147
|
|
|
148
148
|
Раздел «Оставшийся шаг» стоит последним и переписывается тем же вызовом, что и остальное тело,
|
|
149
|
-
— в тот ход, которым
|
|
149
|
+
— в тот ход, которым снимается черновик:
|
|
150
150
|
|
|
151
151
|
```markdown
|
|
152
152
|
## Оставшийся шаг
|
|
153
153
|
|
|
154
|
-
Не осталось:
|
|
154
|
+
Не осталось: прогон зелёный, черновик снят. Можно вливать.
|
|
155
155
|
```
|
|
156
156
|
|
|
157
157
|
Стоит он там потому, что решение о слиянии принимается на этой странице, а не в переписке:
|
|
@@ -125,17 +125,17 @@ Closes #86
|
|
|
125
125
|
|
|
126
126
|
## Оставшийся шаг
|
|
127
127
|
|
|
128
|
-
|
|
129
|
-
|
|
128
|
+
Папка задачи разобрана коммитом `<sha>` — за работой убрано. Осталось дождаться прогона и снять
|
|
129
|
+
черновик; до этого кнопка слияния заблокирована хостингом.
|
|
130
130
|
```
|
|
131
131
|
|
|
132
132
|
Раздел «Оставшийся шаг» стоит последним и переписывается тем же вызовом, что и остальное
|
|
133
|
-
описание, — в тот ход, которым
|
|
133
|
+
описание, — в тот ход, которым снимается черновик:
|
|
134
134
|
|
|
135
135
|
```markdown
|
|
136
136
|
## Оставшийся шаг
|
|
137
137
|
|
|
138
|
-
Не осталось:
|
|
138
|
+
Не осталось: прогон зелёный, черновик снят. Можно вливать.
|
|
139
139
|
```
|
|
140
140
|
|
|
141
141
|
Стоит он там потому, что решение о слиянии принимается на этой странице, а не в переписке:
|