@rt-tools/agent-kit 0.11.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-file-size.mjs +19 -4
- package/assets/checks/check-schema-drift.mjs +28 -5
- package/assets/checks/check-state-next.mjs +10 -2
- package/assets/checks/rt-kit-checks.config.mjs +28 -2
- package/assets/commands/feedback.md +8 -0
- package/assets/defaults/project.sh +59 -6
- package/assets/defaults/turn-map.md +8 -6
- package/assets/hooks/browser-guard-device-id.sh +3 -1
- package/assets/hooks/browser-guard-no-asking.sh +3 -1
- package/assets/hooks/browser-guard-no-other-drivers.sh +6 -4
- package/assets/hooks/browser-guard-require-select.sh +4 -2
- package/assets/hooks/claim-guard.sh +3 -1
- package/assets/hooks/conscience-guard.sh +3 -1
- package/assets/hooks/dev-server-guard.sh +6 -4
- package/assets/hooks/dispatch.sh +69 -0
- package/assets/hooks/docs-guard.sh +6 -4
- package/assets/hooks/exam-guard.sh +5 -3
- 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 +58 -74
- package/assets/hooks/git-guard-main.sh +6 -4
- package/assets/hooks/git-guard-push-tests.sh +8 -6
- package/assets/hooks/grill-gate.sh +4 -2
- package/assets/hooks/handoff-entry-guard.sh +4 -2
- package/assets/hooks/handoff-write.sh +27 -6
- package/assets/hooks/hook-input.sh +77 -0
- package/assets/hooks/lint-after-edit.sh +5 -3
- package/assets/hooks/override-write-guard.sh +107 -0
- package/assets/hooks/postmortem-guard.sh +3 -1
- package/assets/hooks/proposal-guard.sh +3 -1
- package/assets/hooks/prose-style-guard.sh +5 -3
- package/assets/hooks/qa-dataid-guard.sh +4 -2
- package/assets/hooks/rerun-guard.sh +6 -4
- package/assets/hooks/reuse-first-guard.sh +5 -3
- package/assets/hooks/rule-article.sh +99 -0
- package/assets/hooks/rule-source-guard.sh +126 -0
- package/assets/hooks/skill-gate-rearm.sh +3 -1
- package/assets/hooks/skill-gate.sh +23 -2
- package/assets/hooks/skill-loaded.sh +3 -1
- package/assets/hooks/sql-guard-request.sh +2 -1
- package/assets/hooks/sql-guard.sh +4 -2
- package/assets/hooks/task-flow-guard.sh +24 -4
- package/assets/hooks/turn-exit-guard.sh +59 -17
- package/assets/hooks/waiting-turn-guard.sh +3 -1
- package/assets/hooks/window-fill-guard.sh +6 -4
- package/assets/laws/work-conduct.md +5 -9
- package/assets/patterns/dependencies-upgrade.md +1 -1
- package/assets/patterns/doc-style-write.md +3 -3
- package/assets/patterns/git-workflow-commit.azure.md +2 -202
- package/assets/patterns/git-workflow-commit.github.md +2 -258
- package/assets/patterns/git-workflow-commit.gitlab.md +1 -217
- package/assets/patterns/git-workflow-docker.md +3 -3
- package/assets/patterns/git-workflow-merge.md +3 -2
- package/assets/patterns/git-workflow-migration.md +3 -3
- package/assets/patterns/git-workflow-pr.azure.md +224 -0
- package/assets/patterns/git-workflow-pr.github.md +280 -0
- package/assets/patterns/git-workflow-pr.gitlab.md +240 -0
- package/assets/patterns/git-workflow-restart.md +3 -3
- package/assets/patterns/git-workflow-secrets.md +3 -3
- package/assets/patterns/task-flow-archive.md +195 -0
- package/assets/patterns/task-flow-close.md +72 -240
- package/assets/patterns/task-flow-handoff.md +4 -4
- package/assets/patterns/task-flow-resume.md +5 -3
- package/assets/pitfalls/agent-kit.md +80 -0
- package/assets/pitfalls/doc-style.md +80 -0
- package/assets/pitfalls/git-workflow.azure.md +50 -0
- package/assets/pitfalls/git-workflow.github.md +78 -0
- package/assets/pitfalls/git-workflow.gitlab.md +49 -0
- package/assets/pitfalls/spec-driven.md +36 -0
- package/assets/pitfalls/styling-bem.md +45 -0
- package/assets/pitfalls/task-flow.md +62 -0
- package/assets/pitfalls/testing.md +70 -0
- package/assets/rules/deploy-flow.azure.md +114 -0
- package/assets/rules/deploy-flow.github.md +122 -0
- package/assets/rules/deploy-flow.gitlab.md +116 -0
- package/assets/rules/doc-style.md +25 -76
- package/assets/rules/git-workflow.azure.md +6 -92
- package/assets/rules/git-workflow.github.md +20 -128
- package/assets/rules/git-workflow.gitlab.md +6 -93
- package/assets/rules/spec-driven.md +39 -30
- package/assets/rules/styling-bem.md +20 -39
- package/assets/rules/task-flow.md +56 -222
- package/assets/rules/testing.md +3 -64
- package/assets/rules/turn-conduct.md +210 -0
- package/assets/rules/typescript-conventions.md +15 -0
- package/assets/skills/agent-kit.md +67 -89
- package/assets/templates/pitfalls.md +10 -0
- package/assets/templates/proposal.md +16 -1
- package/assets/templates/rule.md +5 -3
- package/bin/agent-kit.d.ts.map +1 -1
- package/bin/agent-kit.js +1 -42
- package/bin/agent-kit.js.map +1 -1
- package/lib/assets.d.ts.map +1 -1
- package/lib/assets.js +6 -1
- package/lib/assets.js.map +1 -1
- package/lib/cascade.d.ts.map +1 -1
- package/lib/cascade.js +19 -1
- package/lib/cascade.js.map +1 -1
- package/lib/commands.d.ts.map +1 -1
- package/lib/commands.js +1 -0
- package/lib/commands.js.map +1 -1
- package/lib/config.d.ts +16 -1
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +8 -0
- package/lib/config.js.map +1 -1
- package/lib/hooks-map.d.ts +13 -0
- package/lib/hooks-map.d.ts.map +1 -1
- package/lib/hooks-map.js +33 -1
- package/lib/hooks-map.js.map +1 -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/ship.d.ts +1 -2
- package/lib/ship.d.ts.map +1 -1
- package/lib/ship.js +0 -54
- package/lib/ship.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/assets/commands/agent-kit-digest.md +0 -89
- package/assets/commands/rules-review.md +0 -98
- package/assets/patterns/cargo-triage-mark.md +0 -119
- package/assets/rules/cargo-triage.md +0 -126
- package/lib/cargo-state.d.ts +0 -62
- package/lib/cargo-state.d.ts.map +0 -1
- package/lib/cargo-state.js +0 -118
- package/lib/cargo-state.js.map +0 -1
- package/rt-tools-agent-kit-0.11.0.tgz +0 -0
|
@@ -26,25 +26,27 @@
|
|
|
26
26
|
# ОТКАЗ В ПОЛЬЗУ РАБОТЫ: не репозиторий, битый ввод, нет профиля — пропуск.
|
|
27
27
|
|
|
28
28
|
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
|
|
29
|
+
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
|
|
29
30
|
|
|
30
|
-
|
|
31
|
+
rt_hook_read
|
|
32
|
+
input="$RT_HOOK_INPUT"
|
|
31
33
|
[ -z "$input" ] && exit 0
|
|
32
34
|
|
|
33
|
-
tool="$(
|
|
35
|
+
tool="$(rt_hook_tool)"
|
|
34
36
|
case "$tool" in
|
|
35
37
|
# Терминал среды и универсальный исполнитель кладут команду в то же поле.
|
|
36
38
|
Bash | mcp__webstorm__execute_terminal_command | mcp__webstorm__execute_tool) ;;
|
|
37
39
|
*) exit 0 ;;
|
|
38
40
|
esac
|
|
39
41
|
|
|
40
|
-
cmd="$(
|
|
42
|
+
cmd="$(rt_hook_cmd)"
|
|
41
43
|
|
|
42
44
|
# Вызов пуша узнаётся по двум признакам сразу — команда `git` в начале строки или за
|
|
43
45
|
# разделителем и слово `push` отдельным словом. Тем же приёмом, что у гарда поставки: одной
|
|
44
46
|
# подстрокой «git push» пуш не поймать — помощник учётных данных и заголовок запроса ставятся
|
|
45
47
|
# ключами `-c` между ними, и ровно этой формой здесь и пушат. Пока признаком была подстрока,
|
|
46
48
|
# весь набор гейта на таком пуше не гонялся вовсе, а молчание гарда читалось как «зелено».
|
|
47
|
-
printf '%s' "$cmd" | grep -qE
|
|
49
|
+
printf '%s' "$cmd" | grep -qE "${RT_CMD_BOUND}git([[:space:]]|\$)" || exit 0
|
|
48
50
|
|
|
49
51
|
# Отложенная правка пушем не бывает: `git stash push` кладёт правку в тайник этой же машины и
|
|
50
52
|
# наружу не отправляет ничего. Слово `push` в ней стоит отдельным, и без этой строки гард гонял
|
|
@@ -69,7 +71,7 @@ esac
|
|
|
69
71
|
#
|
|
70
72
|
# Судится переключение на существующую ветку. Заведение новой (`checkout -b`, `switch -c`)
|
|
71
73
|
# сюда не попадает: у свежей ветки дерево то же самое, что и было.
|
|
72
|
-
if printf '%s' "$cmd" | grep -qE
|
|
74
|
+
if printf '%s' "$cmd" | grep -qE "${RT_CMD_BOUND}git[[:space:]]+(checkout|switch)[[:space:]]+" &&
|
|
73
75
|
! printf '%s' "$cmd" | grep -qE 'git[[:space:]]+(checkout[[:space:]]+-b|switch[[:space:]]+-c)([[:space:]]|$)'; then
|
|
74
76
|
reason="BLOCKED: переключение ветки и пуш одной командой. Набор гейта гоняется в том дереве, какое лежит на момент разбора команды, — то есть по ПРЕЖНЕЙ ветке, а не по той, что уходит на хостинг. Зелёный набор при этом читается как проверка ушедшего, хотя проверял он другое. Раздели вызовы: сперва переключись, затем отдельной командой пушь."
|
|
75
77
|
# Общий хвост отказа: два законных хода и законная форма обхода, если она у отказа есть.
|
|
@@ -88,7 +90,7 @@ ${deny_tail_text}"
|
|
|
88
90
|
exit 0
|
|
89
91
|
fi
|
|
90
92
|
|
|
91
|
-
workdir="$(
|
|
93
|
+
workdir="$(rt_hook_cwd)"
|
|
92
94
|
[ -z "$workdir" ] && workdir="${CLAUDE_PROJECT_DIR:-.}"
|
|
93
95
|
cd "$workdir" 2>/dev/null || exit 0
|
|
94
96
|
git rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0
|
|
@@ -23,14 +23,16 @@
|
|
|
23
23
|
# РАЗРЕШАЕТСЯ (exit 0). Сломанный гард не имеет права заклинить разговор.
|
|
24
24
|
|
|
25
25
|
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
|
|
26
|
+
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
rt_hook_read
|
|
29
|
+
input="$RT_HOOK_INPUT"
|
|
28
30
|
[ -z "$input" ] && exit 0
|
|
29
31
|
|
|
30
32
|
command -v jq >/dev/null 2>&1 || exit 0
|
|
31
33
|
|
|
32
34
|
# Какое событие пришло. У вызова инструмента есть его имя, у завершения хода — нет.
|
|
33
|
-
tool="$(
|
|
35
|
+
tool="$(rt_hook_tool)"
|
|
34
36
|
|
|
35
37
|
# Повторный заход по тому же ходу не судится: иначе ход не кончится никогда — гард сказал своё
|
|
36
38
|
# один раз и отпускает. К вызову инструмента это не относится: там судится сам вызов.
|
|
@@ -15,12 +15,14 @@
|
|
|
15
15
|
# FAIL-OPEN: нет `jq`, нет записи хода, передачи в реплике нет → пропуск.
|
|
16
16
|
|
|
17
17
|
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
|
|
18
|
+
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
|
|
18
19
|
|
|
19
|
-
|
|
20
|
+
rt_hook_read
|
|
21
|
+
input="$RT_HOOK_INPUT"
|
|
20
22
|
[ -z "$input" ] && exit 0
|
|
21
23
|
command -v jq >/dev/null 2>&1 || exit 0
|
|
22
24
|
|
|
23
|
-
tool="$(
|
|
25
|
+
tool="$(rt_hook_tool)"
|
|
24
26
|
case "$tool" in
|
|
25
27
|
Edit | Write | MultiEdit) ;;
|
|
26
28
|
*) exit 0 ;;
|
|
@@ -20,8 +20,10 @@
|
|
|
20
20
|
# нулём и молчит. Сжатие он не отбивает никогда: остановленное сжатие оставит заход без места.
|
|
21
21
|
|
|
22
22
|
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
|
|
23
|
+
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
rt_hook_read
|
|
26
|
+
input="$RT_HOOK_INPUT"
|
|
25
27
|
[ -z "$input" ] && exit 0
|
|
26
28
|
|
|
27
29
|
command -v jq >/dev/null 2>&1 || exit 0
|
|
@@ -33,13 +35,21 @@ for profile in "$rt_hooks_dir/../rt-kit/defaults/project.sh" "$rt_hooks_dir/../d
|
|
|
33
35
|
[ -f "$profile" ] && . "$profile" 2>/dev/null
|
|
34
36
|
done
|
|
35
37
|
|
|
36
|
-
workdir="$(
|
|
38
|
+
workdir="$(rt_hook_cwd)"
|
|
37
39
|
[ -z "$workdir" ] && workdir="${CLAUDE_PROJECT_DIR:-.}"
|
|
38
40
|
cd "$workdir" 2>/dev/null || exit 0
|
|
39
41
|
git rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0
|
|
40
42
|
|
|
43
|
+
# Пустое имя ветки хук не останавливает. На отсоединённой голове имени нет, а всё, что едет в
|
|
44
|
+
# передачу, лежит в дереве и доступно целиком: имя нужно только файлу. Прежний выход нулём был
|
|
45
|
+
# полным молчанием — сжатие приходило без передачи, и заход после него начинал с пустого места.
|
|
41
46
|
branch="$(git branch --show-current 2>/dev/null)"
|
|
42
|
-
|
|
47
|
+
head_name=""
|
|
48
|
+
if [ -z "$branch" ]; then
|
|
49
|
+
head_name="$(git rev-parse --short HEAD 2>/dev/null)"
|
|
50
|
+
[ -z "$head_name" ] && head_name='detached'
|
|
51
|
+
head_name="detached-$head_name"
|
|
52
|
+
fi
|
|
43
53
|
|
|
44
54
|
root="$(git rev-parse --show-toplevel 2>/dev/null)"
|
|
45
55
|
[ -z "$root" ] && exit 0
|
|
@@ -56,7 +66,12 @@ mkdir -p "$root/$handoff_dir" 2>/dev/null || exit 0
|
|
|
56
66
|
trigger="$(printf '%s' "$input" | jq -r '.trigger // "auto"' 2>/dev/null)"
|
|
57
67
|
[ -z "$trigger" ] && trigger='auto'
|
|
58
68
|
|
|
59
|
-
|
|
69
|
+
# Файл передачи ищут по имени ветки; голове без имени он называется её коротким снимком.
|
|
70
|
+
handoff_name="$branch"
|
|
71
|
+
[ -z "$handoff_name" ] && handoff_name="$head_name"
|
|
72
|
+
|
|
73
|
+
progress=""
|
|
74
|
+
[ -n "$branch" ] && progress="$root/$tasks_dir/$branch/progress.md"
|
|
60
75
|
|
|
61
76
|
# Строка раздела «Где стоим» по её названию. Пусто — работа идёт вне папки задачи, и выдумывать
|
|
62
77
|
# за неё состояние нельзя: в передаче тогда стоит то, что известно дереву.
|
|
@@ -76,12 +91,16 @@ uncommitted="$(git status --short 2>/dev/null | head -20)"
|
|
|
76
91
|
ahead="$(git log --oneline origin/main..HEAD 2>/dev/null | head -20)"
|
|
77
92
|
[ -z "$ahead" ] && ahead='нет коммитов сверх главной'
|
|
78
93
|
|
|
79
|
-
target="$root/$handoff_dir/$
|
|
94
|
+
target="$root/$handoff_dir/$handoff_name.md"
|
|
80
95
|
|
|
81
96
|
{
|
|
82
97
|
printf '# Передача захода — сжатие контекста (%s)\n\n' "$trigger"
|
|
83
98
|
printf '**Рабочее дерево:** %s\n' "$root"
|
|
84
|
-
|
|
99
|
+
if [ -n "$branch" ]; then
|
|
100
|
+
printf '**Ветка:** %s\n\n' "$branch"
|
|
101
|
+
else
|
|
102
|
+
printf '**Ветка:** имени нет — отсоединённая голова %s\n\n' "$head_name"
|
|
103
|
+
fi
|
|
85
104
|
|
|
86
105
|
if [ -n "$state" ]; then
|
|
87
106
|
printf '## Где стоим\n\n'
|
|
@@ -90,6 +109,8 @@ target="$root/$handoff_dir/$branch.md"
|
|
|
90
109
|
[ -n "$next_step" ] && printf -- '- **Следующий шаг:** %s\n' "$next_step"
|
|
91
110
|
[ -n "$pull" ] && printf -- '- **PR:** %s\n' "$pull"
|
|
92
111
|
printf '\nХод работы целиком — `%s/%s/progress.md`; замысел рядом с ним.\n\n' "$tasks_dir" "$branch"
|
|
112
|
+
elif [ -z "$branch" ]; then
|
|
113
|
+
printf '## Где стоим\n\nИмени у головы нет, папки задачи при ней тоже: состояние работы взять неоткуда. Читающая сторона ищет передачу по имени ветки, а не найдя его — по последней записи каталога.\n\n'
|
|
93
114
|
else
|
|
94
115
|
printf '## Где стоим\n\nПапки задачи у этой ветки нет: состояние работы взять неоткуда.\n\n'
|
|
95
116
|
fi
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Общее чтение ввода хука. НЕ гард: объявления `rt-hook:` у него нет, к событиям агента он не
|
|
3
|
+
# подключается. Его источают сами гарды — тем же приёмом, каким они источают общий хвост отказа.
|
|
4
|
+
#
|
|
5
|
+
# Зачем он есть. Ввод приходит одним объектом, и каждому гарду из него нужны те же поля: имя
|
|
6
|
+
# инструмента, командная строка, путь правки, рабочий каталог. Пока каждый доставал их сам, один
|
|
7
|
+
# вызов агента стоил больше сотни разборов одного и того же текста — по шесть-семь на гард,
|
|
8
|
+
# восемнадцать гардов на событие. Разбор стоит шесть миллисекунд, запуск оболочки — пять.
|
|
9
|
+
#
|
|
10
|
+
# Как это работает. Диспетчер разбирает ввод однажды и кладёт поля в окружение. Гард спрашивает
|
|
11
|
+
# их отсюда: есть в окружении — берёт готовое, нет — разбирает сам. Второе — это прямой вызов:
|
|
12
|
+
# так гарды зовут наборы сценариев, и работать они обязаны и без диспетчера.
|
|
13
|
+
#
|
|
14
|
+
# ПОЧЕМУ ЧТЕНИЕ ПОТОКА — ОТДЕЛЬНАЯ КОМАНДА, А НЕ ЗНАЧЕНИЕ. Подстановка команд исполняется в
|
|
15
|
+
# подоболочке, и всё, что она запомнила, умирает вместе с ней: первый вызов вычитал бы поток, а
|
|
16
|
+
# второй получил бы пустоту — и гард пропустил бы вызов, который обязан был отбить. Поэтому поток
|
|
17
|
+
# читается командой `rt_hook_read`, кладущей ввод в переменную текущей оболочки, а не функцией,
|
|
18
|
+
# возвращающей его через подстановку.
|
|
19
|
+
#
|
|
20
|
+
# ОТКАЗ В ПОЛЬЗУ РАБОТЫ: нет разборщика, битый ввод, пустое поле — пустая строка. Сломанное
|
|
21
|
+
# чтение не имеет права заклинить работу: гард на пустом поле выходит нулём.
|
|
22
|
+
|
|
23
|
+
# Читает ввод в `RT_HOOK_INPUT`, если его там ещё нет. Зовётся командой, а не подстановкой.
|
|
24
|
+
rt_hook_read() {
|
|
25
|
+
[ -n "${RT_HOOK_INPUT:-}" ] && return 0
|
|
26
|
+
|
|
27
|
+
RT_HOOK_INPUT="$(cat 2>/dev/null)"
|
|
28
|
+
export RT_HOOK_INPUT
|
|
29
|
+
return 0
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# Поле ввода по имени переменной и пути в объекте: `rt_hook_field RT_HOOK_TOOL '.tool_name'`.
|
|
33
|
+
# Переменная окружения главнее разбора — её кладёт диспетчер, разобравший ввод один раз.
|
|
34
|
+
rt_hook_field() {
|
|
35
|
+
local name="$1" path="$2"
|
|
36
|
+
|
|
37
|
+
# Готовому верят только по признаку разбора, а не по тому, что переменная объявлена. Пустая
|
|
38
|
+
# переменная в окружении означает «поля нет», и без признака она неотличима от «поле есть и
|
|
39
|
+
# оно пустое»: гард переставал разбирать ввод и пропускал вызовы, которые обязан был отбить.
|
|
40
|
+
# Наборы поймали это шестнадцатью провалами — окружение прогона несло пустые значения.
|
|
41
|
+
if [ "${RT_HOOK_PARSED:-}" = '1' ] && [ -n "${!name+x}" ]; then
|
|
42
|
+
printf '%s' "${!name}"
|
|
43
|
+
return 0
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
rt_hook_read
|
|
47
|
+
printf '%s' "$RT_HOOK_INPUT" | jq -r "${path} // empty" 2>/dev/null
|
|
48
|
+
}
|
|
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
|
+
|
|
73
|
+
# Поля, которые спрашивают все гарды.
|
|
74
|
+
rt_hook_tool() { rt_hook_field RT_HOOK_TOOL '.tool_name'; }
|
|
75
|
+
rt_hook_cmd() { rt_hook_field RT_HOOK_CMD '.tool_input.command'; }
|
|
76
|
+
rt_hook_file() { rt_hook_field RT_HOOK_FILE '.tool_input.file_path'; }
|
|
77
|
+
rt_hook_cwd() { rt_hook_field RT_HOOK_CWD '.cwd'; }
|
|
@@ -30,14 +30,16 @@
|
|
|
30
30
|
# Сколько файлов линтуется за один перенос. Переезд либы трогает десятки файлов, и прогон по
|
|
31
31
|
# каждому превратил бы хук в минутную паузу; на нарушение границы хватает первых.
|
|
32
32
|
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
|
|
33
|
+
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
|
|
33
34
|
|
|
34
35
|
MAX_FILES=12
|
|
35
36
|
|
|
36
|
-
|
|
37
|
+
rt_hook_read
|
|
38
|
+
input="$RT_HOOK_INPUT"
|
|
37
39
|
[ -z "$input" ] && exit 0
|
|
38
40
|
command -v jq >/dev/null 2>&1 || exit 0
|
|
39
41
|
|
|
40
|
-
tool="$(
|
|
42
|
+
tool="$(rt_hook_tool)"
|
|
41
43
|
case "$tool" in
|
|
42
44
|
Edit | Write | MultiEdit | mcp__webstorm__create_new_file) mode="edit" ;;
|
|
43
45
|
Bash) mode="move" ;;
|
|
@@ -54,7 +56,7 @@ if [ "$mode" = "move" ]; then
|
|
|
54
56
|
esac
|
|
55
57
|
fi
|
|
56
58
|
|
|
57
|
-
workdir="$(
|
|
59
|
+
workdir="$(rt_hook_cwd)"
|
|
58
60
|
[ -z "$workdir" ] && workdir="${CLAUDE_PROJECT_DIR:-.}"
|
|
59
61
|
cd "$workdir" 2>/dev/null || exit 0
|
|
60
62
|
[ -f package.json ] || exit 0
|
|
@@ -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
|
|
@@ -22,8 +22,10 @@
|
|
|
22
22
|
# РАЗРЕШАЕТСЯ (exit 0). Сломанный гард не имеет права заклинить разговор.
|
|
23
23
|
|
|
24
24
|
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
|
|
25
|
+
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
rt_hook_read
|
|
28
|
+
input="$RT_HOOK_INPUT"
|
|
27
29
|
[ -z "$input" ] && exit 0
|
|
28
30
|
|
|
29
31
|
command -v jq >/dev/null 2>&1 || exit 0
|
|
@@ -21,8 +21,10 @@
|
|
|
21
21
|
# заходе ход РАЗРЕШАЕТСЯ (exit 0). Сломанный гард не имеет права заклинить разговор.
|
|
22
22
|
|
|
23
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
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
rt_hook_read
|
|
27
|
+
input="$RT_HOOK_INPUT"
|
|
26
28
|
[ -z "$input" ] && exit 0
|
|
27
29
|
|
|
28
30
|
command -v jq >/dev/null 2>&1 || exit 0
|
|
@@ -13,19 +13,21 @@
|
|
|
13
13
|
# FAIL-OPEN: нет узла, нет проверки, чужой инструмент, не `.md` → пропуск.
|
|
14
14
|
|
|
15
15
|
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
|
|
16
|
+
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
rt_hook_read
|
|
19
|
+
input="$RT_HOOK_INPUT"
|
|
18
20
|
[ -z "$input" ] && exit 0
|
|
19
21
|
command -v jq >/dev/null 2>&1 || exit 0
|
|
20
22
|
command -v node >/dev/null 2>&1 || exit 0
|
|
21
23
|
|
|
22
|
-
tool="$(
|
|
24
|
+
tool="$(rt_hook_tool)"
|
|
23
25
|
case "$tool" in
|
|
24
26
|
Edit | Write | MultiEdit) ;;
|
|
25
27
|
*) exit 0 ;;
|
|
26
28
|
esac
|
|
27
29
|
|
|
28
|
-
path="$(
|
|
30
|
+
path="$(rt_hook_file)"
|
|
29
31
|
case "$path" in
|
|
30
32
|
*.md) ;;
|
|
31
33
|
*) exit 0 ;;
|
|
@@ -26,13 +26,15 @@
|
|
|
26
26
|
# ОТКАЗ В ПОЛЬЗУ РАБОТЫ: нет разборщика, битый ввод, чужой инструмент — пропуск.
|
|
27
27
|
|
|
28
28
|
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
|
|
29
|
+
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
|
|
29
30
|
|
|
30
|
-
|
|
31
|
+
rt_hook_read
|
|
32
|
+
input="$RT_HOOK_INPUT"
|
|
31
33
|
[ -z "$input" ] && exit 0
|
|
32
34
|
command -v jq >/dev/null 2>&1 || exit 0
|
|
33
35
|
command -v perl >/dev/null 2>&1 || exit 0
|
|
34
36
|
|
|
35
|
-
tool="$(
|
|
37
|
+
tool="$(rt_hook_tool)"
|
|
36
38
|
case "$tool" in
|
|
37
39
|
# Инструмент среды заводит файл теми же двумя данными, только называет их иначе — без этой
|
|
38
40
|
# ветки разметка заводилась мимо всех проверок.
|
|
@@ -21,19 +21,21 @@
|
|
|
21
21
|
# упавшего прогона зовут и без него, а угадывать, о каком задании речь, значит отбивать наугад.
|
|
22
22
|
|
|
23
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
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
rt_hook_read
|
|
27
|
+
input="$RT_HOOK_INPUT"
|
|
26
28
|
[ -z "$input" ] && exit 0
|
|
27
29
|
command -v jq >/dev/null 2>&1 || exit 0
|
|
28
30
|
|
|
29
|
-
tool="$(
|
|
31
|
+
tool="$(rt_hook_tool)"
|
|
30
32
|
case "$tool" in
|
|
31
33
|
# Терминал среды и универсальный исполнитель кладут команду в то же поле.
|
|
32
34
|
Bash | mcp__webstorm__execute_terminal_command | mcp__webstorm__execute_tool) ;;
|
|
33
35
|
*) exit 0 ;;
|
|
34
36
|
esac
|
|
35
37
|
|
|
36
|
-
cmd="$(
|
|
38
|
+
cmd="$(rt_hook_cmd)"
|
|
37
39
|
[ -z "$cmd" ] && exit 0
|
|
38
40
|
|
|
39
41
|
# Профиль дерева: сперва умолчание пакета, поверх него — надстройка проекта, если она есть.
|
|
@@ -47,7 +49,7 @@ done
|
|
|
47
49
|
|
|
48
50
|
# Вызов перезапуска: слово перезапуска отдельным словом рядом со словом прогона.
|
|
49
51
|
host_cli="${RT_HOST_CLI:-gh}"
|
|
50
|
-
printf '%s' "$cmd" | grep -qE "
|
|
52
|
+
printf '%s' "$cmd" | grep -qE "${RT_CMD_BOUND}${host_cli}([[:space:]]|\$)" || exit 0
|
|
51
53
|
printf '%s' "$cmd" | grep -qE '(^|[[:space:]])(run|workflow)([[:space:]]|$)' || exit 0
|
|
52
54
|
printf '%s' "$cmd" | grep -qE '(^|[[:space:]])rerun([[:space:]]|$)' || exit 0
|
|
53
55
|
|
|
@@ -33,13 +33,15 @@
|
|
|
33
33
|
# ОТКАЗ В ПОЛЬЗУ РАБОТЫ: нет разборщика, битый ввод, чужой инструмент — пропуск.
|
|
34
34
|
|
|
35
35
|
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
|
|
36
|
+
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
rt_hook_read
|
|
39
|
+
input="$RT_HOOK_INPUT"
|
|
38
40
|
[ -z "$input" ] && exit 0
|
|
39
41
|
command -v jq >/dev/null 2>&1 || exit 0
|
|
40
42
|
command -v perl >/dev/null 2>&1 || exit 0
|
|
41
43
|
|
|
42
|
-
tool="$(
|
|
44
|
+
tool="$(rt_hook_tool)"
|
|
43
45
|
shell_cmd=""
|
|
44
46
|
case "$tool" in
|
|
45
47
|
# Инструмент среды заводит файл теми же двумя данными, только называет их иначе — без этой
|
|
@@ -54,7 +56,7 @@ case "$tool" in
|
|
|
54
56
|
# имён гард стоял бы объявленным на них и молча пропускал — состояние хуже необъявленного,
|
|
55
57
|
# потому что снаружи выглядит закрытым.
|
|
56
58
|
Bash | mcp__webstorm__execute_terminal_command | mcp__webstorm__execute_tool)
|
|
57
|
-
shell_cmd="$(
|
|
59
|
+
shell_cmd="$(rt_hook_cmd)"
|
|
58
60
|
[ -z "$shell_cmd" ] && exit 0
|
|
59
61
|
# Универсальный исполнитель прячет настоящую команду во вложенной строке: без её разбора
|
|
60
62
|
# путь стоит за кавычкой, и до него не дотягивается ни один образец.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Разбор признака применимости у статьи правила. Помощник: своего события не объявляет, его
|
|
3
|
+
# подключает тот, кому нужен текст статьи, — гейт правил в своём отказе.
|
|
4
|
+
#
|
|
5
|
+
# Правило весит от двадцати до шестидесяти килобайт, а под конкретную правку подпадает одна его
|
|
6
|
+
# статья. Отказ, зовущий правило целиком, платит за решение полной ценой правила — и учит не
|
|
7
|
+
# тому: дешевле не читать лишнего, то есть работать хуже разведанным.
|
|
8
|
+
#
|
|
9
|
+
# Признак стоит при самой статье: карта гейта знает путь и правило, но не знает, какая из двух
|
|
10
|
+
# десятков статей про этот путь. Статья знает о себе всё.
|
|
11
|
+
#
|
|
12
|
+
# - **Заголовок статьи.** Текст статьи, как обычно.
|
|
13
|
+
# <!-- rt-when: *.scss *.css -->
|
|
14
|
+
#
|
|
15
|
+
# Комментарий не виден в собранной разметке и не мешает читать статью; образцы разделены
|
|
16
|
+
# пробелом и сверяются с путём правки самой оболочкой, а не поиском по словам: поиск называет не
|
|
17
|
+
# ту статью и молчит об этом.
|
|
18
|
+
#
|
|
19
|
+
# ОТКАЗ В ПОЛЬЗУ РАБОТЫ: статья без признака законна, правило без единого признака — тоже. Не
|
|
20
|
+
# нашлось ни одной подошедшей статьи — печатается пусто, и зовущий остаётся при прежнем отказе.
|
|
21
|
+
|
|
22
|
+
# Статьи правила, чей признак совпал с путём правки. Доводы: файл правила, путь правки.
|
|
23
|
+
# Печатает текст подошедших статей целиком, по статье, разделяя их пустой строкой.
|
|
24
|
+
rt_rule_articles() {
|
|
25
|
+
rule_file="$1"
|
|
26
|
+
edited="$2"
|
|
27
|
+
[ -f "$rule_file" ] || return 0
|
|
28
|
+
[ -n "$edited" ] || return 0
|
|
29
|
+
|
|
30
|
+
# Статьи выбираются в два прохода: сперва номера строк, где стоят признаки, и сами образцы,
|
|
31
|
+
# затем — текст той статьи, чей образец совпал. Сопоставление делает оболочка: `case` знает
|
|
32
|
+
# правила путей, а разборщик текста — нет.
|
|
33
|
+
while IFS=' ' read -r line patterns; do
|
|
34
|
+
[ -n "$line" ] || continue
|
|
35
|
+
hit=''
|
|
36
|
+
# Образцы разбираются под выключенным раскрытием имён: `*.scss` в цикле развернулось бы
|
|
37
|
+
# в имена файлов рабочего каталога, и статья выбиралась бы по тому, где стоял вызов.
|
|
38
|
+
# Разделитель задаётся здесь же — внешний мог быть переопределён тем, кто нас позвал.
|
|
39
|
+
set -f
|
|
40
|
+
old_ifs="$IFS"
|
|
41
|
+
IFS=' '
|
|
42
|
+
# shellcheck disable=SC2086
|
|
43
|
+
set -- $patterns
|
|
44
|
+
IFS="$old_ifs"
|
|
45
|
+
set +f
|
|
46
|
+
for pattern in "$@"; do
|
|
47
|
+
[ -n "$pattern" ] || continue
|
|
48
|
+
# shellcheck disable=SC2254
|
|
49
|
+
case "$edited" in
|
|
50
|
+
$pattern) hit=1; break ;;
|
|
51
|
+
esac
|
|
52
|
+
# Образец без каталога сверяется и с именем файла: статья говорит «про такие файлы»,
|
|
53
|
+
# а правка приходит полным путём.
|
|
54
|
+
# shellcheck disable=SC2254
|
|
55
|
+
case "${edited##*/}" in
|
|
56
|
+
$pattern) hit=1; break ;;
|
|
57
|
+
esac
|
|
58
|
+
done
|
|
59
|
+
[ -n "$hit" ] || continue
|
|
60
|
+
rt_rule_article_at "$rule_file" "$line"
|
|
61
|
+
done <<EOF
|
|
62
|
+
$(rt_rule_article_marks "$rule_file")
|
|
63
|
+
EOF
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
# Признаки правила: строка «номер строки — образцы». Отдельной функцией, потому что её же зовёт
|
|
67
|
+
# сверка полноты текстов: правило, у которого признак записан не той формой, тихо остаётся без
|
|
68
|
+
# статьи в отказе, и заметить это нечем.
|
|
69
|
+
rt_rule_article_marks() {
|
|
70
|
+
[ -f "$1" ] || return 0
|
|
71
|
+
grep -n 'rt-when:' "$1" 2>/dev/null \
|
|
72
|
+
| sed -e 's/^\([0-9]*\):.*rt-when:[[:space:]]*/\1 /' -e 's/[[:space:]]*-->.*$//'
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
# Текст статьи, внутри которой стоит строка с признаком. Статья начинается ближайшим сверху
|
|
76
|
+
# пунктом списка верхнего уровня и кончается перед следующим таким пунктом или перед строкой без
|
|
77
|
+
# отступа: продолжение статьи всегда идёт с отступом.
|
|
78
|
+
rt_rule_article_at() {
|
|
79
|
+
awk -v mark="$2" '
|
|
80
|
+
NR <= mark && /^- / { start = NR }
|
|
81
|
+
{ line[NR] = $0 }
|
|
82
|
+
END {
|
|
83
|
+
if (!start) { exit }
|
|
84
|
+
for (i = start + 1; i <= NR; i++) {
|
|
85
|
+
if (line[i] ~ /^- / || (line[i] !~ /^[ \t]/ && line[i] != "")) { break }
|
|
86
|
+
stop = i
|
|
87
|
+
}
|
|
88
|
+
if (!stop) { stop = start }
|
|
89
|
+
last = start
|
|
90
|
+
for (i = start; i <= stop; i++) {
|
|
91
|
+
if (line[i] ~ /rt-when:/) { continue }
|
|
92
|
+
if (line[i] == "" && i == stop) { continue }
|
|
93
|
+
print line[i]
|
|
94
|
+
last = i
|
|
95
|
+
}
|
|
96
|
+
print ""
|
|
97
|
+
}
|
|
98
|
+
' "$1" 2>/dev/null
|
|
99
|
+
}
|