@rt-tools/agent-kit 0.23.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +8 -2
  2. package/assets/checks/board.github.mjs +1 -1
  3. package/assets/checks/check-board.github.mjs +14 -0
  4. package/assets/checks/check-doc-paths.mjs +24 -5
  5. package/assets/checks/check-file-size.mjs +8 -2
  6. package/assets/checks/check-prose-style.mjs +10 -1
  7. package/assets/defaults/gate-map.sh +13 -0
  8. package/assets/defaults/project.sh +10 -0
  9. package/assets/defaults/shell.sh +18 -3
  10. package/assets/hooks/browser-guard-device-id.sh +42 -12
  11. package/assets/hooks/dispatch.sh +40 -9
  12. package/assets/hooks/docs-guard.sh +10 -0
  13. package/assets/hooks/exam-guard.sh +66 -16
  14. package/assets/hooks/git-guard-delivery-draft.sh +78 -0
  15. package/assets/hooks/git-guard-delivery.sh +49 -111
  16. package/assets/hooks/git-guard-main.sh +39 -4
  17. package/assets/hooks/git-guard-push-tests.sh +49 -3
  18. package/assets/hooks/hook-input.sh +17 -6
  19. package/assets/hooks/rule-source-guard.sh +11 -0
  20. package/assets/hooks/stand-login-guard.sh +101 -0
  21. package/assets/hooks/write-targets.sh +37 -4
  22. package/assets/laws/verifiability.md +12 -2
  23. package/assets/laws/work-conduct.md +59 -65
  24. package/assets/patterns/browser-verification-measure.md +41 -1
  25. package/assets/patterns/browser-verification-stand.md +56 -15
  26. package/assets/patterns/doc-style-human.md +75 -0
  27. package/assets/patterns/git-workflow-commit.azure.md +12 -0
  28. package/assets/patterns/git-workflow-commit.github.md +16 -3
  29. package/assets/patterns/git-workflow-commit.gitlab.md +12 -0
  30. package/assets/patterns/git-workflow-merge.md +8 -0
  31. package/assets/patterns/task-flow-start.md +1 -1
  32. package/assets/patterns/testing-e2e.md +18 -8
  33. package/assets/pitfalls/task-flow.md +40 -40
  34. package/assets/rules/browser-verification.md +29 -3
  35. package/assets/rules/doc-style.md +36 -0
  36. package/assets/rules/git-workflow.github.md +20 -23
  37. package/assets/rules/reuse-first.md +8 -2
  38. package/assets/rules/styling-bem.md +8 -1
  39. package/assets/rules/task-flow.md +73 -73
  40. package/assets/rules/testing.md +21 -0
  41. package/assets/skills/agent-kit.md +60 -70
  42. package/lib/commands.d.ts.map +1 -1
  43. package/lib/commands.js +63 -2
  44. package/lib/commands.js.map +1 -1
  45. package/lib/enroll.d.ts.map +1 -1
  46. package/lib/enroll.js +1 -1
  47. package/lib/enroll.js.map +1 -1
  48. package/lib/observations.d.ts +10 -1
  49. package/lib/observations.d.ts.map +1 -1
  50. package/lib/observations.js +1 -0
  51. package/lib/observations.js.map +1 -1
  52. package/lib/override-marks.d.ts +24 -0
  53. package/lib/override-marks.d.ts.map +1 -0
  54. package/lib/override-marks.js +98 -0
  55. package/lib/override-marks.js.map +1 -0
  56. package/lib/shipment.d.ts.map +1 -1
  57. package/lib/shipment.js +1 -1
  58. package/lib/shipment.js.map +1 -1
  59. package/package.json +1 -1
  60. package/rt-tools-agent-kit-0.24.0.tgz +0 -0
  61. package/rt-tools-agent-kit-0.23.0.tgz +0 -0
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env bash
2
+ # Снятие черновика для гарда поставки: есть ли у заявки разбор, не конфликтует ли она и той ли
3
+ # записью открыта.
4
+ #
5
+ # Строки `# rt-hook:` здесь нет намеренно: событие и образец вызова объявляет сам гард, а
6
+ # помощник рядом хуком не регистрируется и в одиночку ничего не решает. Он зовётся из гарда и
7
+ # пользуется его же переменными — командой, корнем дерева, машинной записью — и его отказом.
8
+ #
9
+ # Вынесен он отсюда потому, что гард дорос до предела длины: снятие черновика среди его
10
+ # предметов самое отдельное — своя точка проверки, свой сетевой ярус, свои три отказа.
11
+
12
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
13
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
14
+
15
+ rt_delivery_draft_ready() {
16
+ # --- снятие черновика --------------------------------------------------------------------
17
+ #
18
+ # Ревьювера до этой правки не спрашивал никто. Он стоял прозой в правиле, а хостинг запрос
19
+ # разбора на самого себя принимает молча и не создаёт: разбор при этом выглядит запрошенным.
20
+ # Раньше снятия черновика спросить негде — до открытия заявки ревьювера нет вовсе, — а само
21
+ # снятие и есть тот ход, которым работа объявляется готовой.
22
+ #
23
+ # Ярус сетевой, и молчит он так же, как ярус состояния задачи: нет ответа — нет требования.
24
+ #
25
+ # Возврат заявки в черновик под требование не подпадает: он делает ровно то, чего гард и
26
+ # добивается, — снимает с работы вид готовой.
27
+ if printf '%s' "$cmd" | grep -qE "${RT_CMD_BOUND}(gh[[:space:]]+pr[[:space:]]+ready|glab[[:space:]]+mr[[:space:]]+update[^|;&]*--ready)([[:space:]]|\$)" \
28
+ && ! printf '%s' "$cmd" | grep -q -- '--undo'; then
29
+ # Ссылка на заявку необязательна: без неё клиент берёт заявку текущей ветки, и это самая
30
+ # короткая форма вызова. Требовать номер значило бы снимать всё требование одним пробелом.
31
+ # Поэтому берётся первый довод, каким бы он ни был — номер, адрес или имя ветки, — а его
32
+ # отсутствие означает «спроси про текущую ветку».
33
+ 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)"
34
+ if rt_needs rt_pull_state git-guard-delivery; then
35
+ pull="$(cd "$root" && rt_pull_state "$pull_ref" 2>/dev/null)" || pull=''
36
+ if [ -n "$pull" ] && printf '%s' "$pull" | jq -e '.exists' >/dev/null 2>&1; then
37
+ # Номер берётся из ответа, а если его там нет — из самой команды: заявка,
38
+ # названная адресом или именем ветки, в отказе должна остаться узнаваемой.
39
+ pull_name="$(printf '%s' "$pull" | jq -r '.number // empty' 2>/dev/null)"
40
+ # Номер пишется с решёткой, а имя ветки или адрес — в кавычках: решётка перед
41
+ # адресом читается как опечатка, а не как ссылка на заявку.
42
+ [ -z "$pull_name" ] && pull_name="$pull_ref"
43
+ case "$pull_name" in
44
+ '') ;;
45
+ *[!0-9]*) pull_name=" «${pull_name}»" ;;
46
+ *) pull_name=" #${pull_name}" ;;
47
+ esac
48
+ printf '%s' "$pull" | jq -e '.reviewed' >/dev/null 2>&1 \
49
+ || fault "у заявки${pull_name} нет разбора: ревьювер не запрошен и отзыва никто не оставлял. Снятый черновик читается как «можно вливать», а вливать некому — назначь ревьювера и повтори."
50
+
51
+ # Конфликт приезжает в отданную заявку чужим слиянием, без единого действия её
52
+ # автора: основание, проверенное на открытии, к моменту снятия черновика уже
53
+ # вчерашнее. Снятый черновик читается как «можно вливать», а вливать нечего —
54
+ # владелец открывает заявку и находит там конфликт. Молчание при неизвестной
55
+ # сливаемости остаётся: хостинг считает её заново после каждой правки главной ветки,
56
+ # и «ещё не посчитано» — не «конфликтует».
57
+ printf '%s' "$pull" | jq -e '.conflicting' >/dev/null 2>&1 \
58
+ && fault "заявка${pull_name} конфликтует с главной веткой. Влей её в свою ветку, разбери конфликт и повтори: снятый черновик читается как «можно вливать», а слить эту заявку нельзя."
59
+
60
+ # Автор заявки. На открытии судить было нечем, кроме текста команды: личность вызова
61
+ # приходит окружением. Здесь она уже названа хостингом, и это последний ход, где
62
+ # промах ещё исправим — после снятия черновика заявку вливают, а переоткрыть влитую
63
+ # нельзя. Дерево, не назвавшее машинной записи, автора не судит.
64
+ if [ -n "$task_bot" ]; then
65
+ pull_author="$(printf '%s' "$pull" | jq -r '.author // empty' 2>/dev/null)"
66
+ [ -n "$pull_author" ] && [ "$pull_author" != "$task_bot" ] \
67
+ && fault "заявку${pull_name} открыла запись «${pull_author}», а не машинная «${task_bot}». Автор заявки её ревьювером не бывает, и разбор ей назначить нечем. Автора не сменить — закрой заявку и открой заново${pull_token_hint:+, подставив токен: ${pull_token_hint} …}."
68
+ fi
69
+ fi
70
+ fi
71
+
72
+ # Папка задачи: тот же предмет, что на открытии и на слиянии, третьим рубежом. Условие
73
+ # местное — оно читает ветку, а не хостинг, — и потому стоит вне сетевого яруса выше.
74
+ command -v rt_delivery_ready_folder >/dev/null 2>&1 && rt_delivery_ready_folder
75
+
76
+ deny_faults
77
+ fi
78
+ }
@@ -122,15 +122,12 @@ tasks_dir="${RT_TASKS_DIR:-}"
122
122
  archive_dir="${RT_ARCHIVE_DIR:-}"
123
123
  main_branch="${RT_MAIN_BRANCH:-main}"
124
124
 
125
- # Обход требования: строка с причиной. Причина видна тому, кто вливает, поэтому обход законен.
126
- # Без причины это просто молчаливый пропуск, поэтому она обязательна. Порог в три знака — тот
127
- # же, что у гарда документа: если сделать по-разному, две формы одного обхода разойдутся.
125
+ # Обход требования: строка с причиной. Причину видит тот, кто вливает, поэтому обход разрешён;
126
+ # без причины это молчаливый пропуск. Порог в три знака — тот же, что у гарда документа.
128
127
  #
129
- # Строка обхода начинает строкусвою в теле PR или комментарий в конце команды — и
130
- # подстановки не принимает. Без этих двух условий текст, который ОБЪЯСНЯЕТ, что обход называется
131
- # так-то, от самого обхода неотличим: тело PR со строкой-примером снимало требование само
132
- # собой, и тот же пример гард печатает в своём отказе ниже. Нашлось это приёмкой такого же
133
- # признака — прогон обязан был покраснеть на настоящем нарушении и остался зелёным.
128
+ # Строка обхода стоит в начале строки своей в теле PR или комментария в конце команды — и
129
+ # подстановки не принимает. Иначе текст, который объясняет, как называется обход, неотличим от
130
+ # самого обхода: тело PR со строкой-примером снимало требование само.
134
131
  folder_skip_re='(^|#)[[:space:]]*Task-folder-skip:[[:space:]]*[^[:space:]<"'"'"'][^[:space:]"'"'"']{2,}'
135
132
 
136
133
  deny() {
@@ -234,18 +231,13 @@ if [ -n "$branch_arg" ]; then
234
231
  || deny "BLOCKED: имя ветки «${branch_arg}» не той формы, что принята здесь. Номер у ветки тот же, что у задачи и у заголовка заявки на слияние."
235
232
  check_task "$number_arg" "ветка «${branch_arg}»"
236
233
 
237
- # Условия поставки, известные уже здесь, спрашиваются здесь. Прежде их спрашивали на
238
- # пуше и на открытии PR то есть после того, как работа сделана: основание чинится
239
- # мержем с разбором конфликта, а подпись коммитов переписыванием всей ветки. В
240
- # начале работы обе стоят одну команду.
234
+ # Условия поставки, известные уже здесь, проверяются здесь. После того как работа
235
+ # сделана, основание чинится слиянием с разбором конфликта, а подпись коммитов
236
+ # переписыванием ветки; в начале работы обе стоят одну команду.
241
237
  #
242
238
  # Основание: вершина главной ветки обязана лежать в том, от чего растёт новая ветка.
243
- # Ветка, заведённая от вчерашнего основания, узнаёт об этом на открытии PR — и узнаёт
244
- # так, что владелец видит её конфликтующей.
245
- #
246
- # Судится названное основание, а не вершина рабочей копии: команда, которой основание
247
- # как раз и берут свежим — `git checkout -b <ветка> origin/<главная>`, — иначе
248
- # отбивалась бы наравне с той, у которой основание вчерашнее.
239
+ # Проверяется названное основание, а не вершина рабочей копии: иначе команда, которой
240
+ # основание берут свежим `git checkout -b <ветка> origin/<главная>`, — запрещалась бы.
249
241
  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)"
250
242
  base_ref="${base_arg:-HEAD}"
251
243
  if git rev-parse --verify --quiet "refs/remotes/origin/${main_branch}" >/dev/null 2>&1 \
@@ -289,68 +281,13 @@ fi
289
281
  && . "$rt_hooks_dir/git-guard-delivery-signature.sh" 2>/dev/null
290
282
  command -v rt_delivery_signature >/dev/null 2>&1 && rt_delivery_signature
291
283
 
292
- # --- снятие черновика --------------------------------------------------------------------
293
- #
294
- # Ревьювера до этой правки не спрашивал никто. Он стоял прозой в правиле, а хостинг запрос
295
- # разбора на самого себя принимает молча и не создаёт: разбор при этом выглядит запрошенным.
296
- # Раньше снятия черновика спросить негде — до открытия заявки ревьювера нет вовсе, — а само
297
- # снятие и есть тот ход, которым работа объявляется готовой.
298
- #
299
- # Ярус сетевой, и молчит он так же, как ярус состояния задачи: нет ответа — нет требования.
300
- #
301
- # Возврат заявки в черновик под требование не подпадает: он делает ровно то, чего гард и
302
- # добивается, — снимает с работы вид готовой.
303
- if printf '%s' "$cmd" | grep -qE "${RT_CMD_BOUND}(gh[[:space:]]+pr[[:space:]]+ready|glab[[:space:]]+mr[[:space:]]+update[^|;&]*--ready)([[:space:]]|\$)" \
304
- && ! printf '%s' "$cmd" | grep -q -- '--undo'; then
305
- # Ссылка на заявку необязательна: без неё клиент берёт заявку текущей ветки, и это самая
306
- # короткая форма вызова. Требовать номер значило бы снимать всё требование одним пробелом.
307
- # Поэтому берётся первый довод, каким бы он ни был — номер, адрес или имя ветки, — а его
308
- # отсутствие означает «спроси про текущую ветку».
309
- 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)"
310
- if rt_needs rt_pull_state git-guard-delivery; then
311
- pull="$(cd "$root" && rt_pull_state "$pull_ref" 2>/dev/null)" || pull=''
312
- if [ -n "$pull" ] && printf '%s' "$pull" | jq -e '.exists' >/dev/null 2>&1; then
313
- # Номер берётся из ответа, а если его там нет — из самой команды: заявка,
314
- # названная адресом или именем ветки, в отказе должна остаться узнаваемой.
315
- pull_name="$(printf '%s' "$pull" | jq -r '.number // empty' 2>/dev/null)"
316
- # Номер пишется с решёткой, а имя ветки или адрес — в кавычках: решётка перед
317
- # адресом читается как опечатка, а не как ссылка на заявку.
318
- [ -z "$pull_name" ] && pull_name="$pull_ref"
319
- case "$pull_name" in
320
- '') ;;
321
- *[!0-9]*) pull_name=" «${pull_name}»" ;;
322
- *) pull_name=" #${pull_name}" ;;
323
- esac
324
- printf '%s' "$pull" | jq -e '.reviewed' >/dev/null 2>&1 \
325
- || fault "у заявки${pull_name} нет разбора: ревьювер не запрошен и отзыва никто не оставлял. Снятый черновик читается как «можно вливать», а вливать некому — назначь ревьювера и повтори."
326
-
327
- # Конфликт приезжает в отданную заявку чужим слиянием, без единого действия её
328
- # автора: основание, проверенное на открытии, к моменту снятия черновика уже
329
- # вчерашнее. Снятый черновик читается как «можно вливать», а вливать нечего —
330
- # владелец открывает заявку и находит там конфликт. Молчание при неизвестной
331
- # сливаемости остаётся: хостинг считает её заново после каждой правки главной ветки,
332
- # и «ещё не посчитано» — не «конфликтует».
333
- printf '%s' "$pull" | jq -e '.conflicting' >/dev/null 2>&1 \
334
- && fault "заявка${pull_name} конфликтует с главной веткой. Влей её в свою ветку, разбери конфликт и повтори: снятый черновик читается как «можно вливать», а слить эту заявку нельзя."
335
-
336
- # Автор заявки. На открытии судить было нечем, кроме текста команды: личность вызова
337
- # приходит окружением. Здесь она уже названа хостингом, и это последний ход, где
338
- # промах ещё исправим — после снятия черновика заявку вливают, а переоткрыть влитую
339
- # нельзя. Дерево, не назвавшее машинной записи, автора не судит.
340
- if [ -n "$task_bot" ]; then
341
- pull_author="$(printf '%s' "$pull" | jq -r '.author // empty' 2>/dev/null)"
342
- [ -n "$pull_author" ] && [ "$pull_author" != "$task_bot" ] \
343
- && fault "заявку${pull_name} открыла запись «${pull_author}», а не машинная «${task_bot}». Автор заявки её ревьювером не бывает, и разбор ей назначить нечем. Автора не сменить — закрой заявку и открой заново${pull_token_hint:+, подставив токен: ${pull_token_hint} …}."
344
- fi
345
- fi
346
- fi
347
-
348
- # Папка задачи: тот же предмет, что на открытии и на слиянии, третьим рубежом. Условие
349
- # местное — оно читает ветку, а не хостинг, — и потому стоит вне сетевого яруса выше.
350
- command -v rt_delivery_ready_folder >/dev/null 2>&1 && rt_delivery_ready_folder
351
-
352
- deny_faults
353
- fi
284
+ # --- снятие черновика ----------------------------------------------------------------------
285
+ # Предмет живёт помощником рядом — `git-guard-delivery-draft.sh`, тем же приёмом, что и подпись.
286
+ # Нет помощника снятие черновика не судится, а работа идёт дальше.
287
+ # shellcheck disable=SC1090
288
+ [ -f "$rt_hooks_dir/git-guard-delivery-draft.sh" ] \
289
+ && . "$rt_hooks_dir/git-guard-delivery-draft.sh" 2>/dev/null
290
+ command -v rt_delivery_draft_ready >/dev/null 2>&1 && rt_delivery_draft_ready
354
291
 
355
292
  # --- слияние заявки ----------------------------------------------------------------------
356
293
  #
@@ -404,17 +341,13 @@ if [ -n "$title" ]; then
404
341
  fi
405
342
  fi
406
343
 
407
- # Главная ветка влита до открытия PR. PR от разошедшейся ветки показывает ревьюверу свою
408
- # правку вперемешку с чужой, а проверки на нём гоняются от устаревшего основания.
409
- #
410
- # Ярусов два, и порядок между ними такой же, как у проверки задачи. Первый читает локальную
411
- # вершину и работает без сети. Второй спрашивает удалённую ссылку — без него молчание гарда
412
- # значит лишь «твоя ссылка не старше твоей ветки», а читается как «главная ветка влита»: ровно
413
- # так открытый PR и оказался конфликтующим, и узнал об этом владелец.
344
+ # Главная ветка влита до открытия PR: иначе ревьювер видит чужую правку вперемешку со своей, а
345
+ # проверки идут от устаревшего основания.
414
346
  #
415
- # Сеть здесь допустима по той же причине, по которой её зовёт проверка очереди работ ниже:
416
- # отсутствие ответа пропускается молча, и проверка, падающая в самолёте, не отбивает работу.
417
- # Предел ожидания задаётся переменными самого git внешний `timeout` есть не на всякой машине.
347
+ # Ярусов два. Первый читает локальную вершину и работает без сети. Второй спрашивает удалённую
348
+ # ссылку: без него молчание значит лишь «локальная ссылка не старше ветки», а читается как
349
+ # «главная влита». Нет ответа сетипропуск молча; предел ожидания задаётся переменными git,
350
+ # внешний `timeout` есть не на всякой машине.
418
351
  if git rev-parse --verify --quiet "refs/remotes/origin/${main_branch}" >/dev/null 2>&1 \
419
352
  && ! git merge-base --is-ancestor "origin/${main_branch}" HEAD 2>/dev/null; then
420
353
  behind="$(git rev-list --count "HEAD..origin/${main_branch}" 2>/dev/null)"
@@ -438,27 +371,36 @@ if [ -n "$remote_main" ] && [ -n "$local_main" ] && [ "$remote_main" != "$local_
438
371
  fault "твоя ссылка origin/${main_branch} отстала от удалённой — ${local_main:0:8} против ${remote_main:0:8}.${age} Гард сравнивает ветку с тем, что лежит в дереве, поэтому молчание первого яруса значит «ссылка не старше ветки», а не «главная ветка влита». Влей и повтори: git fetch origin && git merge origin/${main_branch}."
439
372
  fi
440
373
 
441
- # Личность вызова. Клиент хостинга держит две записи сразу — залогиненную и ту, чей токен стоит
442
- # в окружении вызова, — и какая из них откроет заявку, из текста команды не видно ничем, кроме
443
- # явной подстановки. Читающие вызовы идут от залогиненной и работают, поэтому пишущий выглядит
444
- # таким же обычным; промах всплывает шагом позже, на назначении ревьювера: автор заявки её
445
- # ревьювером не бывает. Чинится это только переоткрытием — автора у заявки не сменить.
374
+ # Личность вызова. Клиент хостинга держит две записи — залогиненную и ту, чей токен стоит в
375
+ # окружении вызова; какая откроет заявку, из команды видно только по явной подстановке. Промах
376
+ # всплывает на назначении ревьювера: автор заявки её ревьювером не бывает, а автора не сменить.
446
377
  #
447
- # Судится текст команды, а не ответ хостинга: спросить, чьим токеном пойдёт вызов, до его
448
- # исполнения нельзя. Дерево, не назвавшее переменной токена, требования не получает.
378
+ # Проверяется текст команды. Дерево, не назвавшее переменной токена, требования не получает.
449
379
  if [ -n "$pull_token_var" ] \
450
380
  && ! printf '%s' "$cmd" | grep -qE "(^|[;&|(]|&&|\|\||[[:space:]])${pull_token_var}="; then
451
381
  fault "заявка открывается без токена машинной записи: в команде нет подстановки «${pull_token_var}». Открытая залогиненной записью, она выйдет от владельца — ревьювером его тогда не назначить, и чинится это только переоткрытием.${pull_token_hint:+ Подставь токен: ${pull_token_hint} …}"
452
382
  fi
453
383
 
454
- # Тело заявки несёт раздел об оставшемся шаге, и несёт его с минуты открытия. Требование
455
- # записано было и прежде прозой в паттерне закрытия работы, ниже того места, где описано само
456
- # открытие: читающий доходил до открытия, открывал и раздела ещё не прочитал. Так заявка и
457
- # уехала без него, а владелец влил её кнопкой, пока шёл прогон.
384
+ # Второй ярус той же личности: кто на самом деле придёт по этому токену. Подстановка говорит
385
+ # о намерении, а не о результате: она читала файл, которого на машине нет, пустую строку клиент
386
+ # принял за незаданный токен, и заявка вышла от владельца при верной с виду команде.
458
387
  #
459
- # Судится текст команды: тело приходит доводом либо файлом, и оба вида читаются здесь же. Файл
460
- # читается с диска к моменту разбора он уже написан. Ни того ни другого в команде нет
461
- # требования нет: заявка без тела вовсе судится строкой выше, а не этой.
388
+ # У хостинга спрашивает дерево, а не пакет: хостинг, клиент и путь к токену у каждого свои.
389
+ # Пустой ответ означает «спросить не удалось»: вызов пропускается, и об этом сообщается
390
+ # молчаливый пропуск неотличим от сошедшейся сверки.
391
+ if [ -n "$task_bot" ] && command -v rt_pull_token_login >/dev/null 2>&1; then
392
+ token_login="$(rt_pull_token_login 2>/dev/null)"
393
+ if [ -z "$token_login" ]; then
394
+ printf 'гард поставки: кто придёт по токену, спросить не удалось — сверка по ответу хостинга пропущена.\n' >&2
395
+ elif [ "$token_login" != "$task_bot" ]; then
396
+ fault "по токену вызова хостинг отвечает записью «${token_login}», а не машинной «${task_bot}»: подстановка в команде есть, но значение пустое или чужое — так заявка выходит от владельца. Проверь, что файл токена на месте и читается${pull_token_hint:+: ${pull_token_hint}}."
397
+ fi
398
+ fi
399
+
400
+ # Тело заявки несёт раздел об оставшемся шаге с минуты открытия: без него владелец вливает
401
+ # заявку кнопкой, пока идёт прогон. Тело приходит доводом либо файлом, оба читаются здесь; файл
402
+ # к моменту разбора уже написан. Нет ни того ни другого — требования нет: заявка без тела
403
+ # проверяется строкой выше.
462
404
  if [ -n "$pull_body_section" ]; then
463
405
  body=''
464
406
  if command -v perl >/dev/null 2>&1; then
@@ -482,13 +424,9 @@ fi
482
424
 
483
425
  check_task "$number" "заявка с ветки «${branch}»" да
484
426
 
485
- # Папка задачи разбирается до открытия заявки, а не после одобрения. Прежде здесь стояло
486
- # напоминание, а требование на слиянии: считалось, что папка нужна на диске всё время
487
- # разбора, потому что правку по замечаниям без замысла отбивает гард хода работы. Владелец же
488
- # вливает, как только видит зелёное, и закрывающему коммиту места не остаётся — трижды подряд
489
- # папка уехала в главную неразобранной, а гард слияния этого не видел вовсе: кнопку нажимает
490
- # человек на хостинге, и до него гард не достаёт. Разбор переехал сюда, а замысел после уборки
491
- # гард хода работы берёт из истории ветки, а не с диска.
427
+ # Папка задачи разбирается до открытия заявки, а не после одобрения: владелец вливает, как
428
+ # только видит зелёное, и закрывающему коммиту места не остаётся трижды подряд папка уехала в
429
+ # главную неразобранной. Замысел после уборки гард хода работы берёт из истории ветки.
492
430
  rt_delivery_open_folder
493
431
 
494
432
  # Всё несошедшееся названо здесь, разом: до этой строки собирались условия, каждое из которых
@@ -44,10 +44,45 @@ if [ "$tool" = "mcp__webstorm__execute_tool" ] && command -v perl >/dev/null 2>&
44
44
  ' 2>/dev/null)"
45
45
  [ -n "$inner" ] && cmd="$inner"
46
46
  fi
47
- case "$cmd" in
48
- *git\ commit*) ;;
49
- *) exit 0 ;;
50
- esac
47
+ # Глагол ищется в позиции команды, а не подстрокой в строке.
48
+ #
49
+ # Голый поиск «git commit» промахивается в обе стороны. Мимо него уходит вызов, у которого между
50
+ # `git` и глаголом стоит ключ — `git -c user.name=… commit`, `git -C <дерево> commit`, — и ровно
51
+ # так коммитят машинной учётной записью. В него же попадает строка, где эти два слова стоят
52
+ # рядом по другому поводу: `git log --grep 'git commit'`, разбор чужого вывода, текст сообщения.
53
+ # Отбой на чтении истории стоит дороже пропуска: гард, мешающий читать, выключают в первый день.
54
+ #
55
+ # Разбор простой: у каждого слова `git` в строке пропускаются ключи — сами по себе и вместе со
56
+ # значением, если ключ его берёт, — и первое слово без дефиса и есть глагол. Слов `git` в строке
57
+ # бывает несколько (`git add . && git commit`), поэтому печатаются глаголы всех, а судится
58
+ # список целиком.
59
+ if command -v awk >/dev/null 2>&1; then
60
+ verbs="$(printf '%s\n' "$cmd" | awk '
61
+ {
62
+ for (i = 1; i <= NF; i++) {
63
+ word = $i
64
+ sub(/^.*\//, "", word)
65
+ if (word != "git") continue
66
+ for (j = i + 1; j <= NF; j++) {
67
+ arg = $j
68
+ if (arg == "-c" || arg == "-C" || arg == "--git-dir" || arg == "--work-tree" \
69
+ || arg == "--namespace" || arg == "--exec-path") { j++; continue }
70
+ if (substr(arg, 1, 1) == "-") continue
71
+ print arg
72
+ break
73
+ }
74
+ }
75
+ }
76
+ ' 2>/dev/null)"
77
+ printf '%s\n' "$verbs" | grep -qx 'commit' || exit 0
78
+ else
79
+ # Разборщика нет — остаётся прежний признак: он врёт в обе стороны, но гард без него не
80
+ # судит вовсе.
81
+ case "$cmd" in
82
+ *git\ commit*) ;;
83
+ *) exit 0 ;;
84
+ esac
85
+ fi
51
86
 
52
87
  # Коммит выполнится в рабочем каталоге вызова, поэтому и ветку смотрим там же; корень проекта
53
88
  # — запасной вариант, и он важен для отдельного рабочего дерева, где ветка своя.
@@ -114,7 +114,30 @@ done
114
114
  # shellcheck disable=SC1090
115
115
  [ -f "$rt_hooks_dir/profile-check.sh" ] && . "$rt_hooks_dir/profile-check.sh"
116
116
  command -v rt_needs >/dev/null 2>&1 || rt_needs() { command -v "$1" >/dev/null 2>&1; }
117
- rt_needs rt_push_checks git-guard-push-tests || exit 0
117
+
118
+ # Строка наблюдения на каждый исход. Гард, пишущий только отбои, отвечает на один вопрос из
119
+ # трёх: сколько пушей он остановил. «Набор прогнан и зелёный» и «набора не нашлось» выглядят в
120
+ # записи одинаково — молчанием, — и гейт, не гонявший ни одной проверки за неделю, неотличим от
121
+ # гейта, у которого всё зелено. Пишется это той же записью наблюдений, что и отбои гардов, и
122
+ # гаснет тем же выключателем дерева.
123
+ rt_push_gate_note() {
124
+ local outcome="$1" sid
125
+ # shellcheck disable=SC1090
126
+ [ -f "$rt_hooks_dir/observe.sh" ] && . "$rt_hooks_dir/observe.sh" 2>/dev/null
127
+ command -v rt_note >/dev/null 2>&1 || return 0
128
+ sid="$(printf '%s' "$input" | jq -r '.session_id // empty' 2>/dev/null)"
129
+ if [ -n "$sid" ]; then
130
+ rt_note push-gate "res=$outcome" "sid=$sid"
131
+ else
132
+ rt_note push-gate "res=$outcome"
133
+ fi
134
+ return 0
135
+ }
136
+
137
+ if ! rt_needs rt_push_checks git-guard-push-tests; then
138
+ rt_push_gate_note no-checks
139
+ exit 0
140
+ fi
118
141
 
119
142
  main_branch="${RT_MAIN_BRANCH:-main}"
120
143
  base=''
@@ -131,8 +154,10 @@ rt_skip_code="${RT_SKIP_CODE:-7}"
131
154
  failed=""
132
155
  output=""
133
156
  skipped=""
157
+ ran=0
134
158
  while IFS= read -r check; do
135
159
  [ -z "$check" ] && continue
160
+ ran=$((ran + 1))
136
161
  out="$(eval "$check" 2>&1)"
137
162
  status=$?
138
163
  [ "$status" -eq 0 ] && continue
@@ -150,6 +175,13 @@ EOF
150
175
 
151
176
  # Пропущенное называется вслух и тогда, когда набор прошёл: молчание о нём и есть та самая
152
177
  # неотличимость, ради которой код заведён. Пуш при этом идёт — отказа здесь нет.
178
+ # Функция профиля есть, а печатать ей в этом дереве нечего: исход тот же, что и без неё, —
179
+ # набора не нашлось, и молчание о нём читалось бы как зелёный прогон.
180
+ if [ "$ran" -eq 0 ]; then
181
+ rt_push_gate_note no-checks
182
+ exit 0
183
+ fi
184
+
153
185
  if [ -z "$failed" ] && [ -n "$skipped" ]; then
154
186
  printf 'гейт пуша: набор прошёл, но эти проверки смотреть было не на что:\n%s\n' "$skipped" >&2
155
187
  fi
@@ -176,11 +208,25 @@ if [ ! -f "$gap_mark" ]; then
176
208
  : >"$gap_mark" 2>/dev/null || true
177
209
  fi
178
210
 
179
- [ -z "$failed" ] && exit 0
211
+ if [ -z "$failed" ]; then
212
+ rt_push_gate_note green
213
+ exit 0
214
+ fi
215
+
216
+ rt_push_gate_note red
180
217
 
181
218
  # Хвост вывода, а не весь: у прогонщика он длинный, а нужна причина отказа.
182
219
  tail_out="$(printf '%s' "$output" | tail -n 40 | tr -d '\000')"
183
- reason="BLOCKED: пуш без зелёного локального прогона. «${failed}» упала — почини и пушь снова, обходить гард нельзя. Пуш вход в конвейер, и красное отсюда проверяется уже на проде. Хвост вывода:
220
+ # Красное бывает двух родов, и гард их не различает: он проверяет только код возврата. Когда
221
+ # падает проверка кода, «почини и пушь снова» верно. Когда ошибается сама проверка, тот же текст
222
+ # велит чинить код, которого никто не трогал: разобранный однажды отказ целиком лежал в
223
+ # документах, не тронутых ни одним коммитом ветки. Закон о проверяемости говорит, что сломанная
224
+ # проверка работу не останавливает, и до этой строки такого варианта в отказе не было.
225
+ reason="BLOCKED: пуш без зелёного локального прогона. «${failed}» упала — почини и пушь снова, обходить гард нельзя. Пуш — вход в конвейер, и красное отсюда проверяется уже на проде.
226
+
227
+ Ходов отсюда три: починить названное и повторить вызов; починить саму проверку, если ошибается она, — разобрать отказы поимённо, показать разбор владельцу и поправить проверку; либо принести владельцу цену обхода и ждать его слова. Спорное в список известного не вносится: он хранит принятое, а не результаты сломанной проверки.
228
+
229
+ Хвост вывода:
184
230
 
185
231
  ${tail_out}"
186
232
 
@@ -58,17 +58,28 @@ rt_hook_field() {
58
58
  # прошли мимо своих гардов пуш, открытие заявки, снятие черновика и слияние: коммит с чужой
59
59
  # подписью уехал в главную ветку, а запрет на слияние агентом снимался подстановкой токена.
60
60
  #
61
- # Значение присваивания берётся без пробелов: `ИМЯ="a b"` в кавычках сюда не подпадает, и вызов
62
- # за ним останется несудимым. Пробел внутри значения перед вызовом встречается редко, а
63
- # образец, разбирающий кавычки, ошибается в другую сторону считает вызовом то, что им не
64
- # является. Признак обязан ошибаться в сторону лишнего срабатывания: гард, не узнавший вызов,
65
- # молчит и выглядит исправным, а сработавший на лишнем виден сразу и правится.
61
+ # Значение присваивания бывает трёх видов: без пробелов, подстановкой `$( )` и в кавычках.
62
+ # Раньше принимался только первый, а заявки открывают командой `GH_TOKEN=$(cat <файл>) gh pr
63
+ # create …` путь в подстановке содержит пробел, и гард поставки такую команду не проверял:
64
+ # ни номер ветки, ни тело заявки, ни разбор папки задачи. Молчание гарда неотличимо от
65
+ # разрешения.
66
+ #
67
+ # Подстановка берётся до первой закрывающей скобки, строка в кавычках — до закрывающей кавычки
68
+ # того же вида. Вложенные скобки и кавычки образец не разбирает: признак остаётся образцом, а не
69
+ # разборщиком оболочки. Ошибаться признак должен в сторону лишнего срабатывания: гард, не
70
+ # распознавший вызов, молчит и выглядит исправным; сработавший лишний раз виден сразу и правится.
66
71
  #
67
72
  # Само по себе присваивание вызовом не считается: за ним обязано стоять имя команды.
68
73
  #
74
+ # Каталог перед именем — та же команда. Клиент, у которого своё имя занято псевдонимом оболочки,
75
+ # зовут полным путём, и признак, знавший только голое имя, такой вызов не узнавал вовсе: гард
76
+ # выходил нулём, а молчание его неотличимо от разрешения. Так заявки и уехали открытыми не
77
+ # машинной записью. Часть пути не переходит через пробел, поэтому она остаётся внутри одного
78
+ # слова и лишнего к вызову не приклеивает.
79
+ #
69
80
  # Объявлено это одним местом, а не литералом в каждом гарде: разойдясь, копии чинятся по одной
70
81
  # и молчат о том, что остальные остались слепыми.
71
- RT_CMD_BOUND='(^|[;&|(]|&&|\|\|)[[:space:]]*([A-Za-z_][A-Za-z0-9_]*=[^[:space:]]*[[:space:]]+)*'
82
+ RT_CMD_BOUND='(^|[;&|(]|&&|\|\|)[[:space:]]*([A-Za-z_][A-Za-z0-9_]*=([^[:space:]]*|\$\([^)]*\)|"[^"]*"|'"'"'[^'"'"']*'"'"')[[:space:]]+)*([^[:space:]]*/)?'
72
83
 
73
84
  # Поля, которые спрашивают все гарды.
74
85
  rt_hook_tool() { rt_hook_field RT_HOOK_TOOL '.tool_name'; }
@@ -107,6 +107,17 @@ while IFS= read -r candidate; do
107
107
  resource="$(head -12 "$candidate" 2>/dev/null | sed -nE 's/.*rt-kit v[^ ]+ · ([^ ]+) · [0-9a-f]+.*/\1/p' | head -1)"
108
108
  [ -z "$resource" ] && continue
109
109
 
110
+ # Файл в конфликте пропускается наравне со снятым. Разрешение конфликта содержания копии не
111
+ # меняет — раскладка кладёт её заново, — а отбитый здесь исполнитель остаётся с наполовину
112
+ # слитой веткой и без законного хода: источник править нечего, надстройка конфликта не
113
+ # снимает. Признаков два, и хватает любого: запись индекса о неслитом файле и маркеры
114
+ # слияния в самом файле — второй нужен там, где слияние ведёт не git, а сторонний
115
+ # инструмент, оставляющий маркеры без записи в индексе.
116
+ if [ -n "$(git -C "$root" ls-files -u -- "$candidate" 2>/dev/null)" ] \
117
+ || grep -qE '^(<<<<<<< |>>>>>>> )' "$candidate" 2>/dev/null; then
118
+ continue
119
+ fi
120
+
110
121
  if [ -n "$sources" ] && [ -f "$root/$sources/$resource" ]; then
111
122
  deny "BLOCKED by rule-source-guard: «${candidate#"$root"/}» разложен пакетом, и правка на его месте теряется на следующей раскладке — а до тех пор раскладка отказывает по этому файлу целиком, и цену платит тот, кто в этот день правит соседний ресурс. Ресурс — «${resource}». Правь источник: ${sources}/${resource} — потом собери пакет и разложи." \
112
123
  "правка, верная только этому дереву, идёт в надстройку .claude/rt-kit/overrides/${resource} — она сливается по разделу «## » и раскладку переживает"