@rt-tools/agent-kit 0.9.1 → 0.10.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/README.md +5 -0
- package/assets/agents/conscience.md +58 -0
- package/assets/agents/prose-editor.md +44 -0
- package/assets/agents/strict-teacher.md +59 -0
- package/assets/checks/board.github.mjs +56 -1
- package/assets/checks/check-board.github.mjs +24 -0
- package/assets/checks/check-doc-paths.mjs +4 -15
- package/assets/checks/check-dupes.mjs +5 -6
- package/assets/checks/check-file-size.mjs +6 -20
- package/assets/checks/check-prose-style.mjs +137 -0
- package/assets/checks/check-reuse.mjs +5 -5
- package/assets/checks/check-styles.mjs +5 -5
- package/assets/checks/lib-common.mjs +3 -3
- package/assets/checks/rt-kit-checks.config.mjs +85 -1
- package/assets/defaults/project.sh +50 -0
- package/assets/docs/GLOSSARY.md +17 -15
- package/assets/hooks/browser-guard-device-id.sh +9 -1
- package/assets/hooks/browser-guard-no-asking.sh +9 -1
- package/assets/hooks/browser-guard-no-listing.sh +9 -1
- package/assets/hooks/browser-guard-no-other-drivers.sh +7 -1
- package/assets/hooks/browser-guard-require-select.sh +11 -2
- package/assets/hooks/claim-guard.sh +113 -0
- package/assets/hooks/conscience-guard.sh +98 -0
- package/assets/hooks/deny-tail.sh +32 -0
- package/assets/hooks/dev-server-guard.sh +8 -2
- package/assets/hooks/docs-guard.sh +14 -2
- package/assets/hooks/exam-guard.sh +121 -0
- package/assets/hooks/git-guard-delivery-signature.sh +75 -0
- package/assets/hooks/git-guard-delivery.sh +154 -68
- package/assets/hooks/git-guard-main.sh +12 -0
- package/assets/hooks/git-guard-push-tests.sh +49 -2
- package/assets/hooks/grill-gate.sh +12 -0
- package/assets/hooks/handoff-entry-guard.sh +71 -0
- package/assets/hooks/postmortem-guard.sh +12 -0
- package/assets/hooks/proposal-guard.sh +12 -0
- package/assets/hooks/prose-style-guard.sh +73 -0
- package/assets/hooks/qa-dataid-guard.sh +12 -1
- package/assets/hooks/rerun-guard.sh +86 -0
- package/assets/hooks/reuse-first-guard.sh +12 -1
- package/assets/hooks/roles.sh +32 -0
- package/assets/hooks/skill-gate.sh +12 -0
- package/assets/hooks/sql-guard-write.sh +2 -1
- package/assets/hooks/sql-guard.sh +12 -1
- package/assets/hooks/task-flow-guard.sh +71 -7
- package/assets/hooks/turn-exit-guard.sh +189 -0
- package/assets/hooks/waiting-turn-guard.sh +12 -0
- package/assets/hooks/window-fill-guard.sh +12 -1
- package/assets/patterns/task-flow-close.md +8 -8
- package/assets/patterns/task-flow-handoff.md +1 -1
- package/assets/patterns/task-flow-resume.md +30 -4
- package/assets/patterns/task-flow-start.md +15 -6
- package/assets/rules/git-workflow.azure.md +36 -9
- package/assets/rules/git-workflow.github.md +74 -9
- package/assets/rules/git-workflow.gitlab.md +40 -12
- package/assets/rules/task-flow.md +123 -71
- package/assets/rules/testing.md +15 -1
- package/assets/samples/tasks/_template/plan.md +4 -1
- package/assets/samples/tasks/_template/progress.md +1 -0
- package/assets/skills/agent-kit.md +33 -0
- package/assets/templates/project.sh +16 -0
- package/lib/config.d.ts +8 -0
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +1 -0
- package/lib/config.js.map +1 -1
- package/package.json +1 -1
- package/rt-tools-agent-kit-0.10.0.tgz +0 -0
- package/rt-tools-agent-kit-0.9.1.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
|
|
3
|
+
# Требует: hooks/profile-check.sh, hooks/deny-tail.sh
|
|
4
4
|
# Гард поставки. PreToolUse на заведении ветки, пуше и открытии заявки на слияние.
|
|
5
5
|
#
|
|
6
6
|
# Закон о поставке требует трёх вещей, которых обычно не проверяет ничто: правка начинается с
|
|
@@ -90,6 +90,11 @@ rt_needs rt_task_branch_ok git-guard-delivery || exit 0
|
|
|
90
90
|
title_re="${RT_TASK_TITLE_RE:-^\[[A-Za-z]+-[0-9]+\][[:space:]]+[^[:space:]]}"
|
|
91
91
|
task_new="${RT_TASK_NEW_CMD:-npm run task:new}"
|
|
92
92
|
board_check="${RT_BOARD_CHECK_CMD:-npm run check:board}"
|
|
93
|
+
task_move="${RT_TASK_MOVE_CMD:-npm run task:move}"
|
|
94
|
+
# Первая колонка очереди работ — та, из которой задача уходит, когда её берут. Умолчания у неё
|
|
95
|
+
# нет: имена колонок дерево называет своими словами, а выдуманное имя не совпало бы ни с чем и
|
|
96
|
+
# молча выключило бы проверку.
|
|
97
|
+
backlog_column="${RT_BOARD_BACKLOG:-}"
|
|
93
98
|
task_bot="${RT_TASK_BOT:-}"
|
|
94
99
|
commit_email="${RT_COMMIT_EMAIL:-}"
|
|
95
100
|
tasks_dir="${RT_TASKS_DIR:-}"
|
|
@@ -108,18 +113,43 @@ main_branch="${RT_MAIN_BRANCH:-main}"
|
|
|
108
113
|
folder_skip_re='(^|#)[[:space:]]*Task-folder-skip:[[:space:]]*[^[:space:]<"'"'"'][^[:space:]"'"'"']{2,}'
|
|
109
114
|
|
|
110
115
|
deny() {
|
|
111
|
-
# Отказ гарда — наблюдение: гард, отбивающий чаще прочих, говорит, какое место поставки
|
|
112
|
-
#
|
|
113
|
-
# и имена веток этого дерева.
|
|
116
|
+
# Отказ гарда — наблюдение: гард, отбивающий чаще прочих, говорит, какое место поставки раз
|
|
117
|
+
# за разом делают не так. Текст отказа туда не идёт: в нём номера задач и имена веток дерева.
|
|
114
118
|
# shellcheck disable=SC1090
|
|
115
119
|
[ -f "$rt_hooks_dir/observe.sh" ] && . "$rt_hooks_dir/observe.sh" 2>/dev/null
|
|
116
120
|
command -v rt_note >/dev/null 2>&1 && rt_note guard-deny res=git-guard-delivery "sid=$sid"
|
|
117
121
|
|
|
118
|
-
|
|
122
|
+
# Хвост отказа: два законных хода и форма обхода вторым параметром.
|
|
123
|
+
# shellcheck disable=SC1090
|
|
124
|
+
[ -f "$rt_hooks_dir/deny-tail.sh" ] && . "$rt_hooks_dir/deny-tail.sh" 2>/dev/null
|
|
125
|
+
reason="$1"
|
|
126
|
+
command -v rt_deny_tail >/dev/null 2>&1 && reason="$1 $(rt_deny_tail "$2")"
|
|
127
|
+
|
|
128
|
+
jq -n --arg r "$reason" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$r}}' 2>/dev/null \
|
|
119
129
|
|| printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Гард поставки."}}\n'
|
|
120
130
|
exit 0
|
|
121
131
|
}
|
|
122
132
|
|
|
133
|
+
# Несошедшиеся условия копятся и называются разом. Отказ по первому промаху заставляет чинить
|
|
134
|
+
# их по одному: исполнитель правит основание, повторяет вызов, упирается в заголовок, правит
|
|
135
|
+
# заголовок, упирается в задачу — и цена отказа растёт на каждом круге, хотя всё несошедшееся
|
|
136
|
+
# было известно уже на первом. Готовность к поставке — одно состояние, и называется оно целиком.
|
|
137
|
+
faults=''
|
|
138
|
+
|
|
139
|
+
fault() {
|
|
140
|
+
faults="${faults}${faults:+
|
|
141
|
+
}— $1"
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
# Отказ по накопленному. Пусто — вызывающая сторона идёт дальше.
|
|
145
|
+
deny_faults() {
|
|
146
|
+
[ -z "$faults" ] && return 0
|
|
147
|
+
deny "BLOCKED: работа к поставке не готова. Несошедшееся названо целиком — чтобы чинить его
|
|
148
|
+
одним заходом, а не по одному промаху на круг:
|
|
149
|
+
|
|
150
|
+
${faults}"
|
|
151
|
+
}
|
|
152
|
+
|
|
123
153
|
# Подсказка вместо отказа: на открытии PR папка ещё нужна. Решения подсказка не несёт,
|
|
124
154
|
# команда идёт дальше своим ходом.
|
|
125
155
|
hint() {
|
|
@@ -138,20 +168,33 @@ folder_in_branch() {
|
|
|
138
168
|
check_task() {
|
|
139
169
|
number="$1"
|
|
140
170
|
where="$2"
|
|
171
|
+
# Колонку спрашивают там, где она уже должна быть переставлена. На заведении ветки её ещё
|
|
172
|
+
# не двигали — паттерн работы переставляет колонку следующей командой, — и требование здесь
|
|
173
|
+
# отбивало бы первую же команду работы вместе с той, которая его и снимает.
|
|
174
|
+
judge_column="${3:-нет}"
|
|
141
175
|
rt_needs rt_task_state git-guard-delivery || return 0
|
|
142
176
|
state="$(cd "$root" && rt_task_state "$number" 2>/dev/null)" || return 0
|
|
143
177
|
[ -z "$state" ] && return 0
|
|
144
178
|
|
|
145
179
|
printf '%s' "$state" | jq -e '.exists' >/dev/null 2>&1 \
|
|
146
|
-
||
|
|
180
|
+
|| fault "${where} ссылается на задачу #${number}, которой нет. Проверь номер или заведи задачу — ${task_new}."
|
|
147
181
|
printf '%s' "$state" | jq -e '.open' >/dev/null 2>&1 \
|
|
148
|
-
||
|
|
182
|
+
|| fault "задача #${number} закрыта, а у задачи одна ветка. Работа за закрытой задачей заводится новой задачей — ${task_new}."
|
|
149
183
|
printf '%s' "$state" | jq -e '.onBoard' >/dev/null 2>&1 \
|
|
150
|
-
||
|
|
184
|
+
|| fault "задачи #${number} нет в очереди работ — правка за ней не видна. Очередь к репозиторию не привязана и задачу сама не забирает; добавь её и сверь — ${board_check}."
|
|
151
185
|
printf '%s' "$state" | jq -e '.assigned' >/dev/null 2>&1 \
|
|
152
|
-
||
|
|
186
|
+
|| fault "у задачи #${number} нет исполнителя — по очереди работ не видно, кто её взял. Поставь исполнителя${task_bot:+: }${task_bot}."
|
|
153
187
|
printf '%s' "$state" | jq -e '.numbered' >/dev/null 2>&1 \
|
|
154
|
-
||
|
|
188
|
+
|| fault "заголовок задачи #${number} не начинается с её номера — одну работу придётся узнавать по тексту названия. Поправь заголовок и сверь очередь — ${board_check}."
|
|
189
|
+
|
|
190
|
+
# Колонка задачи. Ответ очереди работ отдаёт её давно, и не читал её никто: колонку судила
|
|
191
|
+
# только сверка очереди, то есть уже после того, как PR открыт. Задача, оставшаяся в первой
|
|
192
|
+
# колонке, читается по очереди как невзятая — работа при этом сделана и выложена.
|
|
193
|
+
if [ -n "$backlog_column" ] && [ "$judge_column" = "да" ]; then
|
|
194
|
+
column="$(printf '%s' "$state" | jq -r '.status // empty' 2>/dev/null)"
|
|
195
|
+
[ "$column" = "$backlog_column" ] \
|
|
196
|
+
&& fault "задача #${number} стоит в колонке «${column}» — по очереди работ она не взята, хотя работа по ней идёт. Переставь её: ${task_move}."
|
|
197
|
+
fi
|
|
155
198
|
|
|
156
199
|
return 0
|
|
157
200
|
}
|
|
@@ -171,70 +214,109 @@ if [ -n "$branch_arg" ]; then
|
|
|
171
214
|
rt_task_branch_ok "$branch_arg" \
|
|
172
215
|
|| deny "BLOCKED: имя ветки «${branch_arg}» не той формы, что принята здесь. Номер у ветки тот же, что у задачи и у заголовка заявки на слияние."
|
|
173
216
|
check_task "$(printf '%s' "$branch_arg" | sed -nE 's/^[A-Za-z]+-([0-9]+).*/\1/p')" "ветка «${branch_arg}»"
|
|
217
|
+
|
|
218
|
+
# Условия поставки, известные уже здесь, спрашиваются здесь. Прежде их спрашивали на
|
|
219
|
+
# пуше и на открытии PR — то есть после того, как работа сделана: основание чинится
|
|
220
|
+
# мержем с разбором конфликта, а подпись коммитов — переписыванием всей ветки. В
|
|
221
|
+
# начале работы обе стоят одну команду.
|
|
222
|
+
#
|
|
223
|
+
# Основание: вершина главной ветки обязана лежать в том, от чего растёт новая ветка.
|
|
224
|
+
# Ветка, заведённая от вчерашнего основания, узнаёт об этом на открытии PR — и узнаёт
|
|
225
|
+
# так, что владелец видит её конфликтующей.
|
|
226
|
+
#
|
|
227
|
+
# Судится названное основание, а не вершина рабочей копии: команда, которой основание
|
|
228
|
+
# как раз и берут свежим — `git checkout -b <ветка> origin/<главная>`, — иначе
|
|
229
|
+
# отбивалась бы наравне с той, у которой основание вчерашнее.
|
|
230
|
+
base_arg="$(printf '%s' "$cmd" | sed -nE 's/.*git[[:space:]]+(checkout[[:space:]]+-b|switch[[:space:]]+-c)[[:space:]]+[^[:space:];&|]+[[:space:]]+([^[:space:];&|-][^[:space:];&|]*).*/\2/p' | head -1)"
|
|
231
|
+
base_ref="${base_arg:-HEAD}"
|
|
232
|
+
if git rev-parse --verify --quiet "refs/remotes/origin/${main_branch}" >/dev/null 2>&1 \
|
|
233
|
+
&& git rev-parse --verify --quiet "$base_ref" >/dev/null 2>&1 \
|
|
234
|
+
&& ! git merge-base --is-ancestor "origin/${main_branch}" "$base_ref" 2>/dev/null; then
|
|
235
|
+
behind="$(git rev-list --count "${base_ref}..origin/${main_branch}" 2>/dev/null)"
|
|
236
|
+
fault "ветка вырастет из основания, в котором нет вершины «${main_branch}» — она ушла вперёд на ${behind:-несколько} коммитов. Возьми свежее основание: git fetch origin && git checkout -b ${branch_arg} origin/${main_branch}."
|
|
237
|
+
fi
|
|
238
|
+
|
|
239
|
+
# Второй ярус: локальная ссылка на главную ветку сама могла протухнуть, и тогда молчание
|
|
240
|
+
# первого яруса значит «основание не старше моей ссылки», а не «основание свежее».
|
|
241
|
+
# Ответа нет — ярус молчит, как и везде, где гард ходит в сеть.
|
|
242
|
+
remote_head="$(GIT_HTTP_LOW_SPEED_LIMIT=1000 GIT_HTTP_LOW_SPEED_TIME=5 GIT_TERMINAL_PROMPT=0 \
|
|
243
|
+
git ls-remote origin "refs/heads/${main_branch}" 2>/dev/null | cut -f1)"
|
|
244
|
+
local_head="$(git rev-parse --verify --quiet "refs/remotes/origin/${main_branch}" 2>/dev/null)"
|
|
245
|
+
if [ -n "$remote_head" ] && [ -n "$local_head" ] && [ "$remote_head" != "$local_head" ]; then
|
|
246
|
+
fault "твоя ссылка origin/${main_branch} отстала от удалённой — ${local_head:0:8} против ${remote_head:0:8}. Ветка вырастет из вчерашнего дерева, и увидит это владелец на открытии заявки. Подтяни и повтори: git fetch origin."
|
|
247
|
+
fi
|
|
248
|
+
|
|
249
|
+
# Подпись: почта машинной записи объявлена деревом, а рабочая копия её не знает —
|
|
250
|
+
# значит первый же коммит уедет за чужой подписью, и чинить это придётся после пуша
|
|
251
|
+
# силовой отправкой.
|
|
252
|
+
if [ -n "$commit_email" ]; then
|
|
253
|
+
tree_email="$(git config user.email 2>/dev/null)"
|
|
254
|
+
[ -n "$tree_email" ] && [ "$tree_email" != "$commit_email" ] \
|
|
255
|
+
&& fault "рабочая копия подписывает коммиты как «${tree_email}», а дерево объявило почтой машинной записи «${commit_email}». Первый же коммит уедет за чужой подписью: git config user.email \"${commit_email}\"."
|
|
256
|
+
fi
|
|
257
|
+
|
|
258
|
+
deny_faults
|
|
174
259
|
fi
|
|
175
260
|
# Ветка без номера законна и живёт локально: заявка с неё не откроется.
|
|
176
261
|
exit 0
|
|
177
262
|
fi
|
|
178
263
|
|
|
179
264
|
# --- подпись машинного коммита ------------------------------------------------------------
|
|
265
|
+
# Предмет живёт помощником рядом — `git-guard-delivery-signature.sh`: гард дорос до предела
|
|
266
|
+
# длины, и подпись среди его предметов самая отдельная. Нет помощника — подпись не судится, а
|
|
267
|
+
# работа идёт дальше: это тот же отказ в пользу работы, что и у остальных условий.
|
|
268
|
+
# shellcheck disable=SC1090
|
|
269
|
+
[ -f "$rt_hooks_dir/git-guard-delivery-signature.sh" ] \
|
|
270
|
+
&& . "$rt_hooks_dir/git-guard-delivery-signature.sh" 2>/dev/null
|
|
271
|
+
command -v rt_delivery_signature >/dev/null 2>&1 && rt_delivery_signature
|
|
272
|
+
|
|
273
|
+
# --- снятие черновика --------------------------------------------------------------------
|
|
180
274
|
#
|
|
181
|
-
#
|
|
182
|
-
#
|
|
183
|
-
#
|
|
184
|
-
# и
|
|
185
|
-
#
|
|
186
|
-
# Точка — пуш: до него подпись переписывается на месте всей веткой сразу, после — только силовой
|
|
187
|
-
# отправкой. На коммите гард не стоит: почта задаётся переменными самой команды, и разбор её из
|
|
188
|
-
# текста ловил бы ту же строку, которая и так перед глазами у набравшего её.
|
|
189
|
-
#
|
|
190
|
-
# Судится коммит, НАЗВАВШИЙСЯ машинной записью: её логин стоит именем автора либо левой частью
|
|
191
|
-
# почты. Опознавать по самой почте нельзя — она в таком коммите как раз и неверна, а требовать
|
|
192
|
-
# машинную подпись от каждого коммита значило бы отбивать работу, сделанную человеком своими
|
|
193
|
-
# руками в том же дереве.
|
|
275
|
+
# Ревьювера до этой правки не спрашивал никто. Он стоял прозой в правиле, а хостинг запрос
|
|
276
|
+
# разбора на самого себя принимает молча и не создаёт: разбор при этом выглядит запрошенным.
|
|
277
|
+
# Раньше снятия черновика спросить негде — до открытия заявки ревьювера нет вовсе, — а само
|
|
278
|
+
# снятие и есть тот ход, которым работа объявляется готовой.
|
|
194
279
|
#
|
|
195
|
-
#
|
|
196
|
-
# логина разошлись бы молча. Исполнителем задачи он тоже не бывает — в дереве, где хостинг
|
|
197
|
-
# ограничил машинную запись, исполнителем ставят человека, а коммит остаётся машинным.
|
|
280
|
+
# Ярус сетевой, и молчит он так же, как ярус состояния задачи: нет ответа — нет требования.
|
|
198
281
|
#
|
|
199
|
-
#
|
|
200
|
-
#
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
#
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
282
|
+
# Возврат заявки в черновик под требование не подпадает: он делает ровно то, чего гард и
|
|
283
|
+
# добивается, — снимает с работы вид готовой.
|
|
284
|
+
if printf '%s' "$cmd" | grep -qE '(^|[;&|(]|&&|\|\|)[[:space:]]*(gh[[:space:]]+pr[[:space:]]+ready|glab[[:space:]]+mr[[:space:]]+update[^|;&]*--ready)([[:space:]]|$)' \
|
|
285
|
+
&& ! printf '%s' "$cmd" | grep -q -- '--undo'; then
|
|
286
|
+
# Ссылка на заявку необязательна: без неё клиент берёт заявку текущей ветки, и это самая
|
|
287
|
+
# короткая форма вызова. Требовать номер значило бы снимать всё требование одним пробелом.
|
|
288
|
+
# Поэтому берётся первый довод, каким бы он ни был — номер, адрес или имя ветки, — а его
|
|
289
|
+
# отсутствие означает «спроси про текущую ветку».
|
|
290
|
+
pull_ref="$(printf '%s' "$cmd" | sed -nE 's/.*(gh[[:space:]]+pr[[:space:]]+ready|glab[[:space:]]+mr[[:space:]]+update)[[:space:]]+([^[:space:];&|-][^[:space:];&|]*).*/\2/p' | head -1)"
|
|
291
|
+
if rt_needs rt_pull_state git-guard-delivery; then
|
|
292
|
+
pull="$(cd "$root" && rt_pull_state "$pull_ref" 2>/dev/null)" || pull=''
|
|
293
|
+
if [ -n "$pull" ] && printf '%s' "$pull" | jq -e '.exists' >/dev/null 2>&1; then
|
|
294
|
+
# Номер берётся из ответа, а если его там нет — из самой команды: заявка,
|
|
295
|
+
# названная адресом или именем ветки, в отказе должна остаться узнаваемой.
|
|
296
|
+
pull_name="$(printf '%s' "$pull" | jq -r '.number // empty' 2>/dev/null)"
|
|
297
|
+
# Номер пишется с решёткой, а имя ветки или адрес — в кавычках: решётка перед
|
|
298
|
+
# адресом читается как опечатка, а не как ссылка на заявку.
|
|
299
|
+
[ -z "$pull_name" ] && pull_name="$pull_ref"
|
|
300
|
+
case "$pull_name" in
|
|
301
|
+
'') ;;
|
|
302
|
+
*[!0-9]*) pull_name=" «${pull_name}»" ;;
|
|
303
|
+
*) pull_name=" #${pull_name}" ;;
|
|
304
|
+
esac
|
|
305
|
+
printf '%s' "$pull" | jq -e '.reviewed' >/dev/null 2>&1 \
|
|
306
|
+
|| fault "у заявки${pull_name} нет разбора: ревьювер не запрошен и отзыва никто не оставлял. Снятый черновик читается как «можно вливать», а вливать некому — назначь ревьювера и повтори."
|
|
307
|
+
|
|
308
|
+
# Конфликт приезжает в отданную заявку чужим слиянием, без единого действия её
|
|
309
|
+
# автора: основание, проверенное на открытии, к моменту снятия черновика уже
|
|
310
|
+
# вчерашнее. Снятый черновик читается как «можно вливать», а вливать нечего —
|
|
311
|
+
# владелец открывает заявку и находит там конфликт. Молчание при неизвестной
|
|
312
|
+
# сливаемости остаётся: хостинг считает её заново после каждой правки главной ветки,
|
|
313
|
+
# и «ещё не посчитано» — не «конфликтует».
|
|
314
|
+
printf '%s' "$pull" | jq -e '.conflicting' >/dev/null 2>&1 \
|
|
315
|
+
&& fault "заявка${pull_name} конфликтует с главной веткой. Влей её в свою ветку, разбери конфликт и повтори: снятый черновик читается как «можно вливать», а слить эту заявку нельзя."
|
|
316
|
+
fi
|
|
234
317
|
fi
|
|
318
|
+
deny_faults
|
|
235
319
|
fi
|
|
236
|
-
# Своего выхода у этой точки нет: пуш бывает и составной командой, а второе её звено судят
|
|
237
|
-
# разделы ниже.
|
|
238
320
|
|
|
239
321
|
# --- слияние заявки ----------------------------------------------------------------------
|
|
240
322
|
#
|
|
@@ -314,11 +396,11 @@ fi
|
|
|
314
396
|
|
|
315
397
|
if [ -n "$title" ]; then
|
|
316
398
|
printf '%s' "$title" | grep -qE "$title_re" \
|
|
317
|
-
||
|
|
399
|
+
|| fault "заголовок заявки не начинается с номера задачи. В списке заявок тела не видно, а строка связи живёт именно там — без номера в заголовке PR с задачей не сопоставить."
|
|
318
400
|
title_number="$(printf '%s' "$title" | sed -nE 's/^\[[A-Za-z]+-([0-9]+)\].*/\1/p')"
|
|
319
401
|
if [ -n "$number" ] && [ -n "$title_number" ]; then
|
|
320
402
|
[ "$title_number" = "$number" ] \
|
|
321
|
-
||
|
|
403
|
+
|| fault "в заголовке заявки номер ${title_number}, у ветки — ${number}. Задача, ветка и PR несут один и тот же номер."
|
|
322
404
|
fi
|
|
323
405
|
fi
|
|
324
406
|
|
|
@@ -336,7 +418,7 @@ fi
|
|
|
336
418
|
if git rev-parse --verify --quiet "refs/remotes/origin/${main_branch}" >/dev/null 2>&1 \
|
|
337
419
|
&& ! git merge-base --is-ancestor "origin/${main_branch}" HEAD 2>/dev/null; then
|
|
338
420
|
behind="$(git rev-list --count "HEAD..origin/${main_branch}" 2>/dev/null)"
|
|
339
|
-
|
|
421
|
+
fault "«${main_branch}» ушла вперёд на ${behind:-несколько} коммитов, а в ветку не влита. PR от разошедшейся ветки показывает ревьюверу правку вперемешку с чужой, а проверки на нём идут от устаревшего основания. Влей и повтори: git fetch origin && git merge origin/${main_branch} — порядок и разбор конфликта в паттерне git-workflow-merge."
|
|
340
422
|
fi
|
|
341
423
|
|
|
342
424
|
# Второй ярус: локальная ссылка сама могла протухнуть. Ответа нет — ярус молчит.
|
|
@@ -353,10 +435,14 @@ if [ -n "$remote_main" ] && [ -n "$local_main" ] && [ "$remote_main" != "$local_
|
|
|
353
435
|
if [ -n "$fetched_at" ]; then
|
|
354
436
|
age=" Последний git fetch — $(( ( $(date +%s) - fetched_at ) / 60 )) мин. назад."
|
|
355
437
|
fi
|
|
356
|
-
|
|
438
|
+
fault "твоя ссылка origin/${main_branch} отстала от удалённой — ${local_main:0:8} против ${remote_main:0:8}.${age} Гард сравнивает ветку с тем, что лежит в дереве, поэтому молчание первого яруса значит «ссылка не старше ветки», а не «главная ветка влита». Влей и повтори: git fetch origin && git merge origin/${main_branch}."
|
|
357
439
|
fi
|
|
358
440
|
|
|
359
|
-
check_task "$number" "заявка с ветки «${branch}»"
|
|
441
|
+
check_task "$number" "заявка с ветки «${branch}»" да
|
|
442
|
+
|
|
443
|
+
# Всё несошедшееся названо здесь, разом: до этой строки собирались условия, каждое из которых
|
|
444
|
+
# прежде отбивало вызов в одиночку.
|
|
445
|
+
deny_faults
|
|
360
446
|
|
|
361
447
|
# Сейчас папка ещё нужна: правки по замечаниям ревью идут в эту же ветку, а без plan.md их не
|
|
362
448
|
# пропустит гард хода работы. Поэтому здесь только напоминание. Требование стоит на слиянии —
|
|
@@ -1,5 +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/deny-tail.sh
|
|
3
4
|
# Гард главной ветки. PreToolUse на вызове коммита.
|
|
4
5
|
#
|
|
5
6
|
# Коммит в главную ветку минует ветку, PR и разбор, а поставка построена на них целиком —
|
|
@@ -75,6 +76,17 @@ rt_hooks_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
75
76
|
command -v rt_note >/dev/null 2>&1 \
|
|
76
77
|
&& rt_note guard-deny res=git-guard-main "sid=$(printf '%s' "$input" | jq -r '.session_id // "nosession"' 2>/dev/null)"
|
|
77
78
|
|
|
79
|
+
# Общий хвост отказа: два законных хода и законная форма обхода, если она у отказа есть.
|
|
80
|
+
# Файл может быть не разложен — тогда хвоста нет, а причина отказа остаётся прежней.
|
|
81
|
+
# shellcheck disable=SC1090
|
|
82
|
+
[ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" ] \
|
|
83
|
+
&& . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" 2>/dev/null
|
|
84
|
+
command -v rt_deny_tail >/dev/null 2>&1 || rt_deny_tail() { :; }
|
|
85
|
+
deny_tail_text="$(rt_deny_tail "")"
|
|
86
|
+
[ -n "$deny_tail_text" ] && reason="${reason}
|
|
87
|
+
|
|
88
|
+
${deny_tail_text}"
|
|
89
|
+
|
|
78
90
|
jq -n --arg r "$reason" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$r}}' 2>/dev/null \
|
|
79
91
|
|| printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Коммит в главную ветку отбит. Заведи ветку."}}\n'
|
|
80
92
|
|
|
@@ -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
|
|
3
|
+
# Требует: hooks/profile-check.sh, hooks/deny-tail.sh
|
|
4
4
|
# Гард проверок перед пушем. PreToolUse на вызове пуша.
|
|
5
5
|
#
|
|
6
6
|
# Пуш — это вход в конвейер: слияние в главную ветку запускает выкатку, и всё, что не
|
|
@@ -43,13 +43,49 @@ cmd="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null)"
|
|
|
43
43
|
# ключами `-c` между ними, и ровно этой формой здесь и пушат. Пока признаком была подстрока,
|
|
44
44
|
# весь набор гейта на таком пуше не гонялся вовсе, а молчание гарда читалось как «зелено».
|
|
45
45
|
printf '%s' "$cmd" | grep -qE '(^|[;&|(]|&&|\|\|)[[:space:]]*git([[:space:]]|$)' || exit 0
|
|
46
|
-
|
|
46
|
+
|
|
47
|
+
# Отложенная правка пушем не бывает: `git stash push` кладёт правку в тайник этой же машины и
|
|
48
|
+
# наружу не отправляет ничего. Слово `push` в ней стоит отдельным, и без этой строки гард гонял
|
|
49
|
+
# на ней весь набор, а потом отбивал вызов на первой красной проверке — то есть отбивал команду,
|
|
50
|
+
# которая ничего никуда не отправляет. Тайник вырезается из строки, и признак считается по
|
|
51
|
+
# остатку: в составной команде рядом с ним может стоять и настоящий пуш.
|
|
52
|
+
probe="$(printf '%s' "$cmd" | sed -E 's/git[[:space:]]+stash[[:space:]]+push/git stash/g')"
|
|
53
|
+
printf '%s' "$probe" | grep -qE '(^|[[:space:]])push([[:space:]]|$)' || exit 0
|
|
47
54
|
|
|
48
55
|
# Пробный пуш ничего не отправляет: гонять ради него весь набор незачем.
|
|
49
56
|
case "$cmd" in
|
|
50
57
|
*--dry-run*) exit 0 ;;
|
|
51
58
|
esac
|
|
52
59
|
|
|
60
|
+
# Переключение ветки в той же команде отбивается целиком.
|
|
61
|
+
#
|
|
62
|
+
# Гард — это разбор команды ДО её запуска: набор он гоняет в том дереве, какое лежит сейчас.
|
|
63
|
+
# Составная «переключиться и запушить» проходит гейт по ПРЕЖНЕЙ ветке — молча, проверяя не то.
|
|
64
|
+
# Отказа при этом нет, и зелёный набор читается как проверка того, что уходит на хостинг.
|
|
65
|
+
# Поймано это было случайно: гейт отбил пуш красной проверкой длины файла, которого в пушимой
|
|
66
|
+
# ветке нет вовсе — он смотрел ветку, с которой в этой же команде уходили.
|
|
67
|
+
#
|
|
68
|
+
# Судится переключение на существующую ветку. Заведение новой (`checkout -b`, `switch -c`)
|
|
69
|
+
# сюда не попадает: у свежей ветки дерево то же самое, что и было.
|
|
70
|
+
if printf '%s' "$cmd" | grep -qE '(^|[;&|(]|&&|\|\|)[[:space:]]*git[[:space:]]+(checkout|switch)[[:space:]]+' &&
|
|
71
|
+
! printf '%s' "$cmd" | grep -qE 'git[[:space:]]+(checkout[[:space:]]+-b|switch[[:space:]]+-c)([[:space:]]|$)'; then
|
|
72
|
+
reason="BLOCKED: переключение ветки и пуш одной командой. Набор гейта гоняется в том дереве, какое лежит на момент разбора команды, — то есть по ПРЕЖНЕЙ ветке, а не по той, что уходит на хостинг. Зелёный набор при этом читается как проверка ушедшего, хотя проверял он другое. Раздели вызовы: сперва переключись, затем отдельной командой пушь."
|
|
73
|
+
# Общий хвост отказа: два законных хода и законная форма обхода, если она у отказа есть.
|
|
74
|
+
# Файл может быть не разложен — тогда хвоста нет, а причина отказа остаётся прежней.
|
|
75
|
+
# shellcheck disable=SC1090
|
|
76
|
+
[ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" ] \
|
|
77
|
+
&& . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" 2>/dev/null
|
|
78
|
+
command -v rt_deny_tail >/dev/null 2>&1 || rt_deny_tail() { :; }
|
|
79
|
+
deny_tail_text="$(rt_deny_tail "")"
|
|
80
|
+
[ -n "$deny_tail_text" ] && reason="${reason}
|
|
81
|
+
|
|
82
|
+
${deny_tail_text}"
|
|
83
|
+
|
|
84
|
+
jq -n --arg r "$reason" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$r}}' 2>/dev/null \
|
|
85
|
+
|| printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Переключение ветки и пуш одной командой."}}\n'
|
|
86
|
+
exit 0
|
|
87
|
+
fi
|
|
88
|
+
|
|
53
89
|
workdir="$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null)"
|
|
54
90
|
[ -z "$workdir" ] && workdir="${CLAUDE_PROJECT_DIR:-.}"
|
|
55
91
|
cd "$workdir" 2>/dev/null || exit 0
|
|
@@ -99,6 +135,17 @@ reason="BLOCKED: пуш без зелёного локального прого
|
|
|
99
135
|
|
|
100
136
|
${tail_out}"
|
|
101
137
|
|
|
138
|
+
# Общий хвост отказа: два законных хода и законная форма обхода, если она у отказа есть.
|
|
139
|
+
# Файл может быть не разложен — тогда хвоста нет, а причина отказа остаётся прежней.
|
|
140
|
+
# shellcheck disable=SC1090
|
|
141
|
+
[ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" ] \
|
|
142
|
+
&& . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" 2>/dev/null
|
|
143
|
+
command -v rt_deny_tail >/dev/null 2>&1 || rt_deny_tail() { :; }
|
|
144
|
+
deny_tail_text="$(rt_deny_tail "")"
|
|
145
|
+
[ -n "$deny_tail_text" ] && reason="${reason}
|
|
146
|
+
|
|
147
|
+
${deny_tail_text}"
|
|
148
|
+
|
|
102
149
|
jq -n --arg r "$reason" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$r}}' 2>/dev/null \
|
|
103
150
|
|| printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Проверки перед пушем не прошли."}}\n'
|
|
104
151
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# rt-hook: PreToolUse AskUserQuestion
|
|
3
|
+
# Требует: hooks/deny-tail.sh
|
|
3
4
|
# rt-hook: Stop
|
|
4
5
|
# Гард разговора: вопрос владельцу не задаётся, пока за этот же ход не читались законы и
|
|
5
6
|
# правила. Стоит на двух событиях, и это не дублирование.
|
|
@@ -105,6 +106,17 @@ reason="$head Вопрос, ответ на который уже записан
|
|
|
105
106
|
|
|
106
107
|
Гард судит один ход: следующий заход не отбивается."
|
|
107
108
|
|
|
109
|
+
# Общий хвост отказа: два законных хода и законная форма обхода, если она у отказа есть.
|
|
110
|
+
# Файл может быть не разложен — тогда хвоста нет, а причина отказа остаётся прежней.
|
|
111
|
+
# shellcheck disable=SC1090
|
|
112
|
+
[ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" ] \
|
|
113
|
+
&& . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" 2>/dev/null
|
|
114
|
+
command -v rt_deny_tail >/dev/null 2>&1 || rt_deny_tail() { :; }
|
|
115
|
+
deny_tail_text="$(rt_deny_tail "")"
|
|
116
|
+
[ -n "$deny_tail_text" ] && reason="${reason}
|
|
117
|
+
|
|
118
|
+
${deny_tail_text}"
|
|
119
|
+
|
|
108
120
|
# Форма отказа у двух событий разная: вызов инструмента отбивается решением о доступе, а
|
|
109
121
|
# завершение хода — решением о ходе. Одна форма на оба события молча не срабатывает.
|
|
110
122
|
if [ -n "$tool" ]; then
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# rt-hook: PreToolUse Edit|Write|MultiEdit
|
|
3
|
+
# Требует: rules/task-flow.md, hooks/deny-tail.sh
|
|
4
|
+
# Гард входа из передачи: заход, начатый с передачи, не правит файлов, пока не загружено правило
|
|
5
|
+
# ведения работы.
|
|
6
|
+
#
|
|
7
|
+
# Передача написана прошлым заходом, лежит вне дерева и не читается ни одной проверкой. Её
|
|
8
|
+
# читают как задание — и берутся за работу мимо правила: состояние не сверено с деревом, числа
|
|
9
|
+
# взяты на веру, порядок входа исполняется по памяти. Порядок, записанный только словами,
|
|
10
|
+
# исполняется, пока о нём помнят.
|
|
11
|
+
#
|
|
12
|
+
# Заход узнаётся по первой реплике владельца: в ней назван путь к передаче либо её текст.
|
|
13
|
+
# Загрузка правила — по вызову инструмента правил за тот же заход.
|
|
14
|
+
#
|
|
15
|
+
# FAIL-OPEN: нет `jq`, нет записи хода, передачи в реплике нет → пропуск.
|
|
16
|
+
|
|
17
|
+
input="$(cat 2>/dev/null)"
|
|
18
|
+
[ -z "$input" ] && exit 0
|
|
19
|
+
command -v jq >/dev/null 2>&1 || exit 0
|
|
20
|
+
|
|
21
|
+
tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
|
|
22
|
+
case "$tool" in
|
|
23
|
+
Edit | Write | MultiEdit) ;;
|
|
24
|
+
*) exit 0 ;;
|
|
25
|
+
esac
|
|
26
|
+
|
|
27
|
+
transcript="$(printf '%s' "$input" | jq -r '.transcript_path // empty' 2>/dev/null)"
|
|
28
|
+
[ -z "$transcript" ] && exit 0
|
|
29
|
+
[ -f "$transcript" ] || exit 0
|
|
30
|
+
|
|
31
|
+
# Имя правила ведения работы у дерева своё, но каталог передач общий: гард смотрит оба признака.
|
|
32
|
+
rule="${RT_TASK_FLOW_RULE:-task-flow}"
|
|
33
|
+
|
|
34
|
+
verdict="$(jq -s -r --arg rule "$rule" '
|
|
35
|
+
[.[] | select(.type == "user") | .message.content
|
|
36
|
+
| if type == "string" then . elif type == "array"
|
|
37
|
+
then (map(if type == "object" then (.text // "") else "" end) | join("\n")) else "" end] as $said
|
|
38
|
+
| [.[] | select(.type == "assistant") | (.message.content // [])[] | select(.type == "tool_use")
|
|
39
|
+
| ((.name // "") + " " + ((.input.skill // .input.command // "") | tostring))] as $used
|
|
40
|
+
| (($said | join("\n")) | test("handoff|передач[аи][[:space:]]+захода")) as $from_handoff
|
|
41
|
+
| (($used | join("\n")) | test("Skill[[:space:]]+" + $rule + "|skills/" + $rule)) as $loaded
|
|
42
|
+
| if ($from_handoff and ($loaded | not)) then "нет-правила" else "ладно" end
|
|
43
|
+
' "$transcript" 2>/dev/null)"
|
|
44
|
+
|
|
45
|
+
[ "$verdict" = "нет-правила" ] || exit 0
|
|
46
|
+
|
|
47
|
+
reason="BLOCKED by handoff-entry-guard: заход начат с передачи, а правило ведения работы за него не загружено.
|
|
48
|
+
|
|
49
|
+
Передача написана прошлым заходом, лежит вне дерева и не читается ни одной проверкой: всё, что в ней стоит, проверяется деревом. Порядок входа — четыре шага:
|
|
50
|
+
|
|
51
|
+
1. правило ведения работы и паттерн возвращения — первым движением;
|
|
52
|
+
2. ветка и состояние работы читаются в дереве, а не в передаче;
|
|
53
|
+
3. числа из передачи пересчитываются на текущем коммите;
|
|
54
|
+
4. следующий шаг берётся из хода работы.
|
|
55
|
+
|
|
56
|
+
Загрузи правило и повтори правку."
|
|
57
|
+
|
|
58
|
+
# Общий хвост отказа: два законных хода и законная форма обхода, если она у отказа есть.
|
|
59
|
+
# Файл может быть не разложен — тогда хвоста нет, а причина отказа остаётся прежней.
|
|
60
|
+
# shellcheck disable=SC1090
|
|
61
|
+
[ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" ] \
|
|
62
|
+
&& . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" 2>/dev/null
|
|
63
|
+
command -v rt_deny_tail >/dev/null 2>&1 || rt_deny_tail() { :; }
|
|
64
|
+
deny_tail_text="$(rt_deny_tail "")"
|
|
65
|
+
[ -n "$deny_tail_text" ] && reason="${reason}
|
|
66
|
+
|
|
67
|
+
${deny_tail_text}"
|
|
68
|
+
|
|
69
|
+
jq -n --arg r "$reason" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$r}}' 2>/dev/null \
|
|
70
|
+
|| printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"handoff-entry-guard: правило ведения работы не загружено."}}\n'
|
|
71
|
+
exit 0
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# rt-hook: Stop
|
|
3
|
+
# Требует: hooks/deny-tail.sh
|
|
3
4
|
# Гард происшествия: ход, в котором исполнитель признал промах, не заканчивается, пока записи о
|
|
4
5
|
# происшествии нет. Stop.
|
|
5
6
|
#
|
|
@@ -86,6 +87,17 @@ reason="BLOCKED by postmortem-guard: в ответе признан промах
|
|
|
86
87
|
|
|
87
88
|
Гард судит один ход: следующий заход не отбивается."
|
|
88
89
|
|
|
90
|
+
# Общий хвост отказа: два законных хода. Файл может быть не разложен — тогда хвоста нет,
|
|
91
|
+
# а причина отказа остаётся прежней.
|
|
92
|
+
# shellcheck disable=SC1090
|
|
93
|
+
[ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" ] \
|
|
94
|
+
&& . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" 2>/dev/null
|
|
95
|
+
command -v rt_deny_tail >/dev/null 2>&1 || rt_deny_tail() { :; }
|
|
96
|
+
deny_tail_text="$(rt_deny_tail "")"
|
|
97
|
+
[ -n "$deny_tail_text" ] && reason="${reason}
|
|
98
|
+
|
|
99
|
+
${deny_tail_text}"
|
|
100
|
+
|
|
89
101
|
jq -n --arg r "$reason" '{decision:"block",reason:$r}' 2>/dev/null \
|
|
90
102
|
|| printf '{"decision":"block","reason":"postmortem-guard: признан промах — запиши разбор происшествия."}\n'
|
|
91
103
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# rt-hook: Stop
|
|
3
|
+
# Требует: hooks/deny-tail.sh
|
|
3
4
|
# Гард предложения: ход, в котором владелец сказал завести или отправить предложение слою правил,
|
|
4
5
|
# не заканчивается, пока отправки не было. Stop.
|
|
5
6
|
#
|
|
@@ -87,6 +88,17 @@ reason="BLOCKED by proposal-guard: владелец сказал завести
|
|
|
87
88
|
|
|
88
89
|
Гард судит один ход: следующий заход не отбивается."
|
|
89
90
|
|
|
91
|
+
# Общий хвост отказа: два законных хода. Файл может быть не разложен — тогда хвоста нет,
|
|
92
|
+
# а причина отказа остаётся прежней.
|
|
93
|
+
# shellcheck disable=SC1090
|
|
94
|
+
[ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" ] \
|
|
95
|
+
&& . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" 2>/dev/null
|
|
96
|
+
command -v rt_deny_tail >/dev/null 2>&1 || rt_deny_tail() { :; }
|
|
97
|
+
deny_tail_text="$(rt_deny_tail "")"
|
|
98
|
+
[ -n "$deny_tail_text" ] && reason="${reason}
|
|
99
|
+
|
|
100
|
+
${deny_tail_text}"
|
|
101
|
+
|
|
90
102
|
jq -n --arg r "$reason" '{decision:"block",reason:$r}' 2>/dev/null \
|
|
91
103
|
|| printf '{"decision":"block","reason":"proposal-guard: владелец просил предложение — отправь его командой пакета."}\n'
|
|
92
104
|
|