@rt-tools/agent-kit 0.8.1 → 0.8.3

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 (129) hide show
  1. package/README.md +25 -19
  2. package/assets/agents/qa-engineer.md +1 -1
  3. package/assets/agents/rules-reviewer.md +83 -0
  4. package/assets/checks/board.github.mjs +56 -17
  5. package/assets/checks/check-board.github.mjs +49 -5
  6. package/assets/checks/check-dupes.mjs +66 -6
  7. package/assets/checks/check-specs.mjs +100 -15
  8. package/assets/checks/rt-kit-checks.config.mjs +9 -0
  9. package/assets/checks/task-new.github.mjs +33 -5
  10. package/assets/commands/agent-kit-digest.md +10 -5
  11. package/assets/commands/feedback.md +95 -0
  12. package/assets/commands/next-session.md +4 -4
  13. package/assets/commands/rules-review.md +98 -0
  14. package/assets/commands/skill-curator.md +44 -25
  15. package/assets/defaults/gate-map.sh +11 -4
  16. package/assets/defaults/project.sh +46 -0
  17. package/assets/docs/GLOSSARY.md +49 -46
  18. package/assets/hooks/docs-guard.sh +19 -3
  19. package/assets/hooks/git-guard-delivery.sh +106 -13
  20. package/assets/hooks/proposal-guard.sh +93 -0
  21. package/assets/hooks/reuse-first-guard.sh +83 -15
  22. package/assets/hooks/skill-gate-layers.sh +1 -1
  23. package/assets/hooks/skill-gate.sh +27 -1
  24. package/assets/hooks/task-flow-guard.sh +59 -19
  25. package/assets/hooks/waiting-turn-guard.sh +87 -0
  26. package/assets/hooks/window-fill-guard.sh +1 -1
  27. package/assets/laws/delivery.md +41 -7
  28. package/assets/laws/project-documentation.md +39 -0
  29. package/assets/laws/verifiability.md +6 -1
  30. package/assets/laws/work-conduct.md +83 -3
  31. package/assets/patterns/git-workflow-commit.azure.md +84 -4
  32. package/assets/patterns/git-workflow-commit.github.md +90 -4
  33. package/assets/patterns/git-workflow-commit.gitlab.md +84 -5
  34. package/assets/patterns/git-workflow-docker.md +30 -0
  35. package/assets/patterns/git-workflow-merge.md +1 -1
  36. package/assets/patterns/spec-driven-domain.md +7 -1
  37. package/assets/patterns/spec-driven-rule.md +6 -0
  38. package/assets/patterns/task-flow-close.md +197 -21
  39. package/assets/patterns/task-flow-handoff.md +28 -5
  40. package/assets/patterns/task-flow-resume.md +25 -7
  41. package/assets/patterns/task-flow-start.md +37 -6
  42. package/assets/rules/angular-patterns.md +22 -0
  43. package/assets/rules/api-layer.md +25 -0
  44. package/assets/rules/browser-verification.md +42 -1
  45. package/assets/rules/component-structure.md +21 -0
  46. package/assets/rules/dependencies.md +22 -0
  47. package/assets/rules/doc-style.md +37 -5
  48. package/assets/rules/{entity-conventions.md → entity-conventions.needs-admin.md} +21 -0
  49. package/assets/rules/entity-models.md +21 -0
  50. package/assets/rules/git-workflow.azure.md +62 -8
  51. package/assets/rules/git-workflow.github.md +78 -14
  52. package/assets/rules/git-workflow.gitlab.md +61 -8
  53. package/assets/rules/lib-layers.md +25 -0
  54. package/assets/rules/lists.md +27 -0
  55. package/assets/rules/navigation.md +21 -0
  56. package/assets/rules/{observability.md → observability.needs-app.md} +23 -0
  57. package/assets/rules/permissions.md +23 -0
  58. package/assets/rules/platform-access.md +21 -0
  59. package/assets/rules/reuse-first.md +20 -0
  60. package/assets/rules/seo.md +19 -0
  61. package/assets/rules/shared-code.md +19 -0
  62. package/assets/rules/spec-driven.md +36 -0
  63. package/assets/rules/styling-bem.md +19 -0
  64. package/assets/rules/task-flow.md +150 -35
  65. package/assets/rules/testing.md +50 -0
  66. package/assets/rules/translations.md +21 -0
  67. package/assets/rules/typescript-conventions.md +33 -0
  68. package/assets/samples/specs/_template/spec.md +83 -0
  69. package/assets/samples/tasks/_template/grill.md +28 -0
  70. package/assets/samples/tasks/_template/plan.md +39 -0
  71. package/assets/samples/tasks/_template/progress.md +23 -0
  72. package/assets/skills/agent-kit-extend.md +24 -0
  73. package/assets/skills/agent-kit.md +69 -7
  74. package/assets/templates/rule.md +31 -2
  75. package/assets/traits.json +14 -0
  76. package/bin/agent-kit.d.ts.map +1 -1
  77. package/bin/agent-kit.js +31 -16
  78. package/bin/agent-kit.js.map +1 -1
  79. package/index.d.ts +1 -0
  80. package/index.d.ts.map +1 -1
  81. package/index.js +1 -0
  82. package/index.js.map +1 -1
  83. package/lib/argv.d.ts +17 -0
  84. package/lib/argv.d.ts.map +1 -0
  85. package/lib/argv.js +44 -0
  86. package/lib/argv.js.map +1 -0
  87. package/lib/cargo.d.ts +88 -0
  88. package/lib/cargo.d.ts.map +1 -0
  89. package/lib/cargo.js +16 -0
  90. package/lib/cargo.js.map +1 -0
  91. package/lib/catalog.d.ts +18 -1
  92. package/lib/catalog.d.ts.map +1 -1
  93. package/lib/catalog.js +12 -2
  94. package/lib/catalog.js.map +1 -1
  95. package/lib/commands.d.ts +0 -26
  96. package/lib/commands.d.ts.map +1 -1
  97. package/lib/commands.js +79 -122
  98. package/lib/commands.js.map +1 -1
  99. package/lib/companion.d.ts +37 -0
  100. package/lib/companion.d.ts.map +1 -1
  101. package/lib/companion.js +42 -1
  102. package/lib/companion.js.map +1 -1
  103. package/lib/config.d.ts +38 -1
  104. package/lib/config.d.ts.map +1 -1
  105. package/lib/config.js +24 -0
  106. package/lib/config.js.map +1 -1
  107. package/lib/ship.d.ts +39 -0
  108. package/lib/ship.d.ts.map +1 -0
  109. package/lib/ship.js +87 -0
  110. package/lib/ship.js.map +1 -0
  111. package/lib/shipment.d.ts +60 -0
  112. package/lib/shipment.d.ts.map +1 -0
  113. package/lib/shipment.js +247 -0
  114. package/lib/shipment.js.map +1 -0
  115. package/lib/snapshot.d.ts +30 -0
  116. package/lib/snapshot.d.ts.map +1 -0
  117. package/lib/snapshot.js +73 -0
  118. package/lib/snapshot.js.map +1 -0
  119. package/lib/traits.d.ts +32 -0
  120. package/lib/traits.d.ts.map +1 -0
  121. package/lib/traits.js +82 -0
  122. package/lib/traits.js.map +1 -0
  123. package/package.json +6 -2
  124. package/rt-tools-agent-kit-0.8.3.tgz +0 -0
  125. package/lib/submit.d.ts +0 -24
  126. package/lib/submit.d.ts.map +0 -1
  127. package/lib/submit.js +0 -26
  128. package/lib/submit.js.map +0 -1
  129. package/rt-tools-agent-kit-0.8.1.tgz +0 -0
@@ -1,19 +1,21 @@
1
1
  #!/usr/bin/env bash
2
2
  # rt-hook: PreToolUse Bash|mcp__webstorm__execute_terminal_command|mcp__webstorm__execute_tool
3
3
  # Требует: hooks/profile-check.sh
4
- # Гард поставки. PreToolUse на заведении ветки и открытии заявки на слияние.
4
+ # Гард поставки. PreToolUse на заведении ветки, пуше и открытии заявки на слияние.
5
5
  #
6
6
  # Закон о поставке требует трёх вещей, которых обычно не проверяет ничто: правка начинается с
7
- # задачи, видимой в очереди работ; задача, ветка и отчёт несут один номер; у задачи есть
7
+ # задачи, видимой в очереди работ; задача, ветка и PR несут один номер; у задачи есть
8
8
  # исполнитель. Держатся они памятью — и не удерживаются: задачи стоят вне очереди, исполнитель
9
9
  # не проставлен, а большинство влитых заявок приходит с веток, за которыми задачи не стояло
10
10
  # вовсе.
11
11
  #
12
- # Гард стоит в двух точках, и в каждой требует того, что в этот момент исправимо:
12
+ # Гард стоит в трёх точках, и в каждой требует того, что в этот момент исправимо:
13
13
  #
14
14
  # заведение ветки — имя с номером разбирается на месте; имя без номера пропускается:
15
15
  # локальная ветка под пробу законна, в главную она не поедет, потому что заявка с неё
16
16
  # не откроется;
17
+ # пуш — подпись машинного коммита ещё переписывается на месте; после пуша её чинит только
18
+ # силовая отправка;
17
19
  # открытие заявки — ветка обязана нести номер, заголовок обязан начинаться с того же номера,
18
20
  # а задача — быть открытой, стоять в очереди работ и иметь исполнителя.
19
21
  #
@@ -28,7 +30,9 @@
28
30
  # numbered); молчание значит «спросить некого»;
29
31
  # RT_TASK_NEW_CMD — чем заводится задача;
30
32
  # RT_BOARD_CHECK_CMD — чем сверяется очередь работ;
31
- # RT_TASK_BOT — учётная запись, которую ставят исполнителем.
33
+ # RT_TASK_BOT — учётная запись, которую ставят исполнителем;
34
+ # RT_COMMIT_EMAIL — почта, которой подписан машинный коммит; её же левой частью он и
35
+ # опознаётся.
32
36
  # Отказ называет и то, что не так, и чем это чинится: отказ без действия обходят, а не исполняют.
33
37
  #
34
38
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: не репозиторий, нет разборщика, битый ввод, нет профиля — пропуск.
@@ -87,6 +91,7 @@ title_re="${RT_TASK_TITLE_RE:-^\[[A-Za-z]+-[0-9]+\][[:space:]]+[^[:space:]]}"
87
91
  task_new="${RT_TASK_NEW_CMD:-npm run task:new}"
88
92
  board_check="${RT_BOARD_CHECK_CMD:-npm run check:board}"
89
93
  task_bot="${RT_TASK_BOT:-}"
94
+ commit_email="${RT_COMMIT_EMAIL:-}"
90
95
  tasks_dir="${RT_TASKS_DIR:-}"
91
96
  archive_dir="${RT_ARCHIVE_DIR:-}"
92
97
  main_branch="${RT_MAIN_BRANCH:-main}"
@@ -94,7 +99,13 @@ main_branch="${RT_MAIN_BRANCH:-main}"
94
99
  # Обход требования: строка с причиной. Причина видна тому, кто вливает, поэтому обход законен.
95
100
  # Без причины это просто молчаливый пропуск, поэтому она обязательна. Порог в три знака — тот
96
101
  # же, что у гарда документа: если сделать по-разному, две формы одного обхода разойдутся.
97
- folder_skip_re='Task-folder-skip:[[:space:]]*[^[:space:]"'"'"']{3,}'
102
+ #
103
+ # Строка обхода начинает строку — свою в теле PR или комментарий в конце команды — и
104
+ # подстановки не принимает. Без этих двух условий текст, который ОБЪЯСНЯЕТ, что обход называется
105
+ # так-то, от самого обхода неотличим: тело PR со строкой-примером снимало требование само
106
+ # собой, и тот же пример гард печатает в своём отказе ниже. Нашлось это приёмкой такого же
107
+ # признака — прогон обязан был покраснеть на настоящем нарушении и остался зелёным.
108
+ folder_skip_re='(^|#)[[:space:]]*Task-folder-skip:[[:space:]]*[^[:space:]<"'"'"'][^[:space:]"'"'"']{2,}'
98
109
 
99
110
  deny() {
100
111
  # Отказ гарда — наблюдение: гард, отбивающий чаще прочих, говорит, какое место поставки
@@ -109,7 +120,7 @@ deny() {
109
120
  exit 0
110
121
  }
111
122
 
112
- # Подсказка вместо отказа: на открытии отчёта папка ещё нужна. Решения подсказка не несёт,
123
+ # Подсказка вместо отказа: на открытии PR папка ещё нужна. Решения подсказка не несёт,
113
124
  # команда идёт дальше своим ходом.
114
125
  hint() {
115
126
  jq -n --arg c "$1" '{hookSpecificOutput:{hookEventName:"PreToolUse",additionalContext:$c}}' 2>/dev/null
@@ -165,6 +176,66 @@ if [ -n "$branch_arg" ]; then
165
176
  exit 0
166
177
  fi
167
178
 
179
+ # --- подпись машинного коммита ------------------------------------------------------------
180
+ #
181
+ # Служебный адрес хостинга состоит из числа, логина и домена, а сопоставляется по числу: логин
182
+ # рядом с ним не сверяет никто. Коммит с чужим числом хостинг припишет постороннему человеку, и
183
+ # изнутри это выглядит верным — имя учётной записи в истории то самое. Так одиннадцать коммитов
184
+ # и уехали в главную ветку за чужой подписью; нашёл это владелец, читая историю глазами.
185
+ #
186
+ # Точка — пуш: до него подпись переписывается на месте всей веткой сразу, после — только силовой
187
+ # отправкой. На коммите гард не стоит: почта задаётся переменными самой команды, и разбор её из
188
+ # текста ловил бы ту же строку, которая и так перед глазами у набравшего её.
189
+ #
190
+ # Судится коммит, НАЗВАВШИЙСЯ машинной записью: её логин стоит именем автора либо левой частью
191
+ # почты. Опознавать по самой почте нельзя — она в таком коммите как раз и неверна, а требовать
192
+ # машинную подпись от каждого коммита значило бы отбивать работу, сделанную человеком своими
193
+ # руками в том же дереве.
194
+ #
195
+ # Логин читается из объявленной почты, а не объявляется вторым свойством: два объявления одного
196
+ # логина разошлись бы молча. Исполнителем задачи он тоже не бывает — в дереве, где хостинг
197
+ # ограничил машинную запись, исполнителем ставят человека, а коммит остаётся машинным.
198
+ #
199
+ # Вызов пуша узнаётся по двум признакам сразу — команда `git` в начале строки или за
200
+ # разделителем и слово `push` отдельным словом. Одной подстрокой «git push» его не поймать:
201
+ # помощник учётных данных и заголовок запроса ставятся ключами `-c` между ними.
202
+ # Пробный пуш не отправляет ничего, и подпись у него не спрашивают.
203
+ if printf '%s' "$cmd" | grep -qE '(^|[;&|(]|&&|\|\|)[[:space:]]*git([[:space:]]|$)' \
204
+ && printf '%s' "$cmd" | grep -qE '(^|[[:space:]])push([[:space:]]|$)' \
205
+ && ! printf '%s' "$cmd" | grep -q -- '--dry-run'; then
206
+ # Два условия молчания. Дерево, не назвавшее почты машинной записи, требования не получает:
207
+ # своей машинной записи у пакета нет, а выдуманная отбивала бы работу в чужом дереве.
208
+ # Вершины главной ветки нет — вклад считать не от чего.
209
+ #
210
+ # Судится вклад ветки, а не вся история: влитое в главную этой веткой уже не чинится, и
211
+ # отказ за него отбивал бы работу вместо промаха.
212
+ if [ -n "$commit_email" ] \
213
+ && git rev-parse --verify --quiet "refs/remotes/origin/${main_branch}" >/dev/null 2>&1; then
214
+ # Левая часть служебного адреса: `<число>+<логин>` либо просто логин.
215
+ bot_login="${commit_email%%@*}"
216
+ bot_login="${bot_login##*+}"
217
+
218
+ strangers=''
219
+ while IFS="$(printf '\t')" read -r short author email; do
220
+ [ -z "$short" ] && continue
221
+ login="${email%%@*}"
222
+ login="${login##*+}"
223
+ [ "$author" = "$bot_login" ] || [ "$login" = "$bot_login" ] || continue
224
+ [ "$email" = "$commit_email" ] && continue
225
+ strangers="${strangers}${strangers:+, }${short} <${email}>"
226
+ done <<EOF
227
+ $(git log --format='%h%x09%an%x09%ae' "origin/${main_branch}..HEAD" 2>/dev/null)
228
+ EOF
229
+
230
+ [ -n "$strangers" ] \
231
+ && deny "BLOCKED: машинный коммит подписан не той почтой, что объявлена деревом. Хостинг сопоставляет служебный адрес по числу в нём, и коммит с чужим числом он припишет постороннему человеку — изнутри промах не виден, потому что имя учётной записи рядом верное. Расходятся: ${strangers}. Объявлено: ${commit_email} — почта берётся оттуда, а не набирается по памяти. Перепиши подпись до пуша, после него это чинит только силовая отправка:
232
+ последний коммит — GIT_AUTHOR_NAME=\"${bot_login}\" GIT_AUTHOR_EMAIL=\"${commit_email}\" GIT_COMMITTER_NAME=\"${bot_login}\" GIT_COMMITTER_EMAIL=\"${commit_email}\" git commit --amend --no-edit --reset-author
233
+ весь вклад ветки — git filter-branch -f --env-filter 'GIT_AUTHOR_EMAIL=\"${commit_email}\"; GIT_COMMITTER_EMAIL=\"${commit_email}\"' origin/${main_branch}..HEAD"
234
+ fi
235
+ fi
236
+ # Своего выхода у этой точки нет: пуш бывает и составной командой, а второе её звено судят
237
+ # разделы ниже.
238
+
168
239
  # --- слияние заявки ----------------------------------------------------------------------
169
240
  #
170
241
  # Папку задачи разбирают тем же PR, что и работу. После слияния этого уже никто не сделает:
@@ -243,24 +314,46 @@ fi
243
314
 
244
315
  if [ -n "$title" ]; then
245
316
  printf '%s' "$title" | grep -qE "$title_re" \
246
- || deny "BLOCKED: заголовок заявки не начинается с номера задачи. В списке заявок тела не видно, а строка связи живёт именно там — без номера в заголовке отчёт с задачей не сопоставить."
317
+ || deny "BLOCKED: заголовок заявки не начинается с номера задачи. В списке заявок тела не видно, а строка связи живёт именно там — без номера в заголовке PR с задачей не сопоставить."
247
318
  title_number="$(printf '%s' "$title" | sed -nE 's/^\[[A-Za-z]+-([0-9]+)\].*/\1/p')"
248
319
  if [ -n "$number" ] && [ -n "$title_number" ]; then
249
320
  [ "$title_number" = "$number" ] \
250
- || deny "BLOCKED: в заголовке заявки номер ${title_number}, у ветки — ${number}. Задача, ветка и отчёт несут один и тот же номер."
321
+ || deny "BLOCKED: в заголовке заявки номер ${title_number}, у ветки — ${number}. Задача, ветка и PR несут один и тот же номер."
251
322
  fi
252
323
  fi
253
324
 
254
- # Главная ветка влита до открытия отчёта. Отчёт от разошедшейся ветки показывает ревьюверу свою
325
+ # Главная ветка влита до открытия PR. PR от разошедшейся ветки показывает ревьюверу свою
255
326
  # правку вперемешку с чужой, а проверки на нём гоняются от устаревшего основания.
256
327
  #
257
- # Судится локальная вершина главной ветки, без сети: сетевой вызов в разборе команды падал бы
258
- # вместе со связью и отбивал бы работу вместо промаха. Отсюда и граница гард ловит ветку,
259
- # отставшую заведомо; свежесть самой вершины держит `git fetch`, и требует его чеклист.
328
+ # Ярусов два, и порядок между ними такой же, как у проверки задачи. Первый читает локальную
329
+ # вершину и работает без сети. Второй спрашивает удалённую ссылкубез него молчание гарда
330
+ # значит лишь «твоя ссылка не старше твоей ветки», а читается как «главная ветка влита»: ровно
331
+ # так открытый PR и оказался конфликтующим, и узнал об этом владелец.
332
+ #
333
+ # Сеть здесь допустима по той же причине, по которой её зовёт проверка очереди работ ниже:
334
+ # отсутствие ответа пропускается молча, и проверка, падающая в самолёте, не отбивает работу.
335
+ # Предел ожидания задаётся переменными самого git — внешний `timeout` есть не на всякой машине.
260
336
  if git rev-parse --verify --quiet "refs/remotes/origin/${main_branch}" >/dev/null 2>&1 \
261
337
  && ! git merge-base --is-ancestor "origin/${main_branch}" HEAD 2>/dev/null; then
262
338
  behind="$(git rev-list --count "HEAD..origin/${main_branch}" 2>/dev/null)"
263
- deny "BLOCKED: «${main_branch}» ушла вперёд на ${behind:-несколько} коммитов, а в ветку не влита. Отчёт от разошедшейся ветки показывает ревьюверу правку вперемешку с чужой, а проверки на нём идут от устаревшего основания. Влей и повтори: git fetch origin && git merge origin/${main_branch} — порядок и разбор конфликта в паттерне git-workflow-merge."
339
+ deny "BLOCKED: «${main_branch}» ушла вперёд на ${behind:-несколько} коммитов, а в ветку не влита. PR от разошедшейся ветки показывает ревьюверу правку вперемешку с чужой, а проверки на нём идут от устаревшего основания. Влей и повтори: git fetch origin && git merge origin/${main_branch} — порядок и разбор конфликта в паттерне git-workflow-merge."
340
+ fi
341
+
342
+ # Второй ярус: локальная ссылка сама могла протухнуть. Ответа нет — ярус молчит.
343
+ remote_main="$(GIT_HTTP_LOW_SPEED_LIMIT=1000 GIT_HTTP_LOW_SPEED_TIME=5 GIT_TERMINAL_PROMPT=0 \
344
+ git ls-remote origin "refs/heads/${main_branch}" 2>/dev/null | cut -f1)"
345
+ local_main="$(git rev-parse --verify --quiet "refs/remotes/origin/${main_branch}" 2>/dev/null)"
346
+ if [ -n "$remote_main" ] && [ -n "$local_main" ] && [ "$remote_main" != "$local_main" ]; then
347
+ # Возраст ссылки — то, чего исполнитель не видит вовсе, и именно он отличает «ветка
348
+ # отстала» от «я не знаю, отстала ли». Формат `stat` на BSD и на GNU разный, поэтому
349
+ # спрашиваются оба, а не угадывается система.
350
+ fetch_head="$(git rev-parse --git-dir 2>/dev/null)/FETCH_HEAD"
351
+ fetched_at="$(stat -f %m "$fetch_head" 2>/dev/null || stat -c %Y "$fetch_head" 2>/dev/null)"
352
+ age=''
353
+ if [ -n "$fetched_at" ]; then
354
+ age=" Последний git fetch — $(( ( $(date +%s) - fetched_at ) / 60 )) мин. назад."
355
+ fi
356
+ deny "BLOCKED: твоя ссылка origin/${main_branch} отстала от удалённой — ${local_main:0:8} против ${remote_main:0:8}.${age} Гард сравнивает ветку с тем, что лежит в дереве, поэтому молчание первого яруса значит «ссылка не старше ветки», а не «главная ветка влита». Влей и повтори: git fetch origin && git merge origin/${main_branch}."
264
357
  fi
265
358
 
266
359
  check_task "$number" "заявка с ветки «${branch}»"
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env bash
2
+ # rt-hook: Stop
3
+ # Гард предложения: ход, в котором владелец сказал завести или отправить предложение слою правил,
4
+ # не заканчивается, пока отправки не было. Stop.
5
+ #
6
+ # Зачем именно так. Написанное и не отправленное лежит в дереве неотличимо от отправленного:
7
+ # своей записи в слое правил у него нет, и владелец читает работу сделанной, пока не спросит
8
+ # прямо. Отправка при этом неудобна ровно в одном месте — она пишет отметки в файлы предложений и
9
+ # делает дерево грязным, — и этим доводом исполнитель закрывает молчание правил.
10
+ #
11
+ # Ловится просьба образцами, а не пониманием смысла: оценку «владелец просил отправить» назначал
12
+ # бы тот, кому она мешает. Глагол обязателен: «разбери пропозалы» — работа над уже приехавшими, и
13
+ # отправкой она не кончается. Слово «предложение» само по себе не считается тоже — оно ходит в
14
+ # каждом втором ходе о другом, и рядом с ним нужно слово о слое правил или о пакете.
15
+ #
16
+ # Отправкой считается вызов команды пакета без сухого прогона, сделанный в том же ходе: сухой
17
+ # прогон показывает, что уехало бы, ничего не заводит и следа наружу не оставляет.
18
+ #
19
+ # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: при любой ошибке, нехватке `jq`, отсутствии записи хода и повторном
20
+ # заходе ход РАЗРЕШАЕТСЯ (exit 0). Сломанный гард не имеет права заклинить разговор.
21
+
22
+ input="$(cat 2>/dev/null)"
23
+ [ -z "$input" ] && exit 0
24
+
25
+ command -v jq >/dev/null 2>&1 || exit 0
26
+
27
+ # Повторный заход по тому же ходу не судится: гард сказал своё один раз и отпускает.
28
+ active="$(printf '%s' "$input" | jq -r '.stop_hook_active // false' 2>/dev/null)"
29
+ [ "$active" = "true" ] && exit 0
30
+
31
+ transcript="$(printf '%s' "$input" | jq -r '.transcript_path // empty' 2>/dev/null)"
32
+ [ -z "$transcript" ] && exit 0
33
+ [ -f "$transcript" ] || exit 0
34
+
35
+ # Каталог предложений у дерева свой. Заданный пустым — отказ дерева от требования: дереву, не
36
+ # берущему переносимый слой правил, гард не навязывается.
37
+ proposals_dir="${RT_PROPOSALS_DIR-.claude/rt-kit/proposals}"
38
+ [ -z "$proposals_dir" ] && exit 0
39
+
40
+ # Просьба владельца. Набор открыт и пополняется правкой: полнота его — открытый вопрос
41
+ # договорённости, а не обещание.
42
+ asked_re='(завед|напиш|отправ|пошл|зашл|отошл|выгруз)[а-яё]*[^.!?]{0,40}(пропозал|предложени)|(пропозал|предложени)[а-яё]*[^.!?]{0,40}(завед|напиш|отправ|пошл|зашл|отошл|выгруз)'
43
+
44
+ # Сосед, без которого слово «предложение» не считается просьбой о слое правил.
45
+ context_re='пропозал|слою правил|слоя правил|слой правил|пакет|agent-kit|наверх'
46
+
47
+ # Ход — это всё, что записано после последнего настоящего ввода владельца. Ответ инструмента
48
+ # приходит той же ролью, поэтому строки с `tool_result` вводом не считаются.
49
+ #
50
+ # Хвост в 400 строк: запись хода растёт всю сессию, а судится только последний ход.
51
+ verdict="$(tail -n 400 "$transcript" 2>/dev/null | jq -s -r --arg asked "$asked_re" --arg ctx "$context_re" '
52
+ def is_input:
53
+ .type == "user"
54
+ and (((.message.content // []) | if type == "array"
55
+ then ([.[] | select(.type == "tool_result")] | length)
56
+ else 0 end) == 0);
57
+
58
+ def text_of:
59
+ (.message.content // []) | if type == "array"
60
+ then ([.[] | select(.type == "text") | .text] | join("\n"))
61
+ else (. // "") end;
62
+
63
+ (map(is_input) | rindex(true)) as $i
64
+ | (if $i == null then [] else .[$i:] end) as $turn
65
+ | (if $i == null then "" else ($turn[0] | text_of) end) as $said
66
+ | [$turn[] | select(.type == "assistant") | (.message.content // [])[] | select(.type == "tool_use")] as $uses
67
+ # Признак нечувствителен к регистру флагом, а не приведением: приведение знает только
68
+ # латиницу, и «Отправь Пропозал» с большой буквы проходило бы мимо набора образцов молча.
69
+ | (($said | test($asked; "i")) and ($said | test($ctx; "i"))) as $wanted
70
+ | ($uses | map(
71
+ ((.name // "") | test("^Bash$"))
72
+ and ((.input.command // "") | test("agent-kit[^|;&]*propose"))
73
+ and ((.input.command // "") | test("--dry-run") | not)
74
+ ) | any) as $sent
75
+ | if $wanted and ($sent | not) then "owe" else "pass" end
76
+ ' 2>/dev/null)"
77
+
78
+ [ "$verdict" = "owe" ] || exit 0
79
+
80
+ reason="BLOCKED by proposal-guard: владелец сказал завести или отправить предложение слою правил, а отправки в этом ходе не было. Написанное и не отправленное лежит в дереве неотличимо от отправленного: своей записи в слое правил у него нет, и владелец читает работу сделанной, пока не спросит прямо.
81
+
82
+ Предложение пишется файлом в \`$proposals_dir/\` и уезжает в тот же ход:
83
+
84
+ npx agent-kit propose
85
+
86
+ Сухой прогон отправкой не является: он показывает, что уехало бы, и следа наружу не оставляет. Отправка пишет отметки в файлы предложений и делает дерево грязным — при открытом PR они ложатся вторым коммитом в ту же ветку, и это их место, а не повод отложить.
87
+
88
+ Гард судит один ход: следующий заход не отбивается."
89
+
90
+ jq -n --arg r "$reason" '{decision:"block",reason:$r}' 2>/dev/null \
91
+ || printf '{"decision":"block","reason":"proposal-guard: владелец просил предложение — отправь его командой пакета."}\n'
92
+
93
+ exit 0
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # rt-hook: PreToolUse Edit|Write|MultiEdit|mcp__webstorm__create_new_file
2
+ # rt-hook: PreToolUse Edit|Write|MultiEdit|Bash|mcp__webstorm__create_new_file|mcp__webstorm__execute_terminal_command|mcp__webstorm__execute_tool
3
3
  # Требует: hooks/profile-check.sh
4
4
  # Гард «ничего не пишется с нуля». PreToolUse на правке кода и разметки.
5
5
  #
@@ -38,14 +38,60 @@ command -v jq >/dev/null 2>&1 || exit 0
38
38
  command -v perl >/dev/null 2>&1 || exit 0
39
39
 
40
40
  tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
41
+ shell_cmd=""
41
42
  case "$tool" in
42
43
  # Инструмент среды заводит файл теми же двумя данными, только называет их иначе — без этой
43
44
  # ветки файл заводился мимо всех проверок.
44
45
  Edit | Write | MultiEdit | mcp__webstorm__create_new_file) ;;
46
+ # Команда оболочки, которая пишет файл, — та же правка. Без этой ветки гард обходится
47
+ # сменой не инструмента, а способа записи; текстом правки тогда служит сама команда, и
48
+ # заведённое ею в heredoc читается наравне с телом правки. Разбор —
49
+ # `2026-08-15-guard-denied-shell-wrote-anyway.md`.
50
+ #
51
+ # Терминал среды исполняет ту же командную строку и кладёт её в то же поле: без этих двух
52
+ # имён гард стоял бы объявленным на них и молча пропускал — состояние хуже необъявленного,
53
+ # потому что снаружи выглядит закрытым.
54
+ Bash | mcp__webstorm__execute_terminal_command | mcp__webstorm__execute_tool)
55
+ shell_cmd="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null)"
56
+ [ -z "$shell_cmd" ] && exit 0
57
+ # Универсальный исполнитель прячет настоящую команду во вложенной строке: без её разбора
58
+ # путь стоит за кавычкой, и до него не дотягивается ни один образец.
59
+ if [ "$tool" = "mcp__webstorm__execute_tool" ] && command -v perl >/dev/null 2>&1; then
60
+ inner="$(printf '%s' "$shell_cmd" | perl -0ne '
61
+ if (/--command(?:=|\s+)(?:"((?:[^"\\]|\\.)*)"|\x27([^\x27]*)\x27|(.+))/s) {
62
+ print defined $1 ? $1 : (defined $2 ? $2 : $3);
63
+ }
64
+ ' 2>/dev/null)"
65
+ [ -n "$inner" ] && shell_cmd="$inner"
66
+ fi
67
+ ;;
45
68
  *) exit 0 ;;
46
69
  esac
47
70
 
48
- path="$(printf '%s' "$input" | jq -r '.tool_input.file_path // .tool_input.pathInProject // empty' 2>/dev/null)"
71
+ # Профиль дерева: сперва умолчание пакета, поверх него надстройка проекта, если она есть.
72
+ # Читается до разбора пути: пути из команды оболочки вынимает как раз профиль.
73
+ rt_hooks_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
74
+ for profile in "$rt_hooks_dir/../rt-kit/defaults/project.sh" "$rt_hooks_dir/../defaults/project.sh" "${CLAUDE_PROJECT_DIR:-.}/.claude/rt-kit/defaults/project.sh" "${CLAUDE_PROJECT_DIR:-.}/.claude/rt-kit/project.sh"; do
75
+ # shellcheck disable=SC1090
76
+ [ -f "$profile" ] && . "$profile" 2>/dev/null
77
+ done
78
+
79
+ if [ -n "$shell_cmd" ]; then
80
+ command -v rt_shell_writes >/dev/null 2>&1 && command -v rt_shell_paths >/dev/null 2>&1 || exit 0
81
+ rt_shell_writes "$shell_cmd" || exit 0
82
+ # Из команды берётся первый путь, чьё расширение гарду интересно: остальные ему безразличны.
83
+ path=""
84
+ while IFS= read -r candidate; do
85
+ case "$candidate" in
86
+ *.html | *.scss | *.ts) path="$candidate"; break ;;
87
+ esac
88
+ done <<EOF
89
+ $(rt_shell_paths "$shell_cmd")
90
+ EOF
91
+ [ -z "$path" ] && exit 0
92
+ else
93
+ path="$(printf '%s' "$input" | jq -r '.tool_input.file_path // .tool_input.pathInProject // empty' 2>/dev/null)"
94
+ fi
49
95
  # Путь от корня дерева приводится к абсолютному один раз, чтобы образцы не двоились.
50
96
  case "$path" in
51
97
  /*) ;;
@@ -56,13 +102,6 @@ case "$path" in
56
102
  *) exit 0 ;;
57
103
  esac
58
104
 
59
- # Профиль дерева: сперва умолчание пакета, поверх него — надстройка проекта, если она есть.
60
- rt_hooks_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
61
- for profile in "$rt_hooks_dir/../rt-kit/defaults/project.sh" "$rt_hooks_dir/../defaults/project.sh" "${CLAUDE_PROJECT_DIR:-.}/.claude/rt-kit/defaults/project.sh" "${CLAUDE_PROJECT_DIR:-.}/.claude/rt-kit/project.sh"; do
62
- # shellcheck disable=SC1090
63
- [ -f "$profile" ] && . "$profile" 2>/dev/null
64
- done
65
-
66
105
  # Слово о нехватке функции профиля: хук, вышедший молча, неотличим от работающего. Файл может
67
106
  # быть не разложен — тогда остаётся прежнее поведение, молчаливое.
68
107
  # shellcheck disable=SC1090
@@ -82,11 +121,16 @@ if [ -n "${RT_REUSE_SKIP_RE:-}" ] && printf '%s' "$path" | grep -qE "$RT_REUSE_S
82
121
  exit 0
83
122
  fi
84
123
 
85
- # Только новый текст: строка, уже лежавшая в файле, этой правкой не заводилась.
86
- added="$(printf '%s' "$input" | jq -r '
87
- [ .tool_input.content?, .tool_input.text?, .tool_input.new_string?, (.tool_input.edits[]?.new_string) ]
88
- | map(select(. != null)) | join("\n")
89
- ' 2>/dev/null)"
124
+ # Только новый текст: строка, уже лежавшая в файле, этой правкой не заводилась. У команды
125
+ # оболочки таким текстом служит она сама: что она кладёт в файл, лежит в ней же.
126
+ if [ -n "$shell_cmd" ]; then
127
+ added="$shell_cmd"
128
+ else
129
+ added="$(printf '%s' "$input" | jq -r '
130
+ [ .tool_input.content?, .tool_input.text?, .tool_input.new_string?, (.tool_input.edits[]?.new_string) ]
131
+ | map(select(. != null)) | join("\n")
132
+ ' 2>/dev/null)"
133
+ fi
90
134
  [ -z "$added" ] && exit 0
91
135
 
92
136
  # Явный отказ от правила: готового такого нет, автор это осознал и пометил. Считается по
@@ -138,11 +182,19 @@ has_re() {
138
182
  rt_checks_json="${CLAUDE_PROJECT_DIR:-.}/.claude/rt-kit/checks.json"
139
183
  rt_bundles=''
140
184
  rt_own_signals=''
185
+ rt_backend_roots=''
141
186
  if [ -f "$rt_checks_json" ]; then
142
187
  rt_bundles="$(jq -r '.reuse.bundles[]? // empty' "$rt_checks_json" 2>/dev/null | tr '\n' ' ')"
143
188
  own="$(jq -r '.reuse.signals // empty' "$rt_checks_json" 2>/dev/null)"
144
189
  [ -n "$own" ] && rt_own_signals="${CLAUDE_PROJECT_DIR:-.}/$own"
190
+ # Тем же ключом, каким их читает сплошная проверка: второе объявление тех же корней
191
+ # разошлось бы с первым молча.
192
+ rt_backend_roots="$(jq -r '.backendRoots[]? // empty' "$rt_checks_json" 2>/dev/null | tr '\n' ' ')"
145
193
  fi
194
+
195
+ # Путь от корня дерева: образец в признаке пишет дерево, и писать его от корня машины оно не
196
+ # может. Абсолютный путь нужен только для чтения файла с диска.
197
+ rel_path="${path#"${CLAUDE_PROJECT_DIR:-.}"/}"
146
198
  rt_signals_dir="${CLAUDE_PROJECT_DIR:-.}/${RT_REUSE_SIGNALS_DIR:-tools/signals}"
147
199
 
148
200
  # Признаки объявленных наборов: те же файлы читает сплошная проверка. Ключ признака совпал с
@@ -172,9 +224,25 @@ while IFS= read -r signal; do
172
224
  '') ;;
173
225
  *) case "$path" in *"$ext") ;; *) continue ;; esac ;;
174
226
  esac
227
+ # Пропуск корней бэкенда: признак, объявленный с ним, на бэкенде не действует вовсе — там
228
+ # принят другой способ, и базового класса, которого признак требует, у бэкенда нет. Поле
229
+ # читает и сплошная проверка; читать его одному из двоих значит отбивать гардом ту самую
230
+ # правку, которую проверка пропускает, — а провести её больше нечем: маркер отступления
231
+ # объявляет обход готового, а обхода тут не было.
232
+ if [ "$(field "$signal" '.skipBackendRoots')" = 'true' ] && [ -n "$rt_backend_roots" ]; then
233
+ skip_backend=''
234
+ for backend_root in $rt_backend_roots; do
235
+ case "$rel_path" in "$backend_root"*) skip_backend=1 ;; esac
236
+ done
237
+ [ -n "$skip_backend" ] && continue
238
+ fi
239
+
240
+ # Образец имени сверяется с путём от корня дерева, а не с именем файла: слои, которые
241
+ # признак и разделяет, зовут свои файлы одинаково, и по имени они неразличимы. Сплошная
242
+ # проверка сверяет с путём — расходиться им нельзя.
175
243
  only_named="$(field "$signal" '.onlyNamed')"
176
244
  if [ -n "$only_named" ]; then
177
- printf '%s' "${path##*/}" | grep -qE "$only_named" || continue
245
+ printf '%s' "$rel_path" | grep -qE "$only_named" || continue
178
246
  fi
179
247
 
180
248
  case "$(field "$signal" '.scope')" in
@@ -124,7 +124,7 @@ esac
124
124
 
125
125
  # --- слой по пути: ведение работы -------------------------------------------------------------
126
126
  #
127
- # Папка задачи, договорённость о продукте до кода и линия работ — первые файлы, которые
127
+ # Папка задачи, договорённость о продукте до кода и замысел эпика — первые файлы, которые
128
128
  # заводятся в работе. Требование ловит на них того, кто пошёл мимо порядка: «пришла новая
129
129
  # задача» инструментом не является, и поймать это больше нечем.
130
130
  case "$target" in
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # rt-hook: PreToolUse Edit|Write|MultiEdit|Bash
2
+ # rt-hook: PreToolUse Edit|Write|MultiEdit|Bash|mcp__webstorm__create_new_file|mcp__webstorm__execute_terminal_command|mcp__webstorm__execute_tool
3
3
  # Гейт правил: не даёт править файл, пока не загружено правило, под которое он подпадает.
4
4
  #
5
5
  # Закон и правило, которых никто не открывает, не действуют. Напоминание в подсказке помогает
@@ -90,6 +90,32 @@ case "$tool" in
90
90
  fi
91
91
  req="$(skill_for bash "$target" "" 2>/dev/null)"
92
92
  kind="command"
93
+ # Команда оболочки, которая пишет файл, — та же правка, и правило ей нужно то же.
94
+ # Без этого яруса гейт обходится сменой не инструмента, а способа записи: отбитая
95
+ # правка легла командой дважды за один заход. Разбор —
96
+ # `2026-08-15-guard-denied-shell-wrote-anyway.md`.
97
+ for profile in "$rt_hooks_dir/../rt-kit/defaults/project.sh" "$rt_hooks_dir/../defaults/project.sh" "${CLAUDE_PROJECT_DIR:-.}/.claude/rt-kit/defaults/project.sh" "${CLAUDE_PROJECT_DIR:-.}/.claude/rt-kit/project.sh"; do
98
+ # shellcheck disable=SC1090
99
+ [ -f "$profile" ] && . "$profile" 2>/dev/null
100
+ done
101
+ if command -v rt_shell_writes >/dev/null 2>&1 && command -v rt_shell_paths >/dev/null 2>&1 \
102
+ && rt_shell_writes "$target"; then
103
+ while IFS= read -r written_path; do
104
+ [ -z "$written_path" ] && continue
105
+ case "$written_path" in
106
+ /*) ;;
107
+ *) written_path="${CLAUDE_PROJECT_DIR:-.}/$written_path" ;;
108
+ esac
109
+ case "$written_path" in
110
+ "${CLAUDE_PROJECT_DIR:-.}"/*) ;;
111
+ *) continue ;;
112
+ esac
113
+ more="$(skill_for edit "$written_path" "" 2>/dev/null)"
114
+ [ -n "$more" ] && req="$req $more"
115
+ done <<EOF
116
+ $(rt_shell_paths "$target")
117
+ EOF
118
+ fi
93
119
  ;;
94
120
  mcp__claude-in-chrome__*)
95
121
  req="$(skill_for browser "$tool" "" 2>/dev/null)"
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env bash
2
- # rt-hook: PreToolUse Edit|Write|MultiEdit
2
+ # rt-hook: PreToolUse Edit|Write|MultiEdit|Bash|mcp__webstorm__create_new_file|mcp__webstorm__execute_terminal_command|mcp__webstorm__execute_tool
3
3
  # Требует: hooks/profile-check.sh
4
4
  # PreToolUse guard for Edit|Write|MultiEdit: код не пишется раньше замысла.
5
5
  #
6
6
  # Работа идёт много заходов, и между ними исполнитель не помнит ничего. Замысел, лежащий на
7
7
  # диске, — единственное, что переживает перерыв: изменения к этому моменту бывают не
8
- # закоммичены, отчёт не открыт, а очередь работ показывает задачу начатой и молчит о том, что
8
+ # закоммичены, PR не открыт, а очередь работ показывает задачу начатой и молчит о том, что
9
9
  # внутри неё сделано.
10
10
  #
11
11
  # Гард требует три вещи и ровно их: папку задачи по имени ветки, замысел в ней и названную в
@@ -24,22 +24,8 @@ input="$(cat 2>/dev/null)"
24
24
  [ -z "$input" ] && exit 0
25
25
  command -v jq >/dev/null 2>&1 || exit 0
26
26
 
27
- tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
28
- case "$tool" in
29
- # Инструмент редактора заводит файл теми же двумя данными, только называет их иначе —
30
- # без этой ветки правка шла бы мимо гарда сменой инструмента.
31
- Edit | Write | MultiEdit | mcp__webstorm__create_new_file) ;;
32
- *) exit 0 ;;
33
- esac
34
-
35
- path="$(printf '%s' "$input" | jq -r '.tool_input.file_path // .tool_input.pathInProject // empty' 2>/dev/null)"
36
- [ -z "$path" ] && exit 0
37
- case "$path" in
38
- /*) ;;
39
- ?*) path="${CLAUDE_PROJECT_DIR:-.}/$path" ;;
40
- esac
41
-
42
27
  # Профиль дерева: сперва умолчание пакета, поверх него — надстройка проекта, если она есть.
28
+ # Читается до разбора пути: пути из команды оболочки вынимает как раз профиль.
43
29
  rt_hooks_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
44
30
  for profile in "$rt_hooks_dir/../rt-kit/defaults/project.sh" "$rt_hooks_dir/../defaults/project.sh" "${CLAUDE_PROJECT_DIR:-.}/.claude/rt-kit/defaults/project.sh" "${CLAUDE_PROJECT_DIR:-.}/.claude/rt-kit/project.sh"; do
45
31
  # shellcheck disable=SC1090
@@ -52,12 +38,66 @@ done
52
38
  [ -f "$rt_hooks_dir/profile-check.sh" ] && . "$rt_hooks_dir/profile-check.sh"
53
39
  command -v rt_needs >/dev/null 2>&1 || rt_needs() { command -v "$1" >/dev/null 2>&1; }
54
40
 
41
+ tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
42
+ candidates=""
43
+ case "$tool" in
44
+ # Инструмент редактора заводит файл теми же двумя данными, только называет их иначе —
45
+ # без этой ветки правка шла бы мимо гарда сменой инструмента.
46
+ Edit | Write | MultiEdit | mcp__webstorm__create_new_file)
47
+ candidates="$(printf '%s' "$input" | jq -r '.tool_input.file_path // .tool_input.pathInProject // empty' 2>/dev/null)"
48
+ ;;
49
+ # Второй ярус: та же правка, положенная командой оболочки. Без него отказ гарда обходится
50
+ # сменой не инструмента, а способа записи — перенаправлением, `sed -i`, интерпретатором с
51
+ # heredoc. Разбор — `2026-08-15-guard-denied-shell-wrote-anyway.md`.
52
+ #
53
+ # Терминал среды исполняет ту же командную строку и кладёт её в то же поле: без этих двух
54
+ # имён гард стоял бы объявленным на них и молча пропускал — состояние хуже необъявленного,
55
+ # потому что снаружи выглядит закрытым.
56
+ Bash | mcp__webstorm__execute_terminal_command | mcp__webstorm__execute_tool)
57
+ cmd="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null)"
58
+ [ -z "$cmd" ] && exit 0
59
+ # Универсальный исполнитель прячет настоящую команду во вложенной строке: без её разбора
60
+ # путь стоит за кавычкой, и до него не дотягивается ни один образец.
61
+ if [ "$tool" = "mcp__webstorm__execute_tool" ] && command -v perl >/dev/null 2>&1; then
62
+ inner="$(printf '%s' "$cmd" | perl -0ne '
63
+ if (/--command(?:=|\s+)(?:"((?:[^"\\]|\\.)*)"|\x27([^\x27]*)\x27|(.+))/s) {
64
+ print defined $1 ? $1 : (defined $2 ? $2 : $3);
65
+ }
66
+ ' 2>/dev/null)"
67
+ [ -n "$inner" ] && cmd="$inner"
68
+ fi
69
+ rt_needs rt_shell_writes task-flow-guard || exit 0
70
+ rt_needs rt_shell_paths task-flow-guard || exit 0
71
+ rt_shell_writes "$cmd" || exit 0
72
+ candidates="$(rt_shell_paths "$cmd")"
73
+ ;;
74
+ *) exit 0 ;;
75
+ esac
76
+ [ -z "$candidates" ] && exit 0
77
+
55
78
  # Признак «правка меняет поведение» — путь, а не оценка на глаз: оценку назначает тот, кому
56
79
  # она мешает, и порог плывёт. Где живёт код приложения, знает профиль: правила, тексты, обвязка
57
80
  # и зависимости под требование не попадают — иначе разбор задачи нельзя было бы вести до
58
81
  # заведения ветки.
59
82
  rt_needs rt_is_app_code task-flow-guard || exit 0
60
- rt_is_app_code "$path" || exit 0
83
+
84
+ # Судится каждый названный путь: команда пишет столько файлов, сколько в ней стоит, и одного
85
+ # под требованием довольно, чтобы отбить её целиком.
86
+ path=""
87
+ while IFS= read -r candidate; do
88
+ [ -z "$candidate" ] && continue
89
+ case "$candidate" in
90
+ /*) ;;
91
+ *) candidate="${CLAUDE_PROJECT_DIR:-.}/$candidate" ;;
92
+ esac
93
+ if rt_is_app_code "$candidate"; then
94
+ path="$candidate"
95
+ break
96
+ fi
97
+ done <<EOF
98
+ $candidates
99
+ EOF
100
+ [ -z "$path" ] && exit 0
61
101
 
62
102
  # Каталог папок задач: у дерева он свой, но имя обычно общее.
63
103
  tasks_dir="${RT_TASKS_DIR:-docs/tasks}"
@@ -113,7 +153,7 @@ fi
113
153
 
114
154
  # Договорённость, влитая в спек домена, с диска уходит — так и задумано: в главной ветке
115
155
  # директории «предложено» быть не должно. Но замысел на неё ссылается до конца работы, и без
116
- # этой развилки последний коммит отчёта запирал бы ветку: ни правки по замечаниям разбора, ни
156
+ # этой развилки последний коммит PR запирал бы ветку: ни правки по замечаниям разбора, ни
117
157
  # записи в журнал изменений после вливания уже не сделать.
118
158
  #
119
159
  # Влитое от незаведённого отличает история ветки: путь, которого в ней никогда не было,