@rt-tools/agent-kit 0.10.0 → 0.12.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 (139) hide show
  1. package/assets/checks/check-file-size.mjs +19 -4
  2. package/assets/checks/check-state-next.mjs +202 -0
  3. package/assets/checks/check-states.mjs +142 -0
  4. package/assets/checks/check-turn-map.mjs +146 -0
  5. package/assets/checks/rt-kit-checks.config.mjs +16 -2
  6. package/assets/defaults/project.sh +67 -7
  7. package/assets/defaults/turn-map.md +48 -0
  8. package/assets/hooks/browser-device-id.sh +2 -0
  9. package/assets/hooks/browser-guard-device-id.sh +5 -1
  10. package/assets/hooks/browser-guard-no-asking.sh +5 -1
  11. package/assets/hooks/browser-guard-no-listing.sh +2 -0
  12. package/assets/hooks/browser-guard-no-other-drivers.sh +7 -3
  13. package/assets/hooks/browser-guard-require-select.sh +6 -2
  14. package/assets/hooks/claim-guard.sh +5 -1
  15. package/assets/hooks/commit-msg.sh +2 -0
  16. package/assets/hooks/conscience-guard.sh +5 -1
  17. package/assets/hooks/constitution-index.sh +2 -0
  18. package/assets/hooks/dev-server-guard.sh +7 -3
  19. package/assets/hooks/dispatch.sh +69 -0
  20. package/assets/hooks/docs-guard.sh +8 -4
  21. package/assets/hooks/exam-guard.sh +7 -3
  22. package/assets/hooks/git-guard-delivery-signature.sh +2 -0
  23. package/assets/hooks/git-guard-delivery.sh +39 -5
  24. package/assets/hooks/git-guard-main.sh +8 -4
  25. package/assets/hooks/git-guard-push-tests.sh +8 -4
  26. package/assets/hooks/glossary-load.sh +2 -0
  27. package/assets/hooks/grill-gate.sh +6 -2
  28. package/assets/hooks/handoff-entry-guard.sh +6 -2
  29. package/assets/hooks/handoff-write.sh +124 -0
  30. package/assets/hooks/hook-input.sh +54 -0
  31. package/assets/hooks/lint-after-edit.sh +7 -3
  32. package/assets/hooks/observe.sh +2 -0
  33. package/assets/hooks/postmortem-guard.sh +5 -1
  34. package/assets/hooks/proposal-guard.sh +5 -1
  35. package/assets/hooks/prose-style-guard.sh +7 -3
  36. package/assets/hooks/qa-dataid-guard.sh +6 -2
  37. package/assets/hooks/rerun-guard.sh +7 -3
  38. package/assets/hooks/reuse-first-guard.sh +7 -3
  39. package/assets/hooks/roles.sh +2 -0
  40. package/assets/hooks/rule-article.sh +99 -0
  41. package/assets/hooks/skill-gate-layers.sh +2 -0
  42. package/assets/hooks/skill-gate-rearm.sh +5 -1
  43. package/assets/hooks/skill-gate.sh +25 -2
  44. package/assets/hooks/skill-loaded.sh +5 -1
  45. package/assets/hooks/sql-guard-parse.sh +2 -0
  46. package/assets/hooks/sql-guard-request.sh +4 -1
  47. package/assets/hooks/sql-guard-target.sh +2 -0
  48. package/assets/hooks/sql-guard-write.sh +2 -0
  49. package/assets/hooks/sql-guard.sh +6 -2
  50. package/assets/hooks/task-context-load.sh +2 -0
  51. package/assets/hooks/task-flow-guard.sh +8 -4
  52. package/assets/hooks/turn-entry-load.sh +62 -0
  53. package/assets/hooks/turn-exit-guard.sh +44 -17
  54. package/assets/hooks/utf8.sh +35 -0
  55. package/assets/hooks/waiting-turn-guard.sh +5 -1
  56. package/assets/hooks/window-fill-guard.sh +37 -5
  57. package/assets/laws/work-conduct.md +34 -9
  58. package/assets/patterns/dependencies-upgrade.md +1 -1
  59. package/assets/patterns/doc-style-write.md +3 -3
  60. package/assets/patterns/git-workflow-commit.azure.md +2 -202
  61. package/assets/patterns/git-workflow-commit.github.md +2 -258
  62. package/assets/patterns/git-workflow-commit.gitlab.md +1 -217
  63. package/assets/patterns/git-workflow-docker.md +3 -3
  64. package/assets/patterns/git-workflow-merge.md +3 -2
  65. package/assets/patterns/git-workflow-migration.md +3 -3
  66. package/assets/patterns/git-workflow-pr.azure.md +224 -0
  67. package/assets/patterns/git-workflow-pr.github.md +280 -0
  68. package/assets/patterns/git-workflow-pr.gitlab.md +240 -0
  69. package/assets/patterns/git-workflow-restart.md +3 -3
  70. package/assets/patterns/git-workflow-secrets.md +3 -3
  71. package/assets/patterns/task-flow-archive.md +193 -0
  72. package/assets/patterns/task-flow-close.md +11 -160
  73. package/assets/patterns/task-flow-handoff.md +22 -4
  74. package/assets/patterns/task-flow-resume.md +6 -0
  75. package/assets/patterns/task-flow-start.md +41 -0
  76. package/assets/patterns/turn-entry-map.md +81 -0
  77. package/assets/pitfalls/doc-style.md +80 -0
  78. package/assets/pitfalls/git-workflow.azure.md +50 -0
  79. package/assets/pitfalls/git-workflow.github.md +78 -0
  80. package/assets/pitfalls/git-workflow.gitlab.md +49 -0
  81. package/assets/pitfalls/spec-driven.md +36 -0
  82. package/assets/pitfalls/styling-bem.md +45 -0
  83. package/assets/pitfalls/task-flow.md +62 -0
  84. package/assets/pitfalls/testing.md +70 -0
  85. package/assets/rules/deploy-flow.azure.md +106 -0
  86. package/assets/rules/deploy-flow.github.md +113 -0
  87. package/assets/rules/deploy-flow.gitlab.md +108 -0
  88. package/assets/rules/doc-style.md +25 -76
  89. package/assets/rules/git-workflow.azure.md +6 -92
  90. package/assets/rules/git-workflow.github.md +14 -127
  91. package/assets/rules/git-workflow.gitlab.md +6 -93
  92. package/assets/rules/spec-driven.md +39 -30
  93. package/assets/rules/styling-bem.md +20 -39
  94. package/assets/rules/task-flow.md +17 -186
  95. package/assets/rules/testing.md +3 -64
  96. package/assets/rules/turn-conduct.md +206 -0
  97. package/assets/rules/turn-entry.md +93 -0
  98. package/assets/rules/typescript-conventions.md +15 -0
  99. package/assets/skills/agent-kit.md +35 -12
  100. package/assets/templates/pitfalls.md +10 -0
  101. package/assets/templates/rule.md +5 -3
  102. package/lib/assets.d.ts.map +1 -1
  103. package/lib/assets.js +6 -1
  104. package/lib/assets.js.map +1 -1
  105. package/lib/cargo.d.ts +42 -0
  106. package/lib/cargo.d.ts.map +1 -1
  107. package/lib/cargo.js +2 -0
  108. package/lib/cargo.js.map +1 -1
  109. package/lib/cascade.d.ts.map +1 -1
  110. package/lib/cascade.js +19 -1
  111. package/lib/cascade.js.map +1 -1
  112. package/lib/commands.d.ts.map +1 -1
  113. package/lib/commands.js +65 -4
  114. package/lib/commands.js.map +1 -1
  115. package/lib/config.d.ts +16 -1
  116. package/lib/config.d.ts.map +1 -1
  117. package/lib/config.js +8 -0
  118. package/lib/config.js.map +1 -1
  119. package/lib/hooks-map.d.ts +13 -0
  120. package/lib/hooks-map.d.ts.map +1 -1
  121. package/lib/hooks-map.js +33 -1
  122. package/lib/hooks-map.js.map +1 -1
  123. package/lib/observations.d.ts +35 -1
  124. package/lib/observations.d.ts.map +1 -1
  125. package/lib/observations.js +14 -2
  126. package/lib/observations.js.map +1 -1
  127. package/lib/ship.d.ts +2 -0
  128. package/lib/ship.d.ts.map +1 -1
  129. package/lib/ship.js +2 -0
  130. package/lib/ship.js.map +1 -1
  131. package/lib/thresholds.d.ts +49 -0
  132. package/lib/thresholds.d.ts.map +1 -0
  133. package/lib/thresholds.js +151 -0
  134. package/lib/thresholds.js.map +1 -0
  135. package/package.json +1 -1
  136. package/rt-tools-agent-kit-0.12.0.tgz +0 -0
  137. package/assets/commands/agent-kit-digest.md +0 -88
  138. package/assets/commands/rules-review.md +0 -98
  139. package/rt-tools-agent-kit-0.10.0.tgz +0 -0
@@ -87,7 +87,8 @@ rt_push_checks_default() {
87
87
  [ -x "$root/$RT_HOOKS_TESTS" ] && printf '%s\n' "bash $RT_HOOKS_TESTS"
88
88
 
89
89
  for check in check-doc-paths check-specs check-file-size check-dupes check-styles \
90
- check-lib-layers check-reuse check-schema-drift check-push-gate; do
90
+ check-lib-layers check-reuse check-schema-drift check-states check-state-next \
91
+ check-turn-map check-push-gate; do
91
92
  [ -f "$root/$RT_CHECKS_DIR/$check.mjs" ] && printf '%s\n' "node $RT_CHECKS_DIR/$check.mjs"
92
93
  done
93
94
 
@@ -135,9 +136,27 @@ RT_ARCHIVE_DIR="${RT_ARCHIVE_DIR:-docs/archive}"
135
136
  # Размер окна захода в токенах и пороги стража. Пусто — стража нет: считать долю не от чего, а
136
137
  # выведенный из записи захода размер врал бы — модель записана там без пометки о расширенном
137
138
  # окне. Дерево задаёт его в настройке агента, переменной окружения того же имени.
139
+ #
140
+ # Теми же двумя числами задаётся порог, на котором инструмент сжимает контекст сам, — а значит и
141
+ # порог, на котором пишется передача захода. В настройке агента им отвечает пара: размер окна
142
+ # автосжатия и доля в процентах, при которой оно приходит. Разъехавшись, они дают заход, который
143
+ # либо сжимается до того, как передача написана, либо доживает до предела окна. Сведены они или
144
+ # нет — говорит разбор состояния раскладки; здесь у пакета своих значений нет, потому что окно
145
+ # принадлежит дереву, а не ему.
146
+ #
147
+ # Сведёнными считаются не совпавшие числа, а разведённые. Порог сжатия обязан стоять НИЖЕ порога
148
+ # остановки: первым срабатывает то, что заход продолжает, а не то, что его останавливает.
149
+ # Совпавшая пара — гонка, и выигрывает её страж: он стоит на вызове инструмента, а сжатие
150
+ # приходит между ходами. Ровно так заход и вставал на пороге вместо того, чтобы продолжиться
151
+ # сжатым, — при том что сверка обе стороны считала настроенными.
152
+ #
153
+ # Насколько ниже — своё число дерева: сжатие идёт не мгновенно, и разница в один процент
154
+ # требование «ниже» удовлетворяет, а работу не спасает. Запас объявляется, а не выводится
155
+ # разницей.
138
156
  RT_WINDOW_TOKENS="${RT_WINDOW_TOKENS:-}"
139
157
  RT_WINDOW_WARN_PCT="${RT_WINDOW_WARN_PCT:-40}"
140
158
  RT_WINDOW_STOP_PCT="${RT_WINDOW_STOP_PCT:-50}"
159
+ RT_WINDOW_MARGIN_PCT="${RT_WINDOW_MARGIN_PCT:-5}"
141
160
 
142
161
  # Куда кладётся передача захода. Вне дерева: состояние работы живёт в ходе работы и коммитится,
143
162
  # а передача его пересказывает для вставки в новый заход и в историю не едет.
@@ -214,19 +233,52 @@ rt_is_app_code_default() {
214
233
  # Список намеренно широк, и цена этого названа: команда чтения, в которой стоит имя
215
234
  # интерпретатора, будет отбита наравне с командой правки. Узкий список стоил бы дороже —
216
235
  # пропущенная форма записи возвращает обход целиком, а найти её можно только промахом.
236
+ #
237
+ # Перенаправление в пустое устройство и в поток ошибок снимается до разбора: файла оно не
238
+ # пишет, а выглядит как перенаправление в файл. Так глушат вывод команды чтения, и без этого
239
+ # `grep -rn x libs/ 2>/dev/null` судится наравне с записью — правило требуется на чтение, а
240
+ # отбитий, пришедшихся не на правку файла, набирается большинство. Настоящая запись рядом с
241
+ # заглушённым потоком остаётся видной: снимается перенаправление, а не команда целиком.
217
242
  rt_shell_writes_default() {
218
- printf '%s' "$1" | grep -Eq \
219
- '>>?[[:space:]]*[^|&>[:space:]]|\btee\b|\bsed\b[^|]*-i|\bperl\b[^|]*-i|\bpython3?\b|\bnode\b|\bruby\b|\bdd\b[^|]*of=|\bcp\b|\bmv\b|\brm\b|\btouch\b|\btruncate\b|\binstall\b|\bpatch\b|\bgit[[:space:]]+(checkout|restore|apply|stash)\b'
243
+ printf '%s' "$1" \
244
+ | sed -E 's#(&|[0-9]*)>>?[[:space:]]*/dev/(null|stderr)##g; s#[0-9]*>&[0-9-]##g' \
245
+ | grep -Eq \
246
+ '>>?[[:space:]]*[^|&>[:space:]]|\btee\b|\bsed\b[^|]*-i|\bperl\b[^|]*-i|\bpython3?\b|\bnode\b|\bruby\b|\bdd\b[^|]*of=|\bcp\b|\bmv\b|\brm\b|\btouch\b|\btruncate\b|\binstall\b|\bpatch\b|\bgit[[:space:]]+(checkout|restore|apply|stash)\b'
220
247
  }
221
248
 
222
249
  # Пути, названные командой оболочки. Печатает по одному в строке; судит их зовущий.
223
250
  #
224
251
  # Разбирать оболочку по-настоящему нечем — здесь и не разбирают: из текста вынимается всё, что
225
252
  # похоже на путь, и каждое отдаётся признаку. Лишнее он отсеет сам, а пропущенное вернуло бы
226
- # обход. Кавычки и heredoc снимаются заменой на пробел: путь внутри них тот же самый.
253
+ # обход. Кавычки снимаются заменой на пробел: путь внутри них тот же самый.
254
+ #
255
+ # Тело документа на месте путей не даёт: там лежит текст, который команда кладёт в файл, а
256
+ # чужой путь, названный в нём словами, требовал бы правила под запись, которой нет. Запись
257
+ # замысла так отбивалась трижды подряд, пока пути под каталогом кода не были названы иначе.
258
+ # Судится заголовок команды — именно в нём стоит тот путь, куда команда пишет.
259
+ #
260
+ # Исключение — интерпретатор: ему код приходит телом, и путь записи стоит именно там. Признак
261
+ # ошибается в сторону лишнего чтения тела: имя интерпретатора, стоящее в команде где угодно,
262
+ # возвращает разбор тела целиком.
227
263
  rt_shell_paths_default() {
228
- printf '%s' "$1" \
229
- | tr "\"'\`(),;=" ' ' \
264
+ text="$(printf '%s' "$1" | tr "\"'\`" ' ')"
265
+ if ! printf '%s' "$text" \
266
+ | grep -Eq '(^|[|;&(]|[[:space:]])(python3?|node|ruby|perl|php|deno|bun|bash|sh|zsh)([[:space:]]|$)'; then
267
+ text="$(printf '%s' "$text" | awk '
268
+ function trim(s) { sub(/^[ \t]+/, "", s); sub(/[ \t]+$/, "", s); return s }
269
+ tag != "" { if (trim($0) == tag) tag = ""; next }
270
+ {
271
+ print
272
+ if (match($0, /<<-?[ \t]*[A-Za-z_][A-Za-z0-9_]*/)) {
273
+ t = substr($0, RSTART, RLENGTH)
274
+ sub(/^<<-?[ \t]*/, "", t)
275
+ tag = t
276
+ }
277
+ }')"
278
+ fi
279
+
280
+ printf '%s' "$text" \
281
+ | tr "(),;=" ' ' \
230
282
  | tr '[:space:]' '\n' \
231
283
  | grep -E '^[A-Za-z0-9_@.-]*/[A-Za-z0-9_@./-]+$' \
232
284
  | sed 's|^\./||' \
@@ -265,9 +317,17 @@ RT_TASK_TITLE_RE="${RT_TASK_TITLE_RE:-^\[[A-Za-z]+-[0-9]+\][[:space:]]+[^[:space
265
317
  RT_TASK_NEW_CMD="${RT_TASK_NEW_CMD:-npm run task:new}"
266
318
  RT_BOARD_CHECK_CMD="${RT_BOARD_CHECK_CMD:-npm run check:board}"
267
319
 
268
- # Учётная запись, которую ставят исполнителем. Умолчание молчит: у каждого дерева она своя.
320
+ # Учётная запись, которую ставят исполнителем и которой открывают заявку. Умолчание молчит: у
321
+ # каждого дерева она своя, а у иного её нет вовсе.
269
322
  RT_TASK_BOT="${RT_TASK_BOT:-}"
270
323
 
324
+ # Чем вызову подставляют токен машинной записи и как эта подстановка выглядит целиком. Клиент
325
+ # хостинга держит две записи сразу — залогиненную и ту, чей токен стоит в окружении вызова, — и
326
+ # из текста команды видно только явную подстановку. Оба умолчания молчат: дерево без отдельной
327
+ # машинной записи автора заявки не судит вовсе.
328
+ RT_PULL_TOKEN_VAR="${RT_PULL_TOKEN_VAR:-}"
329
+ RT_PULL_TOKEN_HINT="${RT_PULL_TOKEN_HINT:-}"
330
+
271
331
  # Команда перевода задачи между колонками очереди работ и имя первой колонки — той, из которой
272
332
  # задача уходит, когда её берут в работу. Имя без умолчания: колонки дерево называет своими
273
333
  # словами, а выдуманное не совпало бы ни с чем и молча выключило бы проверку колонки.
@@ -0,0 +1,48 @@
1
+ # Карта хода
2
+
3
+ Это не правило, а его короткая выжимка: правило объясняет, карта называет. Полный текст —
4
+ скил `task-flow`; он же называет паттерн, который ведёт каждое состояние. Чем ход
5
+ кончается — скил `turn-conduct` под тем же законом.
6
+
7
+ Состояние работы объявлено строкой в разделе «Где стоим» хода работы. Пока обязательное
8
+ действие не сделано, работа стоит в том же состоянии.
9
+
10
+ ## Состояния и обязательные действия
11
+
12
+ | Состояние | Обязательное действие | Ведёт паттерн |
13
+ | ------------------------- | ------------------------------------------------------ | ------------------ |
14
+ | `просьба-не-разобрана` | разведка по дереву, затем вопросы | `task-flow-start` |
15
+ | `разбор-закрыт` | договорённость о продукте либо причина её отсутствия | `task-flow-start` |
16
+ | `договорённость-записана` | завести задачу, ветку и папку | `task-flow-start` |
17
+ | `задача-взята` | написать замысел | `task-flow-start` |
18
+ | `замысел-записан` | делать первый этап | `task-flow-start` |
19
+ | `этап-идёт` | доделать этап и отметить в ходе работы | `task-flow-resume` |
20
+ | `этапы-кончились` | прогнать набор и открыть PR черновиком | `task-flow-close` |
21
+ | `работа-отдана` | взять следующую задачу | `task-flow-resume` |
22
+ | `разбор-кончился` | влить договорённость, привести тексты, разобрать папку | `task-flow-close` |
23
+ | `папка-разобрана` | снять черновик и попросить влить | `task-flow-archive` |
24
+ | `влито` | разбор работы правилами и сверка очереди | `task-flow-archive` |
25
+
26
+ Ни у одного состояния обязательное действие не звучит как «ждать». Прогон, разбор владельцем и
27
+ слияние идут без исполнителя и от взгляда быстрее не становятся.
28
+
29
+ ## Чем ход кончается
30
+
31
+ Способов четыре, и других нет.
32
+
33
+ | Выход | Чем подтверждается |
34
+ | -------------------------------------------------- | --------------------------------------------------------------- |
35
+ | вопрос владельцу, ответа на который в правилах нет | вопрос задан, и за тот же ход правила читались |
36
+ | отказ гарда | отказ назван владельцу, обход не искался |
37
+ | заполненное окно там, где сжатия нет | ход работы дописан, передача написана |
38
+ | работа отдана, и следующая начата | PR открыт, и по следующей задаче сделано действие, а не сказано |
39
+
40
+ Там, где дерево объявило порог сжатия ниже порога остановки, заполненное окно ход не кончает:
41
+ контекст сжимается, передача приходит входом, и работа идёт дальше тем же заходом. Порог
42
+ остановки там — страховка на случай, когда сжатие не пришло.
43
+
44
+ Всё остальное — продолжение хода. Ходом не кончаются: коммит, записанный замысел, закрытый
45
+ разбор просьбы, прочитанная договорённость, зелёная проверка, сводка о чужом шаге, объявление
46
+ намерения и названная, но не запущенная команда. Переход из состояния в состояние — тем более:
47
+ обязательное действие сделано, и следующее делается тем же ходом. Названо может быть только
48
+ сделанное.
@@ -8,6 +8,8 @@
8
8
  #
9
9
  # Файл с идентификатором в репозиторий не коммитится: у каждой машины он свой.
10
10
 
11
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
12
+
11
13
  if [ -n "${RT_BROWSER_DEVICE_ID:-}" ]; then
12
14
  printf '%s\n' "$RT_BROWSER_DEVICE_ID"
13
15
  exit 0
@@ -11,7 +11,11 @@
11
11
  #
12
12
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: помощник не назвал профиль — пропуск.
13
13
 
14
- input="$(cat 2>/dev/null)"
14
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
15
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
16
+
17
+ rt_hook_read
18
+ input="$RT_HOOK_INPUT"
15
19
 
16
20
  device_id="$("${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/browser-device-id.sh" 2>/dev/null)"
17
21
  [ -z "$device_id" ] && exit 0
@@ -14,7 +14,11 @@
14
14
  #
15
15
  # FAIL-OPEN when browser-device-id.sh prints nothing.
16
16
 
17
- input="$(cat 2>/dev/null)"
17
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
18
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
19
+
20
+ rt_hook_read
21
+ input="$RT_HOOK_INPUT"
18
22
 
19
23
  device_id="$("${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/browser-device-id.sh" 2>/dev/null)"
20
24
  [ -z "$device_id" ] && exit 0
@@ -10,6 +10,8 @@
10
10
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: помощник не назвал профиль — пропуск. Гард, который не может назвать
11
11
  # нужный профиль, ничего не предлагает взамен, и слепой отказ только заводил бы работу в тупик.
12
12
 
13
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
14
+
13
15
  cat >/dev/null 2>&1
14
16
 
15
17
  device_id="$("${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/browser-device-id.sh" 2>/dev/null)"
@@ -13,12 +13,16 @@
13
13
  #
14
14
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: помощник не назвал профиль — пропуск.
15
15
 
16
- input="$(cat 2>/dev/null)"
16
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
17
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
18
+
19
+ rt_hook_read
20
+ input="$RT_HOOK_INPUT"
17
21
 
18
22
  device_id="$("${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/browser-device-id.sh" 2>/dev/null)"
19
23
  [ -z "$device_id" ] && exit 0
20
24
 
21
- tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
25
+ tool="$(rt_hook_tool)"
22
26
 
23
27
  # shellcheck disable=SC1090
24
28
  [ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" ] \
@@ -43,7 +47,7 @@ case "$tool" in
43
47
  *) exit 0 ;;
44
48
  esac
45
49
 
46
- cmd="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null)"
50
+ cmd="$(rt_hook_cmd)"
47
51
  [ -z "$cmd" ] && exit 0
48
52
 
49
53
  if [ "$tool" = "mcp__webstorm__execute_tool" ] && command -v perl >/dev/null 2>&1; then
@@ -20,12 +20,16 @@
20
20
  #
21
21
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: помощник не назвал профиль — пропуск.
22
22
 
23
- input="$(cat 2>/dev/null)"
23
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
24
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
25
+
26
+ rt_hook_read
27
+ input="$RT_HOOK_INPUT"
24
28
 
25
29
  device_id="$("${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/browser-device-id.sh" 2>/dev/null)"
26
30
  [ -z "$device_id" ] && exit 0
27
31
 
28
- tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
32
+ tool="$(rt_hook_tool)"
29
33
  # У перечисления, переключения и самого выбора свои гарды.
30
34
  case "$tool" in
31
35
  *list_connected_browsers|*switch_browser|*select_browser) exit 0 ;;
@@ -25,7 +25,11 @@
25
25
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: при любой ошибке, нехватке `jq`, отсутствии записи хода или пустом
26
26
  # тексте ход РАЗРЕШАЕТСЯ (exit 0). Сломанный гард не имеет права заклинить разговор.
27
27
 
28
- input="$(cat 2>/dev/null)"
28
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
29
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
30
+
31
+ rt_hook_read
32
+ input="$RT_HOOK_INPUT"
29
33
  [ -z "$input" ] && exit 0
30
34
  command -v jq >/dev/null 2>&1 || exit 0
31
35
 
@@ -15,6 +15,8 @@
15
15
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: проверяльщика нет (свежий клон без установленных зависимостей) —
16
16
  # пропуск. Формат тогда не проверяется, и это лучше, чем заклинивший коммит.
17
17
 
18
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
19
+
18
20
  msg_file="$1"
19
21
  [ -z "$msg_file" ] && exit 0
20
22
 
@@ -19,7 +19,11 @@
19
19
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: нет `jq`, нет записи хода, роль молчит или отвечает не по форме — ход
20
20
  # разрешается. Сломанная совесть не имеет права заклинить разговор.
21
21
 
22
- input="$(cat 2>/dev/null)"
22
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
23
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
24
+
25
+ rt_hook_read
26
+ input="$RT_HOOK_INPUT"
23
27
  [ -z "$input" ] && exit 0
24
28
  command -v jq >/dev/null 2>&1 || exit 0
25
29
 
@@ -12,6 +12,8 @@
12
12
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: любая ошибка начинает сессию без добавленного контекста (exit 0).
13
13
  # Сломанный вход не имеет права остановить сессию.
14
14
 
15
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
16
+
15
17
  dir="${CLAUDE_PROJECT_DIR:-.}/docs/constitution"
16
18
  [ -d "$dir" ] || exit 0
17
19
 
@@ -14,9 +14,13 @@
14
14
  # Где именно подняты приложения, знает профиль проекта: .claude/rt-kit/project.sh, переменная
15
15
  # RT_STANDS. Нет профиля — текст отказа остаётся общим, сам гард работает.
16
16
 
17
- input="$(cat 2>/dev/null)"
17
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
18
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
18
19
 
19
- tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
20
+ rt_hook_read
21
+ input="$RT_HOOK_INPUT"
22
+
23
+ tool="$(rt_hook_tool)"
20
24
  case "$tool" in
21
25
  Bash | mcp__webstorm__execute_terminal_command | mcp__webstorm__execute_tool) ;;
22
26
  # Готовая конфигурация запуска командной строки не показывает — видно только её имя.
@@ -46,7 +50,7 @@ case "$tool" in
46
50
  *) exit 0 ;;
47
51
  esac
48
52
 
49
- cmd="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null)"
53
+ cmd="$(rt_hook_cmd)"
50
54
  [ -z "$cmd" ] && exit 0
51
55
 
52
56
  # Универсальный исполнитель среды передаёт настоящую команду вложенной строкой. Разбирать надо
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env bash
2
+ # Диспетчер событий агента. НЕ гард: объявления `rt-hook:` у него нет — наоборот, он читает
3
+ # такие объявления у остальных. В настройке агента он стоит один на событие вместо списка.
4
+ #
5
+ # Зачем он есть. Агент зовёт каждый гард своим процессом и каждому подаёт один и тот же ввод.
6
+ # Гардов на вызове инструмента восемнадцать, и каждый разбирает ввод заново — по шесть-семь
7
+ # вызовов разборщика. Замер этого дерева: восемьсот двадцать шесть миллисекунд на вызов, из
8
+ # которых восемьдесят пять — запуск оболочек, а остальное — повторный разбор одного текста.
9
+ #
10
+ # Что он делает. Читает ввод один раз, разбирает его один раз, кладёт поля в окружение и зовёт
11
+ # ветки события по порядку. Первый ненулевой код возврата отдаётся агенту вместе с выводом
12
+ # гарда — остальные ветки не зовутся: отказ и раньше кончал вызов на первом же гарде.
13
+ #
14
+ # Чего он не делает. Он не судит ничего сам и ни одного гарда не заменяет: файлы остаются как
15
+ # были, меняется точка входа. Событие и образец вызова гард несёт сам, строкой `# rt-hook:` в
16
+ # своей шапке, и карта собирается из них — список, выписанный отдельно, разошёлся бы с набором
17
+ # файлов на первом же добавленном.
18
+ #
19
+ # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: нет события в доводе, нет каталога гардов, нет разборщика — выход нулём.
20
+ # Сломанный диспетчер не имеет права заклинить работу.
21
+
22
+ set -u
23
+
24
+ here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
25
+ . "$here/utf8.sh" 2>/dev/null || true
26
+
27
+ event="${1:-}"
28
+ [ -z "$event" ] && exit 0
29
+
30
+ input="$(cat 2>/dev/null)"
31
+ [ -z "$input" ] && exit 0
32
+
33
+ # Разбор один на все ветки: четыре поля одним вызовом разборщика вместо шести на каждый гард.
34
+ # Значения приходят уже закавыченными для оболочки — `@sh` в разборщике для того и сделан:
35
+ # командная строка держит и кавычки, и переводы строк, и подставить её иначе нельзя.
36
+ assignments="$(printf '%s' "$input" | jq -r '@sh "RT_HOOK_TOOL=\(.tool_name // "") RT_HOOK_CMD=\(.tool_input.command // "") RT_HOOK_FILE=\(.tool_input.file_path // "") RT_HOOK_CWD=\(.cwd // "")"' 2>/dev/null)"
37
+ if [ -n "$assignments" ]; then
38
+ eval "$assignments" 2>/dev/null || true
39
+ export RT_HOOK_TOOL RT_HOOK_CMD RT_HOOK_FILE RT_HOOK_CWD
40
+ # Признак разбора: по нему ветки отличают готовое поле от пустой переменной, случайно
41
+ # оказавшейся в окружении прогона. Без него пустое значение читается как «поля нет».
42
+ export RT_HOOK_PARSED=1
43
+ fi
44
+ export RT_HOOK_INPUT="$input"
45
+
46
+ branches="$(grep -l '^# rt-hook:' "$here"/*.sh 2>/dev/null | sort)"
47
+ [ -z "$branches" ] && exit 0
48
+
49
+ for branch in $branches; do
50
+ declaration="$(sed -n 's/^# rt-hook:[[:space:]]*//p' "$branch" 2>/dev/null | head -1)"
51
+ [ -z "$declaration" ] && continue
52
+
53
+ branch_event="${declaration%% *}"
54
+ [ "$branch_event" = "$event" ] || continue
55
+
56
+ # Образец вызова: его нет вовсе — гард зовётся на любом; есть — сверяется с именем
57
+ # инструмента целиком, а не куском. Звёздочка и точка со звёздочкой значат одно: любой вызов.
58
+ matcher="${declaration#"$branch_event"}"
59
+ matcher="${matcher#"${matcher%%[![:space:]]*}"}"
60
+ if [ -n "$matcher" ] && [ "$matcher" != '*' ] && [ "$matcher" != '.*' ]; then
61
+ [[ "${RT_HOOK_TOOL:-}" =~ ^(${matcher})$ ]] || continue
62
+ fi
63
+
64
+ printf '%s' "$input" | bash "$branch"
65
+ code=$?
66
+ [ "$code" -ne 0 ] && exit "$code"
67
+ done
68
+
69
+ exit 0
@@ -26,11 +26,15 @@
26
26
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: не репозиторий, нет разборщика, битый ввод, пустой список файлов —
27
27
  # пропуск.
28
28
 
29
- input="$(cat 2>/dev/null)"
29
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
30
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
31
+
32
+ rt_hook_read
33
+ input="$RT_HOOK_INPUT"
30
34
  [ -z "$input" ] && exit 0
31
35
  command -v jq >/dev/null 2>&1 || exit 0
32
36
 
33
- tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
37
+ tool="$(rt_hook_tool)"
34
38
 
35
39
  decide() {
36
40
  # Наблюдение пишется только на отказе: подсказку гард раздаёт и там, где всё в порядке, и
@@ -115,7 +119,7 @@ case "$tool" in
115
119
  *) exit 0 ;;
116
120
  esac
117
121
 
118
- cmd="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null)"
122
+ cmd="$(rt_hook_cmd)"
119
123
 
120
124
  # Универсальный исполнитель передаёт настоящую команду вложенной строкой. Разбирать надо её,
121
125
  # иначе имя команды стоит сразу за кавычкой и ни одно правило до него не дотягивается.
@@ -144,7 +148,7 @@ if printf '%s' "$cmd" | grep -qiE '(^|#)[[:space:]]*Docs-skip:[[:space:]]*[^[:sp
144
148
  exit 0
145
149
  fi
146
150
 
147
- workdir="$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null)"
151
+ workdir="$(rt_hook_cwd)"
148
152
  [ -z "$workdir" ] && workdir="${CLAUDE_PROJECT_DIR:-.}"
149
153
  cd "$workdir" 2>/dev/null || exit 0
150
154
  git rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0
@@ -18,7 +18,11 @@
18
18
  # FAIL-OPEN: нет jq, нет записи хода, чужой инструмент → пропуск. Сломанный гард не должен
19
19
  # мешать работать.
20
20
 
21
- input="$(cat 2>/dev/null)"
21
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
22
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
23
+
24
+ rt_hook_read
25
+ input="$RT_HOOK_INPUT"
22
26
  [ -z "$input" ] && exit 0
23
27
  command -v jq >/dev/null 2>&1 || exit 0
24
28
 
@@ -30,14 +34,14 @@ rt_hooks_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
30
34
  [ -f "$rt_hooks_dir/roles.sh" ] && . "$rt_hooks_dir/roles.sh" 2>/dev/null
31
35
  command -v rt_role_off >/dev/null 2>&1 && rt_role_off strict-teacher && exit 0
32
36
 
33
- tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
37
+ tool="$(rt_hook_tool)"
34
38
  # Второй экзамен спрашивается на снятии черновика: работа кончилась, и правила поставки к этому
35
39
  # моменту читались давно — между их чтением и этой минутой прошёл весь заход.
36
40
  ready=0
37
41
  case "$tool" in
38
42
  Edit | Write | MultiEdit | mcp__webstorm__create_new_file) ;;
39
43
  Bash)
40
- cmd="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null)"
44
+ cmd="$(rt_hook_cmd)"
41
45
  printf '%s' "$cmd" | grep -qE 'pr[[:space:]]+ready|mr[[:space:]]+update[^|;&]*--ready' || exit 0
42
46
  ready=1
43
47
  ;;
@@ -10,6 +10,8 @@
10
10
  # Вынесен он отсюда потому, что гард дорос до предела длины: предметов в нём пять, и подпись
11
11
  # среди них самая отдельная — своя точка проверки, свои условия молчания, свой отказ.
12
12
 
13
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
14
+
13
15
  rt_delivery_signature() {
14
16
  # --- подпись машинного коммита ------------------------------------------------------------
15
17
  #
@@ -30,18 +30,24 @@
30
30
  # numbered); молчание значит «спросить некого»;
31
31
  # RT_TASK_NEW_CMD — чем заводится задача;
32
32
  # RT_BOARD_CHECK_CMD — чем сверяется очередь работ;
33
- # RT_TASK_BOT — учётная запись, которую ставят исполнителем;
33
+ # RT_TASK_BOT — учётная запись, которую ставят исполнителем; ею же открывается заявка;
34
+ # RT_PULL_TOKEN_VAR — переменная, которой вызову подставляют её токен;
35
+ # RT_PULL_TOKEN_HINT — готовая подстановка этого токена, целиком;
34
36
  # RT_COMMIT_EMAIL — почта, которой подписан машинный коммит; её же левой частью он и
35
37
  # опознаётся.
36
38
  # Отказ называет и то, что не так, и чем это чинится: отказ без действия обходят, а не исполняют.
37
39
  #
38
40
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: не репозиторий, нет разборщика, битый ввод, нет профиля — пропуск.
39
41
 
40
- input="$(cat 2>/dev/null)"
42
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
43
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
44
+
45
+ rt_hook_read
46
+ input="$RT_HOOK_INPUT"
41
47
  [ -z "$input" ] && exit 0
42
48
  command -v jq >/dev/null 2>&1 || exit 0
43
49
 
44
- tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
50
+ tool="$(rt_hook_tool)"
45
51
  sid="$(printf '%s' "$input" | jq -r '.session_id // "nosession"' 2>/dev/null)"
46
52
  case "$tool" in
47
53
  # Терминал среды и универсальный исполнитель кладут команду в то же поле.
@@ -49,7 +55,7 @@ case "$tool" in
49
55
  *) exit 0 ;;
50
56
  esac
51
57
 
52
- cmd="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null)"
58
+ cmd="$(rt_hook_cmd)"
53
59
  [ -z "$cmd" ] && exit 0
54
60
 
55
61
  # Универсальный исполнитель передаёт настоящую команду вложенной строкой. Разбирать надо её,
@@ -63,7 +69,7 @@ if [ "$tool" = "mcp__webstorm__execute_tool" ] && command -v perl >/dev/null 2>&
63
69
  [ -n "$inner" ] && cmd="$inner"
64
70
  fi
65
71
 
66
- workdir="$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null)"
72
+ workdir="$(rt_hook_cwd)"
67
73
  [ -z "$workdir" ] && workdir="${CLAUDE_PROJECT_DIR:-.}"
68
74
  cd "$workdir" 2>/dev/null || exit 0
69
75
  git rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0
@@ -96,6 +102,11 @@ task_move="${RT_TASK_MOVE_CMD:-npm run task:move}"
96
102
  # молча выключило бы проверку.
97
103
  backlog_column="${RT_BOARD_BACKLOG:-}"
98
104
  task_bot="${RT_TASK_BOT:-}"
105
+ # Личность вызова приходит окружением, а не словом в строке: из текста команды видно только то,
106
+ # подставлен ли токен явно. Дерево, не назвавшее переменной, автора заявки не судит вовсе —
107
+ # у него может не быть отдельной машинной записи.
108
+ pull_token_var="${RT_PULL_TOKEN_VAR:-}"
109
+ pull_token_hint="${RT_PULL_TOKEN_HINT:-}"
99
110
  commit_email="${RT_COMMIT_EMAIL:-}"
100
111
  tasks_dir="${RT_TASKS_DIR:-}"
101
112
  archive_dir="${RT_ARCHIVE_DIR:-}"
@@ -313,6 +324,16 @@ if printf '%s' "$cmd" | grep -qE '(^|[;&|(]|&&|\|\|)[[:space:]]*(gh[[:space:]]+p
313
324
  # и «ещё не посчитано» — не «конфликтует».
314
325
  printf '%s' "$pull" | jq -e '.conflicting' >/dev/null 2>&1 \
315
326
  && fault "заявка${pull_name} конфликтует с главной веткой. Влей её в свою ветку, разбери конфликт и повтори: снятый черновик читается как «можно вливать», а слить эту заявку нельзя."
327
+
328
+ # Автор заявки. На открытии судить было нечем, кроме текста команды: личность вызова
329
+ # приходит окружением. Здесь она уже названа хостингом, и это последний ход, где
330
+ # промах ещё исправим — после снятия черновика заявку вливают, а переоткрыть влитую
331
+ # нельзя. Дерево, не назвавшее машинной записи, автора не судит.
332
+ if [ -n "$task_bot" ]; then
333
+ pull_author="$(printf '%s' "$pull" | jq -r '.author // empty' 2>/dev/null)"
334
+ [ -n "$pull_author" ] && [ "$pull_author" != "$task_bot" ] \
335
+ && fault "заявку${pull_name} открыла запись «${pull_author}», а не машинная «${task_bot}». Автор заявки её ревьювером не бывает, и разбор ей назначить нечем. Автора не сменить — закрой заявку и открой заново${pull_token_hint:+, подставив токен: ${pull_token_hint} …}."
336
+ fi
316
337
  fi
317
338
  fi
318
339
  deny_faults
@@ -438,6 +459,19 @@ if [ -n "$remote_main" ] && [ -n "$local_main" ] && [ "$remote_main" != "$local_
438
459
  fault "твоя ссылка origin/${main_branch} отстала от удалённой — ${local_main:0:8} против ${remote_main:0:8}.${age} Гард сравнивает ветку с тем, что лежит в дереве, поэтому молчание первого яруса значит «ссылка не старше ветки», а не «главная ветка влита». Влей и повтори: git fetch origin && git merge origin/${main_branch}."
439
460
  fi
440
461
 
462
+ # Личность вызова. Клиент хостинга держит две записи сразу — залогиненную и ту, чей токен стоит
463
+ # в окружении вызова, — и какая из них откроет заявку, из текста команды не видно ничем, кроме
464
+ # явной подстановки. Читающие вызовы идут от залогиненной и работают, поэтому пишущий выглядит
465
+ # таким же обычным; промах всплывает шагом позже, на назначении ревьювера: автор заявки её
466
+ # ревьювером не бывает. Чинится это только переоткрытием — автора у заявки не сменить.
467
+ #
468
+ # Судится текст команды, а не ответ хостинга: спросить, чьим токеном пойдёт вызов, до его
469
+ # исполнения нельзя. Дерево, не назвавшее переменной токена, требования не получает.
470
+ if [ -n "$pull_token_var" ] \
471
+ && ! printf '%s' "$cmd" | grep -qE "(^|[;&|(]|&&|\|\||[[:space:]])${pull_token_var}="; then
472
+ fault "заявка открывается без токена машинной записи: в команде нет подстановки «${pull_token_var}». Открытая залогиненной записью, она выйдет от владельца — ревьювером его тогда не назначить, и чинится это только переоткрытием.${pull_token_hint:+ Подставь токен: ${pull_token_hint} …}"
473
+ fi
474
+
441
475
  check_task "$number" "заявка с ветки «${branch}»" да
442
476
 
443
477
  # Всё несошедшееся названо здесь, разом: до этой строки собирались условия, каждое из которых
@@ -13,10 +13,14 @@
13
13
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: не репозиторий, нет гита, открепившийся HEAD, битый ввод — пропуск.
14
14
  # Сломанный гард не должен мешать работать.
15
15
 
16
- input="$(cat 2>/dev/null)"
16
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
17
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
18
+
19
+ rt_hook_read
20
+ input="$RT_HOOK_INPUT"
17
21
  [ -z "$input" ] && exit 0
18
22
 
19
- tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
23
+ tool="$(rt_hook_tool)"
20
24
  # Терминал среды разработки исполняет ту же командную строку и кладёт её в то же поле. Пока
21
25
  # гард проверял только оболочку, весь его смысл обходился сменой инструмента.
22
26
  case "$tool" in
@@ -24,7 +28,7 @@ case "$tool" in
24
28
  *) exit 0 ;;
25
29
  esac
26
30
 
27
- cmd="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null)"
31
+ cmd="$(rt_hook_cmd)"
28
32
 
29
33
  # Универсальный исполнитель среды передаёт настоящую команду вложенной строкой. Разбирать надо
30
34
  # её, а не обёртку: иначе имя команды стоит сразу за кавычкой и ни одно правило до него не
@@ -44,7 +48,7 @@ esac
44
48
 
45
49
  # Коммит выполнится в рабочем каталоге вызова, поэтому и ветку смотрим там же; корень проекта
46
50
  # — запасной вариант, и он важен для отдельного рабочего дерева, где ветка своя.
47
- workdir="$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null)"
51
+ workdir="$(rt_hook_cwd)"
48
52
  [ -z "$workdir" ] && workdir="${CLAUDE_PROJECT_DIR:-.}"
49
53
  cd "$workdir" 2>/dev/null || exit 0
50
54