@rt-tools/agent-kit 0.22.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 (101) hide show
  1. package/README.md +8 -2
  2. package/assets/checks/board-long-work.github.mjs +101 -0
  3. package/assets/checks/board-runs.github.mjs +34 -0
  4. package/assets/checks/board.github.mjs +1 -1
  5. package/assets/checks/check-board.github.mjs +30 -2
  6. package/assets/checks/check-doc-paths.mjs +24 -5
  7. package/assets/checks/check-file-size.mjs +8 -2
  8. package/assets/checks/check-prose-style.mjs +10 -1
  9. package/assets/checks/check-reuse.mjs +4 -1
  10. package/assets/checks/check-schema-drift.mjs +65 -6
  11. package/assets/checks/rt-kit-checks.config.mjs +13 -0
  12. package/assets/checks/signals.mjs +41 -1
  13. package/assets/commands/next-session.md +16 -5
  14. package/assets/defaults/gate-map.sh +13 -0
  15. package/assets/defaults/project.sh +26 -0
  16. package/assets/defaults/shell.sh +18 -3
  17. package/assets/hooks/browser-guard-device-id.sh +42 -12
  18. package/assets/hooks/browser-guard-no-asking.sh +5 -1
  19. package/assets/hooks/dispatch.sh +40 -9
  20. package/assets/hooks/docs-guard.sh +10 -0
  21. package/assets/hooks/exam-guard.sh +66 -16
  22. package/assets/hooks/git-guard-delivery-draft.sh +78 -0
  23. package/assets/hooks/git-guard-delivery.sh +64 -122
  24. package/assets/hooks/git-guard-main.sh +39 -4
  25. package/assets/hooks/git-guard-push-tests.sh +71 -3
  26. package/assets/hooks/glossary-load.sh +23 -2
  27. package/assets/hooks/grill-gate.sh +62 -0
  28. package/assets/hooks/hook-input.sh +17 -6
  29. package/assets/hooks/rule-source-guard.sh +11 -0
  30. package/assets/hooks/stand-login-guard.sh +101 -0
  31. package/assets/hooks/write-targets.sh +37 -4
  32. package/assets/laws/autonomous-work.md +30 -0
  33. package/assets/laws/project-documentation.md +8 -0
  34. package/assets/laws/verifiability.md +12 -2
  35. package/assets/laws/work-conduct.md +59 -65
  36. package/assets/patterns/autonomous-work-run.md +105 -0
  37. package/assets/patterns/browser-verification-measure.md +41 -1
  38. package/assets/patterns/browser-verification-stand.md +57 -16
  39. package/assets/patterns/doc-style-human.md +75 -0
  40. package/assets/patterns/doc-style-write.md +16 -0
  41. package/assets/patterns/git-workflow-commit.azure.md +12 -0
  42. package/assets/patterns/git-workflow-commit.github.md +16 -3
  43. package/assets/patterns/git-workflow-commit.gitlab.md +12 -0
  44. package/assets/patterns/git-workflow-merge.md +8 -0
  45. package/assets/patterns/git-workflow-pr-ready.md +93 -0
  46. package/assets/patterns/git-workflow-pr.azure.md +1 -1
  47. package/assets/patterns/git-workflow-pr.github.md +1 -1
  48. package/assets/patterns/git-workflow-pr.gitlab.md +1 -1
  49. package/assets/patterns/task-flow-start.md +48 -48
  50. package/assets/patterns/testing-e2e.md +18 -8
  51. package/assets/patterns/ts-procedure.md +3 -2
  52. package/assets/pitfalls/task-flow.md +40 -40
  53. package/assets/rules/autonomous-work.md +92 -0
  54. package/assets/rules/browser-verification.md +35 -3
  55. package/assets/rules/deploy-flow.azure.md +7 -0
  56. package/assets/rules/deploy-flow.github.md +7 -0
  57. package/assets/rules/deploy-flow.gitlab.md +7 -0
  58. package/assets/rules/doc-style.md +54 -0
  59. package/assets/rules/git-workflow.azure.md +8 -0
  60. package/assets/rules/git-workflow.github.md +59 -63
  61. package/assets/rules/git-workflow.gitlab.md +8 -0
  62. package/assets/rules/reuse-first.md +25 -5
  63. package/assets/rules/styling-bem.md +8 -1
  64. package/assets/rules/task-flow.md +108 -109
  65. package/assets/rules/testing.md +21 -0
  66. package/assets/skills/agent-kit.md +72 -82
  67. package/lib/commands.d.ts.map +1 -1
  68. package/lib/commands.js +69 -2
  69. package/lib/commands.js.map +1 -1
  70. package/lib/enroll.d.ts.map +1 -1
  71. package/lib/enroll.js +1 -1
  72. package/lib/enroll.js.map +1 -1
  73. package/lib/observations.d.ts +10 -1
  74. package/lib/observations.d.ts.map +1 -1
  75. package/lib/observations.js +1 -0
  76. package/lib/observations.js.map +1 -1
  77. package/lib/override-marks.d.ts +24 -0
  78. package/lib/override-marks.d.ts.map +1 -0
  79. package/lib/override-marks.js +98 -0
  80. package/lib/override-marks.js.map +1 -0
  81. package/lib/push-gate.d.ts +14 -0
  82. package/lib/push-gate.d.ts.map +1 -0
  83. package/lib/push-gate.js +93 -0
  84. package/lib/push-gate.js.map +1 -0
  85. package/lib/shipment.d.ts.map +1 -1
  86. package/lib/shipment.js +1 -1
  87. package/lib/shipment.js.map +1 -1
  88. package/package.json +1 -1
  89. package/rt-tools-agent-kit-0.24.0.tgz +0 -0
  90. package/assets/laws/application/access.md +0 -34
  91. package/assets/laws/application/locales.md +0 -33
  92. package/assets/laws/application/search-visibility.md +0 -24
  93. package/assets/patterns/permissions-procedure.md +0 -71
  94. package/assets/patterns/seo-page.md +0 -104
  95. package/assets/patterns/seo-verify.md +0 -83
  96. package/assets/patterns/translations-content.md +0 -107
  97. package/assets/patterns/translations-key.md +0 -64
  98. package/assets/rules/permissions.md +0 -116
  99. package/assets/rules/seo.md +0 -139
  100. package/assets/rules/translations.md +0 -96
  101. package/rt-tools-agent-kit-0.22.0.tgz +0 -0
@@ -25,6 +25,7 @@
25
25
  #
26
26
  # Что здесь чем зовётся, знает профиль дерева:
27
27
  # rt_task_branch_ok — форма имени ветки под задачу;
28
+ # rt_task_branch_number — номер задачи из этого имени: приставкой бывает и род правки;
28
29
  # RT_TASK_TITLE_RE — форма номера в заголовке заявки;
29
30
  # rt_task_state — состояние задачи одним объектом (existsize, open, onBoard, assigned,
30
31
  # numbered); молчание значит «спросить некого»;
@@ -95,6 +96,7 @@ done
95
96
  [ -f "$rt_hooks_dir/profile-check.sh" ] && . "$rt_hooks_dir/profile-check.sh"
96
97
  command -v rt_needs >/dev/null 2>&1 || rt_needs() { command -v "$1" >/dev/null 2>&1; }
97
98
  rt_needs rt_task_branch_ok git-guard-delivery || exit 0
99
+ rt_needs rt_task_branch_number git-guard-delivery || exit 0
98
100
 
99
101
  title_re="${RT_TASK_TITLE_RE:-^\[[A-Za-z]+-[0-9]+\][[:space:]]+[^[:space:]]}"
100
102
  task_new="${RT_TASK_NEW_CMD:-npm run task:new}"
@@ -120,15 +122,12 @@ tasks_dir="${RT_TASKS_DIR:-}"
120
122
  archive_dir="${RT_ARCHIVE_DIR:-}"
121
123
  main_branch="${RT_MAIN_BRANCH:-main}"
122
124
 
123
- # Обход требования: строка с причиной. Причина видна тому, кто вливает, поэтому обход законен.
124
- # Без причины это просто молчаливый пропуск, поэтому она обязательна. Порог в три знака — тот
125
- # же, что у гарда документа: если сделать по-разному, две формы одного обхода разойдутся.
125
+ # Обход требования: строка с причиной. Причину видит тот, кто вливает, поэтому обход разрешён;
126
+ # без причины это молчаливый пропуск. Порог в три знака — тот же, что у гарда документа.
126
127
  #
127
- # Строка обхода начинает строкусвою в теле PR или комментарий в конце команды — и
128
- # подстановки не принимает. Без этих двух условий текст, который ОБЪЯСНЯЕТ, что обход называется
129
- # так-то, от самого обхода неотличим: тело PR со строкой-примером снимало требование само
130
- # собой, и тот же пример гард печатает в своём отказе ниже. Нашлось это приёмкой такого же
131
- # признака — прогон обязан был покраснеть на настоящем нарушении и остался зелёным.
128
+ # Строка обхода стоит в начале строки своей в теле PR или комментария в конце команды — и
129
+ # подстановки не принимает. Иначе текст, который объясняет, как называется обход, неотличим от
130
+ # самого обхода: тело PR со строкой-примером снимало требование само.
132
131
  folder_skip_re='(^|#)[[:space:]]*Task-folder-skip:[[:space:]]*[^[:space:]<"'"'"'][^[:space:]"'"'"']{2,}'
133
132
 
134
133
  deny() {
@@ -146,10 +145,9 @@ deny() {
146
145
  exit 0
147
146
  }
148
147
 
149
- # Несошедшиеся условия копятся и называются разом. Отказ по первому промаху заставляет чинить
150
- # их по одному: исполнитель правит основание, повторяет вызов, упирается в заголовок, правит
151
- # заголовок, упирается в задачу и цена отказа растёт на каждом круге, хотя всё несошедшееся
152
- # было известно уже на первом. Готовность к поставке — одно состояние, и называется оно целиком.
148
+ # Несошедшиеся условия копятся и называются разом: отказ по первому промаху заставляет чинить их
149
+ # по одному правка основания, повтор, заголовок, снова повтор, задача, — хотя всё несошедшееся
150
+ # известно уже на первом круге. Готовность к поставке одно состояние, и называется оно целиком.
153
151
  faults=''
154
152
 
155
153
  fault() {
@@ -163,10 +161,9 @@ fault() {
163
161
  # shellcheck disable=SC1090
164
162
  [ -f "$rt_hooks_dir/git-guard-delivery-folder.sh" ] && . "$rt_hooks_dir/git-guard-delivery-folder.sh" 2>/dev/null
165
163
 
166
- # Конфликтующая своя заявка: тот же приём, что у папки и подписи, свой предмет живёт
167
- # помощником рядом. Зовётся он до всех ярусов ниже и судит не готовность этой работы, а право
168
- # брать следующую: пока отданное конфликтует, его чинят первым действием хода. Нет помощника —
169
- # ярус не судится, а работа идёт дальше.
164
+ # Конфликтующая своя заявка: тот же приём, что у папки и подписи. Зовётся помощник до всех ярусов
165
+ # ниже и судит не готовность этой работы, а право брать следующую: пока отданное конфликтует, его
166
+ # чинят первым действием хода. Нет помощника — ярус не судится, а работа идёт дальше.
170
167
  # shellcheck disable=SC1090
171
168
  [ -f "$rt_hooks_dir/git-guard-delivery-conflict.sh" ] && . "$rt_hooks_dir/git-guard-delivery-conflict.sh" 2>/dev/null
172
169
  command -v rt_delivery_conflict >/dev/null 2>&1 && rt_delivery_conflict
@@ -225,23 +222,22 @@ fi
225
222
  if [ -n "$branch_arg" ]; then
226
223
  # Имя, притворяющееся веткой под задачу, но не совпадающее с формой, — это промах в имени,
227
224
  # а не осознанная беззадачная ветка. Ловится до первого коммита.
228
- if printf '%s' "$branch_arg" | grep -qiE '^[A-Za-z]+-[0-9]+'; then
225
+ # Номер вынимает профиль: своя регулярка знала одну приставку — ключ задач, — и ветка вида
226
+ # `feat/88-slug`, законная по форме того же профиля, номера не давала вовсе. Проверка формы
227
+ # при заведении не срабатывала на ней ни разу, а сверка номеров пропускалась молча.
228
+ number_arg="$(rt_task_branch_number "$branch_arg")"
229
+ if [ -n "$number_arg" ]; then
229
230
  rt_task_branch_ok "$branch_arg" \
230
231
  || deny "BLOCKED: имя ветки «${branch_arg}» не той формы, что принята здесь. Номер у ветки тот же, что у задачи и у заголовка заявки на слияние."
231
- check_task "$(printf '%s' "$branch_arg" | sed -nE 's/^[A-Za-z]+-([0-9]+).*/\1/p')" "ветка «${branch_arg}»"
232
+ check_task "$number_arg" "ветка «${branch_arg}»"
232
233
 
233
- # Условия поставки, известные уже здесь, спрашиваются здесь. Прежде их спрашивали на
234
- # пуше и на открытии PR то есть после того, как работа сделана: основание чинится
235
- # мержем с разбором конфликта, а подпись коммитов переписыванием всей ветки. В
236
- # начале работы обе стоят одну команду.
234
+ # Условия поставки, известные уже здесь, проверяются здесь. После того как работа
235
+ # сделана, основание чинится слиянием с разбором конфликта, а подпись коммитов
236
+ # переписыванием ветки; в начале работы обе стоят одну команду.
237
237
  #
238
238
  # Основание: вершина главной ветки обязана лежать в том, от чего растёт новая ветка.
239
- # Ветка, заведённая от вчерашнего основания, узнаёт об этом на открытии PR — и узнаёт
240
- # так, что владелец видит её конфликтующей.
241
- #
242
- # Судится названное основание, а не вершина рабочей копии: команда, которой основание
243
- # как раз и берут свежим — `git checkout -b <ветка> origin/<главная>`, — иначе
244
- # отбивалась бы наравне с той, у которой основание вчерашнее.
239
+ # Проверяется названное основание, а не вершина рабочей копии: иначе команда, которой
240
+ # основание берут свежим `git checkout -b <ветка> origin/<главная>`, — запрещалась бы.
245
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)"
246
242
  base_ref="${base_arg:-HEAD}"
247
243
  if git rev-parse --verify --quiet "refs/remotes/origin/${main_branch}" >/dev/null 2>&1 \
@@ -285,68 +281,13 @@ fi
285
281
  && . "$rt_hooks_dir/git-guard-delivery-signature.sh" 2>/dev/null
286
282
  command -v rt_delivery_signature >/dev/null 2>&1 && rt_delivery_signature
287
283
 
288
- # --- снятие черновика --------------------------------------------------------------------
289
- #
290
- # Ревьювера до этой правки не спрашивал никто. Он стоял прозой в правиле, а хостинг запрос
291
- # разбора на самого себя принимает молча и не создаёт: разбор при этом выглядит запрошенным.
292
- # Раньше снятия черновика спросить негде — до открытия заявки ревьювера нет вовсе, — а само
293
- # снятие и есть тот ход, которым работа объявляется готовой.
294
- #
295
- # Ярус сетевой, и молчит он так же, как ярус состояния задачи: нет ответа — нет требования.
296
- #
297
- # Возврат заявки в черновик под требование не подпадает: он делает ровно то, чего гард и
298
- # добивается, — снимает с работы вид готовой.
299
- if printf '%s' "$cmd" | grep -qE "${RT_CMD_BOUND}(gh[[:space:]]+pr[[:space:]]+ready|glab[[:space:]]+mr[[:space:]]+update[^|;&]*--ready)([[:space:]]|\$)" \
300
- && ! printf '%s' "$cmd" | grep -q -- '--undo'; then
301
- # Ссылка на заявку необязательна: без неё клиент берёт заявку текущей ветки, и это самая
302
- # короткая форма вызова. Требовать номер значило бы снимать всё требование одним пробелом.
303
- # Поэтому берётся первый довод, каким бы он ни был — номер, адрес или имя ветки, — а его
304
- # отсутствие означает «спроси про текущую ветку».
305
- 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)"
306
- if rt_needs rt_pull_state git-guard-delivery; then
307
- pull="$(cd "$root" && rt_pull_state "$pull_ref" 2>/dev/null)" || pull=''
308
- if [ -n "$pull" ] && printf '%s' "$pull" | jq -e '.exists' >/dev/null 2>&1; then
309
- # Номер берётся из ответа, а если его там нет — из самой команды: заявка,
310
- # названная адресом или именем ветки, в отказе должна остаться узнаваемой.
311
- pull_name="$(printf '%s' "$pull" | jq -r '.number // empty' 2>/dev/null)"
312
- # Номер пишется с решёткой, а имя ветки или адрес — в кавычках: решётка перед
313
- # адресом читается как опечатка, а не как ссылка на заявку.
314
- [ -z "$pull_name" ] && pull_name="$pull_ref"
315
- case "$pull_name" in
316
- '') ;;
317
- *[!0-9]*) pull_name=" «${pull_name}»" ;;
318
- *) pull_name=" #${pull_name}" ;;
319
- esac
320
- printf '%s' "$pull" | jq -e '.reviewed' >/dev/null 2>&1 \
321
- || fault "у заявки${pull_name} нет разбора: ревьювер не запрошен и отзыва никто не оставлял. Снятый черновик читается как «можно вливать», а вливать некому — назначь ревьювера и повтори."
322
-
323
- # Конфликт приезжает в отданную заявку чужим слиянием, без единого действия её
324
- # автора: основание, проверенное на открытии, к моменту снятия черновика уже
325
- # вчерашнее. Снятый черновик читается как «можно вливать», а вливать нечего —
326
- # владелец открывает заявку и находит там конфликт. Молчание при неизвестной
327
- # сливаемости остаётся: хостинг считает её заново после каждой правки главной ветки,
328
- # и «ещё не посчитано» — не «конфликтует».
329
- printf '%s' "$pull" | jq -e '.conflicting' >/dev/null 2>&1 \
330
- && fault "заявка${pull_name} конфликтует с главной веткой. Влей её в свою ветку, разбери конфликт и повтори: снятый черновик читается как «можно вливать», а слить эту заявку нельзя."
331
-
332
- # Автор заявки. На открытии судить было нечем, кроме текста команды: личность вызова
333
- # приходит окружением. Здесь она уже названа хостингом, и это последний ход, где
334
- # промах ещё исправим — после снятия черновика заявку вливают, а переоткрыть влитую
335
- # нельзя. Дерево, не назвавшее машинной записи, автора не судит.
336
- if [ -n "$task_bot" ]; then
337
- pull_author="$(printf '%s' "$pull" | jq -r '.author // empty' 2>/dev/null)"
338
- [ -n "$pull_author" ] && [ "$pull_author" != "$task_bot" ] \
339
- && fault "заявку${pull_name} открыла запись «${pull_author}», а не машинная «${task_bot}». Автор заявки её ревьювером не бывает, и разбор ей назначить нечем. Автора не сменить — закрой заявку и открой заново${pull_token_hint:+, подставив токен: ${pull_token_hint} …}."
340
- fi
341
- fi
342
- fi
343
-
344
- # Папка задачи: тот же предмет, что на открытии и на слиянии, третьим рубежом. Условие
345
- # местное — оно читает ветку, а не хостинг, — и потому стоит вне сетевого яруса выше.
346
- command -v rt_delivery_ready_folder >/dev/null 2>&1 && rt_delivery_ready_folder
347
-
348
- deny_faults
349
- 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
350
291
 
351
292
  # --- слияние заявки ----------------------------------------------------------------------
352
293
  #
@@ -374,7 +315,7 @@ branch="$(git branch --show-current 2>/dev/null)"
374
315
  rt_task_branch_ok "$branch" \
375
316
  || deny "BLOCKED: заявка с ветки «${branch}», за которой не стоит задачи. Правка начинается с задачи, видимой в очереди работ: заведи её — ${task_new} — и перенеси работу в ветку с её номером."
376
317
 
377
- number="$(printf '%s' "$branch" | sed -nE 's/^[A-Za-z]+-([0-9]+).*/\1/p')"
318
+ number="$(rt_task_branch_number "$branch")"
378
319
 
379
320
  title=''
380
321
  if command -v perl >/dev/null 2>&1; then
@@ -400,17 +341,13 @@ if [ -n "$title" ]; then
400
341
  fi
401
342
  fi
402
343
 
403
- # Главная ветка влита до открытия PR. PR от разошедшейся ветки показывает ревьюверу свою
404
- # правку вперемешку с чужой, а проверки на нём гоняются от устаревшего основания.
405
- #
406
- # Ярусов два, и порядок между ними такой же, как у проверки задачи. Первый читает локальную
407
- # вершину и работает без сети. Второй спрашивает удалённую ссылку — без него молчание гарда
408
- # значит лишь «твоя ссылка не старше твоей ветки», а читается как «главная ветка влита»: ровно
409
- # так открытый PR и оказался конфликтующим, и узнал об этом владелец.
344
+ # Главная ветка влита до открытия PR: иначе ревьювер видит чужую правку вперемешку со своей, а
345
+ # проверки идут от устаревшего основания.
410
346
  #
411
- # Сеть здесь допустима по той же причине, по которой её зовёт проверка очереди работ ниже:
412
- # отсутствие ответа пропускается молча, и проверка, падающая в самолёте, не отбивает работу.
413
- # Предел ожидания задаётся переменными самого git внешний `timeout` есть не на всякой машине.
347
+ # Ярусов два. Первый читает локальную вершину и работает без сети. Второй спрашивает удалённую
348
+ # ссылку: без него молчание значит лишь «локальная ссылка не старше ветки», а читается как
349
+ # «главная влита». Нет ответа сетипропуск молча; предел ожидания задаётся переменными git,
350
+ # внешний `timeout` есть не на всякой машине.
414
351
  if git rev-parse --verify --quiet "refs/remotes/origin/${main_branch}" >/dev/null 2>&1 \
415
352
  && ! git merge-base --is-ancestor "origin/${main_branch}" HEAD 2>/dev/null; then
416
353
  behind="$(git rev-list --count "HEAD..origin/${main_branch}" 2>/dev/null)"
@@ -434,27 +371,36 @@ if [ -n "$remote_main" ] && [ -n "$local_main" ] && [ "$remote_main" != "$local_
434
371
  fault "твоя ссылка origin/${main_branch} отстала от удалённой — ${local_main:0:8} против ${remote_main:0:8}.${age} Гард сравнивает ветку с тем, что лежит в дереве, поэтому молчание первого яруса значит «ссылка не старше ветки», а не «главная ветка влита». Влей и повтори: git fetch origin && git merge origin/${main_branch}."
435
372
  fi
436
373
 
437
- # Личность вызова. Клиент хостинга держит две записи сразу — залогиненную и ту, чей токен стоит
438
- # в окружении вызова, — и какая из них откроет заявку, из текста команды не видно ничем, кроме
439
- # явной подстановки. Читающие вызовы идут от залогиненной и работают, поэтому пишущий выглядит
440
- # таким же обычным; промах всплывает шагом позже, на назначении ревьювера: автор заявки её
441
- # ревьювером не бывает. Чинится это только переоткрытием — автора у заявки не сменить.
374
+ # Личность вызова. Клиент хостинга держит две записи — залогиненную и ту, чей токен стоит в
375
+ # окружении вызова; какая откроет заявку, из команды видно только по явной подстановке. Промах
376
+ # всплывает на назначении ревьювера: автор заявки её ревьювером не бывает, а автора не сменить.
442
377
  #
443
- # Судится текст команды, а не ответ хостинга: спросить, чьим токеном пойдёт вызов, до его
444
- # исполнения нельзя. Дерево, не назвавшее переменной токена, требования не получает.
378
+ # Проверяется текст команды. Дерево, не назвавшее переменной токена, требования не получает.
445
379
  if [ -n "$pull_token_var" ] \
446
380
  && ! printf '%s' "$cmd" | grep -qE "(^|[;&|(]|&&|\|\||[[:space:]])${pull_token_var}="; then
447
381
  fault "заявка открывается без токена машинной записи: в команде нет подстановки «${pull_token_var}». Открытая залогиненной записью, она выйдет от владельца — ревьювером его тогда не назначить, и чинится это только переоткрытием.${pull_token_hint:+ Подставь токен: ${pull_token_hint} …}"
448
382
  fi
449
383
 
450
- # Тело заявки несёт раздел об оставшемся шаге, и несёт его с минуты открытия. Требование
451
- # записано было и прежде прозой в паттерне закрытия работы, ниже того места, где описано само
452
- # открытие: читающий доходил до открытия, открывал и раздела ещё не прочитал. Так заявка и
453
- # уехала без него, а владелец влил её кнопкой, пока шёл прогон.
384
+ # Второй ярус той же личности: кто на самом деле придёт по этому токену. Подстановка говорит
385
+ # о намерении, а не о результате: она читала файл, которого на машине нет, пустую строку клиент
386
+ # принял за незаданный токен, и заявка вышла от владельца при верной с виду команде.
454
387
  #
455
- # Судится текст команды: тело приходит доводом либо файлом, и оба вида читаются здесь же. Файл
456
- # читается с диска к моменту разбора он уже написан. Ни того ни другого в команде нет
457
- # требования нет: заявка без тела вовсе судится строкой выше, а не этой.
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
+ # проверяется строкой выше.
458
404
  if [ -n "$pull_body_section" ]; then
459
405
  body=''
460
406
  if command -v perl >/dev/null 2>&1; then
@@ -478,13 +424,9 @@ fi
478
424
 
479
425
  check_task "$number" "заявка с ветки «${branch}»" да
480
426
 
481
- # Папка задачи разбирается до открытия заявки, а не после одобрения. Прежде здесь стояло
482
- # напоминание, а требование на слиянии: считалось, что папка нужна на диске всё время
483
- # разбора, потому что правку по замечаниям без замысла отбивает гард хода работы. Владелец же
484
- # вливает, как только видит зелёное, и закрывающему коммиту места не остаётся — трижды подряд
485
- # папка уехала в главную неразобранной, а гард слияния этого не видел вовсе: кнопку нажимает
486
- # человек на хостинге, и до него гард не достаёт. Разбор переехал сюда, а замысел после уборки
487
- # гард хода работы берёт из истории ветки, а не с диска.
427
+ # Папка задачи разбирается до открытия заявки, а не после одобрения: владелец вливает, как
428
+ # только видит зелёное, и закрывающему коммиту места не остаётся трижды подряд папка уехала в
429
+ # главную неразобранной. Замысел после уборки гард хода работы берёт из истории ветки.
488
430
  rt_delivery_open_folder
489
431
 
490
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,15 +175,58 @@ 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
156
188
 
157
- [ -z "$failed" ] && exit 0
189
+ # Чем набор гейта уже набора конвейера — сказанное один раз за сессию.
190
+ #
191
+ # Правило поставки требует, чтобы гейт не был уже конвейера, а собрать это требование нечем:
192
+ # файл конвейера у каждого дерева свой, и вывести из него шаги набора пакет не может. Молчание
193
+ # при этом читается как «проверено всё»: расхождение узнаётся из красного конвейера после
194
+ # заявки, когда правка уже отдана человеку.
195
+ #
196
+ # Отметка живёт в каталоге временных файлов, как у слова о нехватке функции профиля: на каждый
197
+ # пуш та же строка повторялась бы за заход десятки раз и перестала бы читаться.
198
+ gap_key="$(printf '%s' "$input" | jq -r '.session_id // empty' 2>/dev/null)"
199
+ [ -z "$gap_key" ] && gap_key="$(date +%Y%m%d 2>/dev/null || printf 'nosession')"
200
+ gap_mark="${TMPDIR:-/tmp}/rt-kit-push-gate-gap-$gap_key"
201
+ if [ ! -f "$gap_mark" ]; then
202
+ printf 'гейт пуша: набор гейта — не набор конвейера. Сборка образов, снимки показа и проверка
203
+ ' >&2
204
+ printf 'собранных пакетов в него не входят: что в нём стоит, показывает разбор состояния
205
+ ' >&2
206
+ printf '(agent-kit doctor, раздел «набор перед пушем»), — прогони недостающее до заявки.
207
+ ' >&2
208
+ : >"$gap_mark" 2>/dev/null || true
209
+ fi
210
+
211
+ if [ -z "$failed" ]; then
212
+ rt_push_gate_note green
213
+ exit 0
214
+ fi
215
+
216
+ rt_push_gate_note red
158
217
 
159
218
  # Хвост вывода, а не весь: у прогонщика он длинный, а нужна причина отказа.
160
219
  tail_out="$(printf '%s' "$output" | tail -n 40 | tr -d '\000')"
161
- reason="BLOCKED: пуш без зелёного локального прогона. «${failed}» упала — почини и пушь снова, обходить гард нельзя. Пуш вход в конвейер, и красное отсюда проверяется уже на проде. Хвост вывода:
220
+ # Красное бывает двух родов, и гард их не различает: он проверяет только код возврата. Когда
221
+ # падает проверка кода, «почини и пушь снова» верно. Когда ошибается сама проверка, тот же текст
222
+ # велит чинить код, которого никто не трогал: разобранный однажды отказ целиком лежал в
223
+ # документах, не тронутых ни одним коммитом ветки. Закон о проверяемости говорит, что сломанная
224
+ # проверка работу не останавливает, и до этой строки такого варианта в отказе не было.
225
+ reason="BLOCKED: пуш без зелёного локального прогона. «${failed}» упала — почини и пушь снова, обходить гард нельзя. Пуш — вход в конвейер, и красное отсюда проверяется уже на проде.
226
+
227
+ Ходов отсюда три: починить названное и повторить вызов; починить саму проверку, если ошибается она, — разобрать отказы поимённо, показать разбор владельцу и поправить проверку; либо принести владельцу цену обхода и ждать его слова. Спорное в список известного не вносится: он хранит принятое, а не результаты сломанной проверки.
228
+
229
+ Хвост вывода:
162
230
 
163
231
  ${tail_out}"
164
232
 
@@ -15,11 +15,32 @@ GLOSSARY="${CLAUDE_PROJECT_DIR:-.}/docs/GLOSSARY.md"
15
15
  [ -f "$GLOSSARY" ] || exit 0
16
16
  command -v jq >/dev/null 2>&1 || exit 0
17
17
 
18
+ # Где слово заводят. Словарь бывает разложен пакетом — тогда правка на его месте теряется на
19
+ # следующей раскладке, а до тех пор раскладка отказывает по нему целиком. Вводная, которую читает
20
+ # каждая сессия, обязана вести в надстройку: она — единственный текст о словаре, доезжающий до
21
+ # читателя гарантированно, и, ведя в собранный файл, она отменяет собой всё сказанное на других
22
+ # слоях. Опровержение стоит строкой ниже, в шапке раскладки, но читается служебной строкой сборки:
23
+ # начинается оно с версии и суммы, а не со слова.
24
+ #
25
+ # Адрес выводится из идентификатора ресурса в самой шапке, а не зашивается: каталог надстроек у
26
+ # каждого дерева свой, и зашитый путь врал бы в первом же, которое держит их иначе. Шапки нет —
27
+ # словарь принадлежит дереву целиком, и «заводится здесь же» верно как есть.
28
+ header="$(head -1 "$GLOSSARY" 2>/dev/null)"
29
+ resource="$(printf '%s' "$header" | sed -n 's/.*rt-kit v[^ ]* · \([^ ]*\) · .*/\1/p')"
30
+
31
+ if [ -n "$resource" ]; then
32
+ where="$(printf 'Словарь разложен пакетом и на месте не правится: новое слово идёт в надстройку\n.claude/rt-kit/overrides/%s — она сливается по разделу «## » и раскладку переживает.\n' "$resource")"
33
+ else
34
+ where='Словарь принадлежит дереву целиком: новое слово заводится в нём же той же правкой.
35
+ '
36
+ fi
37
+
18
38
  {
19
39
  printf 'СЛОВАРЬ ПРОЕКТА — `docs/GLOSSARY.md`, ниже целиком.\n\n'
20
40
  printf 'Слово отсюда употребляется в том значении, что здесь: и в файлах, и в ответах владельцу.\n'
21
41
  printf 'Раздел «Так не пишем» действует так же — левая колонка не пишется и не произносится нигде.\n'
22
- printf 'Слова, которого здесь нет, у читателя нет тоже: оно либо заводится здесь же той же правкой,\n'
23
- printf 'либо заменяется тем, что уже есть.\n\n'
42
+ printf 'Слова, которого здесь нет, у читателя нет тоже: оно вводится тем же изменением, которым\n'
43
+ printf 'впервые употреблено, либо заменяется тем, что уже есть.\n'
44
+ printf '%s\n' "$where"
24
45
  cat "$GLOSSARY"
25
46
  } | jq -Rs '{hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:.}}'
@@ -165,6 +165,68 @@ verdict="$(tail -n 400 "$transcript" 2>/dev/null | jq -s -r --arg re "$read_re"
165
165
  | if ($asked_prose or $asking_now) and ($read | not) then "ask" else "pass" end
166
166
  ' 2>/dev/null)"
167
167
 
168
+ # Второй признак того же гарда: на этот вопрос владелец уже отвечал.
169
+ #
170
+ # Первый признак судит, читались ли правила, и на разрешённой работе молчит. А промах бывает
171
+ # другой: владелец дал указание прямой репликой, исполнитель нашёл факт, который меняет цену
172
+ # указания, но не его смысл, — и вместо строки о цене задал меню, где два варианта из трёх
173
+ # предлагали отменить решение владельца. Работа встала до ответа, разрешённая минутой раньше.
174
+ #
175
+ # Судится пересечение слов: вопрос, который сейчас уходит, против последней реплики владельца —
176
+ # и только там, где в записи уже был вызов инструмента вопроса, то есть владелец на вопрос
177
+ # отвечал. Понимания текста здесь нет и не нужно: три общих значимых слова означают тот же
178
+ # предмет, а разбор из шести вопросов идёт по разным предметам и порога не набирает.
179
+ #
180
+ # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: нет вопроса в вызове, нет реплики владельца, нет прошлого вопроса —
181
+ # признак молчит.
182
+ if [ -n "$tool" ]; then
183
+ asked_json="$(printf '%s' "$input" | jq -r '(.tool_input.questions // []) | tostring' 2>/dev/null)"
184
+ seen="$(tail -n 400 "$transcript" 2>/dev/null | jq -s -r --arg now "$asked_json" '
185
+ def is_input:
186
+ .type == "user"
187
+ and ((.isCompactSummary // false) | not)
188
+ and (((.message.content // []) | if type == "array"
189
+ then ([.[] | select(.type == "tool_result")] | length)
190
+ else 0 end) == 0);
191
+
192
+ def words: [splits("[^\\p{L}\\p{N}]+")] | map(select(length >= 5)) | unique;
193
+
194
+ (map(is_input) | rindex(true)) as $i
195
+ | if $i == null then "нет" else
196
+ (.[$i] | (.message.content // []) | if type == "array"
197
+ then ([.[] | select(.type == "text") | .text] | join(" "))
198
+ else (. // "") end) as $said
199
+ | ([.[:$i][] | select(.type == "assistant") | (.message.content // [])[]
200
+ | select(.type == "tool_use") | select(.name == "AskUserQuestion")] | length) as $before
201
+ | if $before == 0 or ($said | length) == 0 then "нет" else
202
+ (($now | words) - (($now | words) - ($said | words))) as $common
203
+ | if ($common | length) >= 3 then "было" else "нет" end
204
+ end
205
+ end
206
+ ' 2>/dev/null)"
207
+
208
+ if [ "$seen" = "было" ]; then
209
+ reason="BLOCKED by grill-gate: на этот вопрос владелец уже отвечал в этом разговоре — продолжай работу, а не переспрашивай.
210
+
211
+ Указание владельца действует до его отмены. Новый факт против действующего указания — это строка в ответе о цене, а не новый вопрос: переспрашивают только то, чего указание не покрывает. Промах здесь не в форме вопроса, а в остановке работы, которая уже разрешена.
212
+
213
+ Вопрос всё-таки о другом предмете — назови в нём то, чего в прежнем ответе владельца нет: признак судит общие слова вопроса и последней реплики владельца, а не смысл."
214
+
215
+ # Общий хвост отказа: два законных хода и законная форма обхода, если она у отказа есть.
216
+ # shellcheck disable=SC1090
217
+ [ -f "$rt_hooks_dir/deny-tail.sh" ] && . "$rt_hooks_dir/deny-tail.sh" 2>/dev/null
218
+ command -v rt_deny_tail >/dev/null 2>&1 || rt_deny_tail() { :; }
219
+ deny_tail_text="$(rt_deny_tail "")"
220
+ [ -n "$deny_tail_text" ] && reason="${reason}
221
+
222
+ ${deny_tail_text}"
223
+
224
+ jq -n --arg r "$reason" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$r}}' 2>/dev/null \
225
+ || printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"grill-gate: на этот вопрос уже отвечали."}}\n'
226
+ exit 0
227
+ fi
228
+ fi
229
+
168
230
  [ "$verdict" = "ask" ] || exit 0
169
231
 
170
232
  if [ -n "$tool" ]; then
@@ -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'; }