@rt-tools/agent-kit 0.11.0 → 0.13.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 (138) hide show
  1. package/assets/checks/board-runs.github.mjs +87 -0
  2. package/assets/checks/board.github.mjs +0 -40
  3. package/assets/checks/check-board.github.mjs +39 -8
  4. package/assets/checks/check-file-size.mjs +19 -4
  5. package/assets/checks/check-schema-drift.mjs +28 -5
  6. package/assets/checks/check-state-next.mjs +10 -2
  7. package/assets/checks/rt-kit-checks.config.mjs +28 -2
  8. package/assets/commands/feedback.md +8 -0
  9. package/assets/defaults/project.sh +59 -6
  10. package/assets/defaults/turn-map.md +8 -6
  11. package/assets/hooks/browser-guard-device-id.sh +3 -1
  12. package/assets/hooks/browser-guard-no-asking.sh +3 -1
  13. package/assets/hooks/browser-guard-no-other-drivers.sh +6 -4
  14. package/assets/hooks/browser-guard-require-select.sh +4 -2
  15. package/assets/hooks/claim-guard.sh +3 -1
  16. package/assets/hooks/conscience-guard.sh +3 -1
  17. package/assets/hooks/dev-server-guard.sh +6 -4
  18. package/assets/hooks/dispatch.sh +69 -0
  19. package/assets/hooks/docs-guard.sh +6 -4
  20. package/assets/hooks/exam-guard.sh +5 -3
  21. package/assets/hooks/git-guard-delivery-folder.sh +99 -0
  22. package/assets/hooks/git-guard-delivery-signature.sh +10 -4
  23. package/assets/hooks/git-guard-delivery.sh +58 -74
  24. package/assets/hooks/git-guard-main.sh +6 -4
  25. package/assets/hooks/git-guard-push-tests.sh +8 -6
  26. package/assets/hooks/grill-gate.sh +4 -2
  27. package/assets/hooks/handoff-entry-guard.sh +4 -2
  28. package/assets/hooks/handoff-write.sh +27 -6
  29. package/assets/hooks/hook-input.sh +77 -0
  30. package/assets/hooks/lint-after-edit.sh +5 -3
  31. package/assets/hooks/override-write-guard.sh +107 -0
  32. package/assets/hooks/postmortem-guard.sh +3 -1
  33. package/assets/hooks/proposal-guard.sh +3 -1
  34. package/assets/hooks/prose-style-guard.sh +5 -3
  35. package/assets/hooks/qa-dataid-guard.sh +4 -2
  36. package/assets/hooks/rerun-guard.sh +6 -4
  37. package/assets/hooks/reuse-first-guard.sh +5 -3
  38. package/assets/hooks/rule-article.sh +99 -0
  39. package/assets/hooks/rule-source-guard.sh +126 -0
  40. package/assets/hooks/skill-gate-rearm.sh +3 -1
  41. package/assets/hooks/skill-gate.sh +23 -2
  42. package/assets/hooks/skill-loaded.sh +3 -1
  43. package/assets/hooks/sql-guard-request.sh +2 -1
  44. package/assets/hooks/sql-guard.sh +4 -2
  45. package/assets/hooks/task-flow-guard.sh +24 -4
  46. package/assets/hooks/turn-exit-guard.sh +59 -17
  47. package/assets/hooks/waiting-turn-guard.sh +3 -1
  48. package/assets/hooks/window-fill-guard.sh +6 -4
  49. package/assets/laws/work-conduct.md +5 -9
  50. package/assets/patterns/dependencies-upgrade.md +1 -1
  51. package/assets/patterns/doc-style-write.md +3 -3
  52. package/assets/patterns/git-workflow-commit.azure.md +2 -202
  53. package/assets/patterns/git-workflow-commit.github.md +2 -258
  54. package/assets/patterns/git-workflow-commit.gitlab.md +1 -217
  55. package/assets/patterns/git-workflow-docker.md +3 -3
  56. package/assets/patterns/git-workflow-merge.md +3 -2
  57. package/assets/patterns/git-workflow-migration.md +3 -3
  58. package/assets/patterns/git-workflow-pr.azure.md +224 -0
  59. package/assets/patterns/git-workflow-pr.github.md +280 -0
  60. package/assets/patterns/git-workflow-pr.gitlab.md +240 -0
  61. package/assets/patterns/git-workflow-restart.md +3 -3
  62. package/assets/patterns/git-workflow-secrets.md +3 -3
  63. package/assets/patterns/task-flow-archive.md +195 -0
  64. package/assets/patterns/task-flow-close.md +72 -240
  65. package/assets/patterns/task-flow-handoff.md +4 -4
  66. package/assets/patterns/task-flow-resume.md +5 -3
  67. package/assets/pitfalls/agent-kit.md +80 -0
  68. package/assets/pitfalls/doc-style.md +80 -0
  69. package/assets/pitfalls/git-workflow.azure.md +50 -0
  70. package/assets/pitfalls/git-workflow.github.md +78 -0
  71. package/assets/pitfalls/git-workflow.gitlab.md +49 -0
  72. package/assets/pitfalls/spec-driven.md +36 -0
  73. package/assets/pitfalls/styling-bem.md +45 -0
  74. package/assets/pitfalls/task-flow.md +62 -0
  75. package/assets/pitfalls/testing.md +70 -0
  76. package/assets/rules/deploy-flow.azure.md +114 -0
  77. package/assets/rules/deploy-flow.github.md +122 -0
  78. package/assets/rules/deploy-flow.gitlab.md +116 -0
  79. package/assets/rules/doc-style.md +25 -76
  80. package/assets/rules/git-workflow.azure.md +6 -92
  81. package/assets/rules/git-workflow.github.md +20 -128
  82. package/assets/rules/git-workflow.gitlab.md +6 -93
  83. package/assets/rules/spec-driven.md +39 -30
  84. package/assets/rules/styling-bem.md +20 -39
  85. package/assets/rules/task-flow.md +56 -222
  86. package/assets/rules/testing.md +3 -64
  87. package/assets/rules/turn-conduct.md +210 -0
  88. package/assets/rules/typescript-conventions.md +15 -0
  89. package/assets/skills/agent-kit.md +67 -89
  90. package/assets/templates/pitfalls.md +10 -0
  91. package/assets/templates/proposal.md +16 -1
  92. package/assets/templates/rule.md +5 -3
  93. package/bin/agent-kit.d.ts.map +1 -1
  94. package/bin/agent-kit.js +1 -42
  95. package/bin/agent-kit.js.map +1 -1
  96. package/lib/assets.d.ts.map +1 -1
  97. package/lib/assets.js +6 -1
  98. package/lib/assets.js.map +1 -1
  99. package/lib/cascade.d.ts.map +1 -1
  100. package/lib/cascade.js +19 -1
  101. package/lib/cascade.js.map +1 -1
  102. package/lib/commands.d.ts.map +1 -1
  103. package/lib/commands.js +1 -0
  104. package/lib/commands.js.map +1 -1
  105. package/lib/config.d.ts +16 -1
  106. package/lib/config.d.ts.map +1 -1
  107. package/lib/config.js +8 -0
  108. package/lib/config.js.map +1 -1
  109. package/lib/hooks-map.d.ts +13 -0
  110. package/lib/hooks-map.d.ts.map +1 -1
  111. package/lib/hooks-map.js +33 -1
  112. package/lib/hooks-map.js.map +1 -1
  113. package/lib/proposals.d.ts +5 -1
  114. package/lib/proposals.d.ts.map +1 -1
  115. package/lib/proposals.js +74 -5
  116. package/lib/proposals.js.map +1 -1
  117. package/lib/ship.d.ts +1 -2
  118. package/lib/ship.d.ts.map +1 -1
  119. package/lib/ship.js +0 -54
  120. package/lib/ship.js.map +1 -1
  121. package/lib/shipment.d.ts.map +1 -1
  122. package/lib/shipment.fixture.d.ts +39 -0
  123. package/lib/shipment.fixture.d.ts.map +1 -0
  124. package/lib/shipment.fixture.js +99 -0
  125. package/lib/shipment.fixture.js.map +1 -0
  126. package/lib/shipment.js +59 -7
  127. package/lib/shipment.js.map +1 -1
  128. package/package.json +1 -1
  129. package/rt-tools-agent-kit-0.13.0.tgz +0 -0
  130. package/assets/commands/agent-kit-digest.md +0 -89
  131. package/assets/commands/rules-review.md +0 -98
  132. package/assets/patterns/cargo-triage-mark.md +0 -119
  133. package/assets/rules/cargo-triage.md +0 -126
  134. package/lib/cargo-state.d.ts +0 -62
  135. package/lib/cargo-state.d.ts.map +0 -1
  136. package/lib/cargo-state.js +0 -118
  137. package/lib/cargo-state.js.map +0 -1
  138. package/rt-tools-agent-kit-0.11.0.tgz +0 -0
@@ -26,8 +26,10 @@
26
26
  # тексте ход РАЗРЕШАЕТСЯ (exit 0). Сломанный гард не имеет права заклинить разговор.
27
27
 
28
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
29
30
 
30
- input="$(cat 2>/dev/null)"
31
+ rt_hook_read
32
+ input="$RT_HOOK_INPUT"
31
33
  [ -z "$input" ] && exit 0
32
34
  command -v jq >/dev/null 2>&1 || exit 0
33
35
 
@@ -20,8 +20,10 @@
20
20
  # разрешается. Сломанная совесть не имеет права заклинить разговор.
21
21
 
22
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
23
24
 
24
- input="$(cat 2>/dev/null)"
25
+ rt_hook_read
26
+ input="$RT_HOOK_INPUT"
25
27
  [ -z "$input" ] && exit 0
26
28
  command -v jq >/dev/null 2>&1 || exit 0
27
29
 
@@ -15,10 +15,12 @@
15
15
  # RT_STANDS. Нет профиля — текст отказа остаётся общим, сам гард работает.
16
16
 
17
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
- input="$(cat 2>/dev/null)"
20
+ rt_hook_read
21
+ input="$RT_HOOK_INPUT"
20
22
 
21
- tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
23
+ tool="$(rt_hook_tool)"
22
24
  case "$tool" in
23
25
  Bash | mcp__webstorm__execute_terminal_command | mcp__webstorm__execute_tool) ;;
24
26
  # Готовая конфигурация запуска командной строки не показывает — видно только её имя.
@@ -48,7 +50,7 @@ case "$tool" in
48
50
  *) exit 0 ;;
49
51
  esac
50
52
 
51
- cmd="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null)"
53
+ cmd="$(rt_hook_cmd)"
52
54
  [ -z "$cmd" ] && exit 0
53
55
 
54
56
  # Универсальный исполнитель среды передаёт настоящую команду вложенной строкой. Разбирать надо
@@ -98,7 +100,7 @@ RUNNER='((npx|pnpm|yarn|bun|npm)([[:space:]]+(exec|run|dlx))?[[:space:]]+)?'
98
100
 
99
101
  # Начало вызова: начало строки или разделитель команд. Кавычку в границы вносить нельзя — тогда
100
102
  # поиск по тексту и снятие процесса по шаблону читаются как запуск.
101
- BOUND='(^|[;&|(]|&&|\|\|)[[:space:]]*'
103
+ BOUND="$RT_CMD_BOUND"
102
104
 
103
105
  printf '%s' "$cmd" | grep -qE "${BOUND}${RUNNER}(nx|ng)[[:space:]]+(run[[:space:]]+[^[:space:]]*:)?(serve|dev)" \
104
106
  && deny "Запуск ещё одного сервера разработки через каркас."
@@ -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
@@ -27,12 +27,14 @@
27
27
  # пропуск.
28
28
 
29
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
30
31
 
31
- input="$(cat 2>/dev/null)"
32
+ rt_hook_read
33
+ input="$RT_HOOK_INPUT"
32
34
  [ -z "$input" ] && exit 0
33
35
  command -v jq >/dev/null 2>&1 || exit 0
34
36
 
35
- tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
37
+ tool="$(rt_hook_tool)"
36
38
 
37
39
  decide() {
38
40
  # Наблюдение пишется только на отказе: подсказку гард раздаёт и там, где всё в порядке, и
@@ -117,7 +119,7 @@ case "$tool" in
117
119
  *) exit 0 ;;
118
120
  esac
119
121
 
120
- cmd="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null)"
122
+ cmd="$(rt_hook_cmd)"
121
123
 
122
124
  # Универсальный исполнитель передаёт настоящую команду вложенной строкой. Разбирать надо её,
123
125
  # иначе имя команды стоит сразу за кавычкой и ни одно правило до него не дотягивается.
@@ -146,7 +148,7 @@ if printf '%s' "$cmd" | grep -qiE '(^|#)[[:space:]]*Docs-skip:[[:space:]]*[^[:sp
146
148
  exit 0
147
149
  fi
148
150
 
149
- workdir="$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null)"
151
+ workdir="$(rt_hook_cwd)"
150
152
  [ -z "$workdir" ] && workdir="${CLAUDE_PROJECT_DIR:-.}"
151
153
  cd "$workdir" 2>/dev/null || exit 0
152
154
  git rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0
@@ -19,8 +19,10 @@
19
19
  # мешать работать.
20
20
 
21
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
22
23
 
23
- input="$(cat 2>/dev/null)"
24
+ rt_hook_read
25
+ input="$RT_HOOK_INPUT"
24
26
  [ -z "$input" ] && exit 0
25
27
  command -v jq >/dev/null 2>&1 || exit 0
26
28
 
@@ -32,14 +34,14 @@ rt_hooks_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
32
34
  [ -f "$rt_hooks_dir/roles.sh" ] && . "$rt_hooks_dir/roles.sh" 2>/dev/null
33
35
  command -v rt_role_off >/dev/null 2>&1 && rt_role_off strict-teacher && exit 0
34
36
 
35
- tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
37
+ tool="$(rt_hook_tool)"
36
38
  # Второй экзамен спрашивается на снятии черновика: работа кончилась, и правила поставки к этому
37
39
  # моменту читались давно — между их чтением и этой минутой прошёл весь заход.
38
40
  ready=0
39
41
  case "$tool" in
40
42
  Edit | Write | MultiEdit | mcp__webstorm__create_new_file) ;;
41
43
  Bash)
42
- cmd="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null)"
44
+ cmd="$(rt_hook_cmd)"
43
45
  printf '%s' "$cmd" | grep -qE 'pr[[:space:]]+ready|mr[[:space:]]+update[^|;&]*--ready' || exit 0
44
46
  ready=1
45
47
  ;;
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env bash
2
+ # Условия поставки о папке задачи. НЕ гард: объявления `rt-hook:` у него нет, к событиям агента
3
+ # он не подключается. Его источает гард поставки — тем же приёмом, каким источает хвост отказа и
4
+ # запись наблюдений.
5
+ #
6
+ # Зачем он отдельным файлом. Условий о папке два, и живут они в разных местах вызова: одно на
7
+ # открытии заявки, другое на слиянии. Вместе с разбором команды, подписью, основанием и очередью
8
+ # работ они переросли предел длины файла, а делить гард по событиям нельзя — он одно целое,
9
+ # и разбор команды у него общий. Делится он по предмету: папка задачи — свой предмет, у неё свой
10
+ # обход и своя запись в архиве.
11
+ #
12
+ # ЧТО ЗДЕСЬ. Три вызова, и все три зовутся из гарда поставки, где уже объявлены `deny`, `fault`
13
+ # и переменные профиля: `tasks_dir`, `archive_dir`, `main_branch`, `folder_skip_re`, `root`.
14
+ # Своих у помощника нет — он не читает ни ввода, ни настроек.
15
+
16
+ # Есть ли папка задачи в ветке. Смотрим содержимое ветки, а не рабочее дерево: если папку
17
+ # удалили, но не закоммитили, проверка прошла бы, а папка всё равно уехала бы в главную. Имя
18
+ # ветки подставляем целиком, вместе с косой: у ветки вида `chore/312-slug` папка лежит во
19
+ # вложенном каталоге.
20
+ rt_folder_in_branch() {
21
+ git ls-tree -d --name-only HEAD -- "$1" 2>/dev/null | head -1
22
+ }
23
+
24
+ # Была ли папка задачи в этой ветке вообще. Спрашивается у ветки, которая её удалила: иначе
25
+ # требование записи в архиве цеплялось бы к работе, у которой папки и не было.
26
+ rt_folder_was_in_branch() {
27
+ _base="$1"
28
+ _folder="$2"
29
+ _had="$(git ls-tree -d --name-only "$_base" -- "$_folder" 2>/dev/null | head -1)"
30
+ [ -z "$_had" ] && _had="$(git log "$_base..HEAD" --diff-filter=A --name-only --pretty=format: -- "$_folder" 2>/dev/null | head -1)"
31
+ [ -n "$_had" ]
32
+ }
33
+
34
+ # Условие открытия заявки: папка разобрана, и запись в архиве от ветки прибыла. Отказ копится
35
+ # вместе с прочими условиями поставки и печатается разом.
36
+ #
37
+ # Разбор стоит здесь, а не на слиянии. Прежде было наоборот: считалось, что папка нужна на диске
38
+ # всё время разбора, потому что правку по замечаниям без замысла отбивает гард хода работы.
39
+ # Владелец же вливает, как только видит зелёное, и закрывающему коммиту места не остаётся —
40
+ # трижды подряд папка уехала в главную неразобранной, а гард слияния этого не видел вовсе:
41
+ # кнопку нажимает человек на хостинге, и до него гард не достаёт. Замысел после уборки гард хода
42
+ # работы берёт из истории ветки, а не с диска.
43
+ rt_delivery_open_folder() {
44
+ [ -n "$tasks_dir" ] || return 0
45
+ printf '%s' "$cmd" | grep -qiE "$folder_skip_re" && return 0
46
+
47
+ _folder="$tasks_dir/$branch"
48
+ _lying="$(rt_folder_in_branch "$_folder")"
49
+ if [ -n "$_lying" ]; then
50
+ fault "в ветке лежит папка задачи «${_lying}» — заявка открывается после уборки, а не до неё. Перенеси в «${archive_dir:-архив}» то, что объясняет принятые решения, остальное удали, закоммить последним коммитом и повтори. Если работа вливается частями, поставь в команду комментарий «# Task-folder-skip: <причина>»."
51
+ return 0
52
+ fi
53
+
54
+ [ -n "$archive_dir" ] || return 0
55
+ _base="$(git merge-base "$main_branch" HEAD 2>/dev/null)"
56
+ [ -z "$_base" ] && return 0
57
+ rt_folder_was_in_branch "$_base" "$_folder" || return 0
58
+
59
+ _gained="$(git diff --name-only --diff-filter=A "$_base" HEAD -- "$archive_dir" 2>/dev/null | head -1)"
60
+ [ -z "$_gained" ] \
61
+ && fault "папку задачи удалили, но в «${archive_dir}» ветка ничего не добавила. Удалить проще, чем разобрать, — и вместе с папкой пропадает разбор просьбы, единственная запись слов владельца. Перенеси то, что объясняет принятые решения, одним файлом с понятным именем и повтори."
62
+ }
63
+
64
+ # Условие слияния: тот же предмет вторым рубежом. Он ловит слияние, идущее командой, — то, до
65
+ # которого гард достаёт. Отсюда выходят только отказом или молчанием: слияние решается целиком.
66
+ rt_delivery_merge_folder() {
67
+ [ -n "$tasks_dir" ] || exit 0 # ведения работы папкой в дереве нет
68
+
69
+ _branch="$(git branch --show-current 2>/dev/null)"
70
+ [ -z "$_branch" ] && exit 0
71
+ rt_task_branch_ok "$_branch" || exit 0 # за беззадачной веткой папки не стоит
72
+
73
+ _folder="$tasks_dir/$_branch"
74
+
75
+ # Сначала ищем обход в самой команде — это работает и без сети. Если читать только
76
+ # тело заявки, то без сети гард отбил бы слияние, причина которого в этом теле и написана.
77
+ printf '%s' "$cmd" | grep -qiE "$folder_skip_re" && exit 0
78
+
79
+ _number="$(printf '%s' "$cmd" | sed -nE 's/.*(pr|mr)[[:space:]]+(merge|update)[[:space:]]+([0-9]+).*/\3/p' | head -1)"
80
+ if [ -n "$_number" ] && rt_needs rt_report_body git-guard-delivery; then
81
+ _body="$(cd "$root" && rt_report_body "$_number" 2>/dev/null)"
82
+ [ -n "$_body" ] && printf '%s' "$_body" | grep -qiE "$folder_skip_re" && exit 0
83
+ fi
84
+
85
+ _lying="$(rt_folder_in_branch "$_folder")"
86
+ [ -n "$_lying" ] \
87
+ && deny "BLOCKED: в ветке осталась папка задачи «${_lying}» — она уедет в главную. Разобрать её потом будет некому: работа перейдёт к следующей задаче, а эта заявка закроется. Перенеси в «${archive_dir:-архив}» то, что объясняет принятые решения, остальное удали и повтори. Если работа вливается частями, поставь в тело заявки строку «Task-folder-skip: <причина>»."
88
+
89
+ [ -n "$archive_dir" ] || exit 0
90
+ _base="$(git merge-base "$main_branch" HEAD 2>/dev/null)"
91
+ [ -z "$_base" ] && exit 0
92
+ rt_folder_was_in_branch "$_base" "$_folder" || exit 0
93
+
94
+ _gained="$(git diff --name-only --diff-filter=A "$_base" HEAD -- "$archive_dir" 2>/dev/null | head -1)"
95
+ [ -z "$_gained" ] \
96
+ && deny "BLOCKED: папку задачи удалили, но в «${archive_dir}» ветка ничего не добавила. Удалить проще, чем разобрать, — и вместе с папкой пропадает разбор просьбы, единственная запись слов владельца. Перенеси то, что объясняет принятые решения, одним файлом с понятным именем и повтори."
97
+
98
+ exit 0
99
+ }
@@ -11,6 +11,10 @@
11
11
  # среди них самая отдельная — своя точка проверки, свои условия молчания, свой отказ.
12
12
 
13
13
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
14
+ # Начало вызова помощник берёт оттуда же, откуда все гарды. Источается он и здесь, хотя зовущий
15
+ # гард это уже сделал: пустой образец превратил бы признак в поиск слова по всей строке, то есть
16
+ # в отказ на каждом упоминании команды.
17
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
14
18
 
15
19
  rt_delivery_signature() {
16
20
  # --- подпись машинного коммита ------------------------------------------------------------
@@ -33,13 +37,15 @@ rt_delivery_signature() {
33
37
  # логина разошлись бы молча. Исполнителем задачи он тоже не бывает — в дереве, где хостинг
34
38
  # ограничил машинную запись, исполнителем ставят человека, а коммит остаётся машинным.
35
39
  #
36
- # Вызов пуша узнаётся по двум признакам сразу — команда `git` в начале строки или за
37
- # разделителем и слово `push` отдельным словом. Одной подстрокой «git push» его не поймать:
38
- # помощник учётных данных и заголовок запроса ставятся ключами `-c` между ними.
40
+ # Вызов пуша узнаётся по двум признакам сразу — начало вызова перед словом `git` и слово `push`
41
+ # отдельным словом. Одной подстрокой «git push» его не поймать: помощник учётных данных и
42
+ # заголовок запроса ставятся ключами `-c` между ними. Начало вызова считает присваивания
43
+ # переменных частью команды: пуш подписанного коммита набирают с подстановкой токена, и признак
44
+ # без них пропускал ровно тот вызов, ради которого этот помощник и заведён.
39
45
  # Пробный пуш не отправляет ничего, и подпись у него не спрашивают. Отложенная правка — тоже:
40
46
  # `git stash push` кладёт правку в тайник этой же машины, поэтому тайник вырезается из строки, а
41
47
  # признак считается по остатку.
42
- if printf '%s' "$cmd" | grep -qE '(^|[;&|(]|&&|\|\|)[[:space:]]*git([[:space:]]|$)' \
48
+ if printf '%s' "$cmd" | grep -qE "${RT_CMD_BOUND}git([[:space:]]|\$)" \
43
49
  && printf '%s' "$cmd" | sed -E 's/git[[:space:]]+stash[[:space:]]+push/git stash/g' | grep -qE '(^|[[:space:]])push([[:space:]]|$)' \
44
50
  && ! printf '%s' "$cmd" | grep -q -- '--dry-run'; then
45
51
  # Два условия молчания. Дерево, не назвавшее почты машинной записи, требования не получает:
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bash
2
2
  # rt-hook: PreToolUse Bash|mcp__webstorm__execute_terminal_command|mcp__webstorm__execute_tool
3
- # Требует: hooks/profile-check.sh, hooks/deny-tail.sh
3
+ # Требует: hooks/git-guard-delivery-folder.sh, hooks/profile-check.sh, hooks/deny-tail.sh
4
4
  # Гард поставки. PreToolUse на заведении ветки, пуше и открытии заявки на слияние.
5
5
  #
6
6
  # Закон о поставке требует трёх вещей, которых обычно не проверяет ничто: правка начинается с
@@ -30,7 +30,9 @@
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
  # Отказ называет и то, что не так, и чем это чинится: отказ без действия обходят, а не исполняют.
@@ -38,12 +40,14 @@
38
40
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: не репозиторий, нет разборщика, битый ввод, нет профиля — пропуск.
39
41
 
40
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
41
44
 
42
- input="$(cat 2>/dev/null)"
45
+ rt_hook_read
46
+ input="$RT_HOOK_INPUT"
43
47
  [ -z "$input" ] && exit 0
44
48
  command -v jq >/dev/null 2>&1 || exit 0
45
49
 
46
- tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
50
+ tool="$(rt_hook_tool)"
47
51
  sid="$(printf '%s' "$input" | jq -r '.session_id // "nosession"' 2>/dev/null)"
48
52
  case "$tool" in
49
53
  # Терминал среды и универсальный исполнитель кладут команду в то же поле.
@@ -51,7 +55,7 @@ case "$tool" in
51
55
  *) exit 0 ;;
52
56
  esac
53
57
 
54
- cmd="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null)"
58
+ cmd="$(rt_hook_cmd)"
55
59
  [ -z "$cmd" ] && exit 0
56
60
 
57
61
  # Универсальный исполнитель передаёт настоящую команду вложенной строкой. Разбирать надо её,
@@ -65,7 +69,7 @@ if [ "$tool" = "mcp__webstorm__execute_tool" ] && command -v perl >/dev/null 2>&
65
69
  [ -n "$inner" ] && cmd="$inner"
66
70
  fi
67
71
 
68
- workdir="$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null)"
72
+ workdir="$(rt_hook_cwd)"
69
73
  [ -z "$workdir" ] && workdir="${CLAUDE_PROJECT_DIR:-.}"
70
74
  cd "$workdir" 2>/dev/null || exit 0
71
75
  git rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0
@@ -98,6 +102,11 @@ task_move="${RT_TASK_MOVE_CMD:-npm run task:move}"
98
102
  # молча выключило бы проверку.
99
103
  backlog_column="${RT_BOARD_BACKLOG:-}"
100
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:-}"
101
110
  commit_email="${RT_COMMIT_EMAIL:-}"
102
111
  tasks_dir="${RT_TASKS_DIR:-}"
103
112
  archive_dir="${RT_ARCHIVE_DIR:-}"
@@ -143,6 +152,12 @@ fault() {
143
152
  }— $1"
144
153
  }
145
154
 
155
+ # Условия о папке задачи вынесены соседним файлом: у папки свой предмет, свой обход и своя
156
+ # запись в архиве, а вместе с разбором команды и очередью работ они переросли предел длины.
157
+ # Подключается он после `deny` и `fault` — обе зовутся изнутри.
158
+ # shellcheck disable=SC1090
159
+ [ -f "$rt_hooks_dir/git-guard-delivery-folder.sh" ] && . "$rt_hooks_dir/git-guard-delivery-folder.sh" 2>/dev/null
160
+
146
161
  # Отказ по накопленному. Пусто — вызывающая сторона идёт дальше.
147
162
  deny_faults() {
148
163
  [ -z "$faults" ] && return 0
@@ -152,21 +167,6 @@ deny_faults() {
152
167
  ${faults}"
153
168
  }
154
169
 
155
- # Подсказка вместо отказа: на открытии PR папка ещё нужна. Решения подсказка не несёт,
156
- # команда идёт дальше своим ходом.
157
- hint() {
158
- jq -n --arg c "$1" '{hookSpecificOutput:{hookEventName:"PreToolUse",additionalContext:$c}}' 2>/dev/null
159
- exit 0
160
- }
161
-
162
- # Есть ли папка задачи в ветке. Смотрим содержимое ветки, а не рабочее дерево: если папку
163
- # удалили, но не закоммитили, проверка прошла бы, а папка всё равно уехала бы в main. Имя
164
- # ветки подставляем целиком, вместе с косой: у ветки вида `chore/312-slug` папка лежит во
165
- # вложенном каталоге.
166
- folder_in_branch() {
167
- git ls-tree -d --name-only HEAD -- "$1" 2>/dev/null | head -1
168
- }
169
-
170
170
  check_task() {
171
171
  number="$1"
172
172
  where="$2"
@@ -283,7 +283,7 @@ command -v rt_delivery_signature >/dev/null 2>&1 && rt_delivery_signature
283
283
  #
284
284
  # Возврат заявки в черновик под требование не подпадает: он делает ровно то, чего гард и
285
285
  # добивается, — снимает с работы вид готовой.
286
- if printf '%s' "$cmd" | grep -qE '(^|[;&|(]|&&|\|\|)[[:space:]]*(gh[[:space:]]+pr[[:space:]]+ready|glab[[:space:]]+mr[[:space:]]+update[^|;&]*--ready)([[:space:]]|$)' \
286
+ if printf '%s' "$cmd" | grep -qE "${RT_CMD_BOUND}(gh[[:space:]]+pr[[:space:]]+ready|glab[[:space:]]+mr[[:space:]]+update[^|;&]*--ready)([[:space:]]|\$)" \
287
287
  && ! printf '%s' "$cmd" | grep -q -- '--undo'; then
288
288
  # Ссылка на заявку необязательна: без неё клиент берёт заявку текущей ветки, и это самая
289
289
  # короткая форма вызова. Требовать номер значило бы снимать всё требование одним пробелом.
@@ -315,6 +315,16 @@ if printf '%s' "$cmd" | grep -qE '(^|[;&|(]|&&|\|\|)[[:space:]]*(gh[[:space:]]+p
315
315
  # и «ещё не посчитано» — не «конфликтует».
316
316
  printf '%s' "$pull" | jq -e '.conflicting' >/dev/null 2>&1 \
317
317
  && fault "заявка${pull_name} конфликтует с главной веткой. Влей её в свою ветку, разбери конфликт и повтори: снятый черновик читается как «можно вливать», а слить эту заявку нельзя."
318
+
319
+ # Автор заявки. На открытии судить было нечем, кроме текста команды: личность вызова
320
+ # приходит окружением. Здесь она уже названа хостингом, и это последний ход, где
321
+ # промах ещё исправим — после снятия черновика заявку вливают, а переоткрыть влитую
322
+ # нельзя. Дерево, не назвавшее машинной записи, автора не судит.
323
+ if [ -n "$task_bot" ]; then
324
+ pull_author="$(printf '%s' "$pull" | jq -r '.author // empty' 2>/dev/null)"
325
+ [ -n "$pull_author" ] && [ "$pull_author" != "$task_bot" ] \
326
+ && fault "заявку${pull_name} открыла запись «${pull_author}», а не машинная «${task_bot}». Автор заявки её ревьювером не бывает, и разбор ей назначить нечем. Автора не сменить — закрой заявку и открой заново${pull_token_hint:+, подставив токен: ${pull_token_hint} …}."
327
+ fi
318
328
  fi
319
329
  fi
320
330
  deny_faults
@@ -322,59 +332,20 @@ fi
322
332
 
323
333
  # --- слияние заявки ----------------------------------------------------------------------
324
334
  #
325
- # Папку задачи разбирают тем же PR, что и работу. После слияния этого уже никто не сделает:
326
- # работа перешла к следующей задаче, а PR закрыт. Раньше слияния требовать нельзя — пока идёт
327
- # ревью, plan.md нужен на диске, иначе гард хода работы не даст править код.
335
+ # Второй рубеж того же условия, что стоит на открытии заявки: он ловит слияние, идущее командой.
328
336
  # Команду ищем от начала строки или после разделителя, а не где угодно в тексте. Иначе гард
329
337
  # отбивает сообщение, где `gh pr merge` просто упомянут в кавычках, — так он и сработал на
330
338
  # правке этого же текста. Полностью подстроку в кавычках так не отсечь, но случайное упоминание
331
339
  # внутри слова или пути мимо уже не пройдёт.
332
- if printf '%s' "$cmd" | grep -qE '(^|[;&|(]|&&|\|\|)[[:space:]]*(gh[[:space:]]+pr[[:space:]]+merge|glab[[:space:]]+mr[[:space:]]+merge|az[[:space:]]+repos[[:space:]]+pr[[:space:]]+update)([[:space:]]|$)'; then
333
- [ -n "$tasks_dir" ] || exit 0 # ведения работы папкой в дереве нет
334
-
335
- merge_branch="$(git branch --show-current 2>/dev/null)"
336
- [ -z "$merge_branch" ] && exit 0
337
- rt_task_branch_ok "$merge_branch" || exit 0 # за беззадачной веткой папки не стоит
338
-
339
- folder="$tasks_dir/$merge_branch"
340
-
341
- # Сначала ищем обход в самой команде — это работает и без сети. Если читать только
342
- # тело PR, то без сети гард отбил бы слияние, причина которого в этом теле и написана.
343
- printf '%s' "$cmd" | grep -qiE "$folder_skip_re" && exit 0
344
-
345
- merge_number="$(printf '%s' "$cmd" | sed -nE 's/.*(pr|mr)[[:space:]]+(merge|update)[[:space:]]+([0-9]+).*/\3/p' | head -1)"
346
- if [ -n "$merge_number" ] && rt_needs rt_report_body git-guard-delivery; then
347
- body="$(cd "$root" && rt_report_body "$merge_number" 2>/dev/null)"
348
- [ -n "$body" ] && printf '%s' "$body" | grep -qiE "$folder_skip_re" && exit 0
349
- fi
350
-
351
- lying="$(folder_in_branch "$folder")"
352
- [ -n "$lying" ] \
353
- && deny "BLOCKED: в ветке осталась папка задачи «${lying}» — она уедет в главную. Разобрать её потом будет некому: работа перейдёт к следующей задаче, а этот PR закроется. Перенеси в «${archive_dir:-архив}» то, что объясняет принятые решения, остальное удали и повтори. Если работа вливается частями, поставь в тело PR строку «Task-folder-skip: <причина>»."
354
-
355
- # Запись в архиве спрашиваем только у ветки, которая папку удалила. Иначе проверка
356
- # цеплялась бы к работе, у которой папки и не было. Без общего предка с главной веткой
357
- # сравнивать не с чем — тогда молчим.
358
- [ -n "$archive_dir" ] || exit 0
359
- base="$(git merge-base "$main_branch" HEAD 2>/dev/null)"
360
- [ -z "$base" ] && exit 0
361
-
362
- had="$(git ls-tree -d --name-only "$base" -- "$folder" 2>/dev/null | head -1)"
363
- [ -z "$had" ] && had="$(git log "$base..HEAD" --diff-filter=A --name-only --pretty=format: -- "$folder" 2>/dev/null | head -1)"
364
- [ -z "$had" ] && exit 0
365
-
366
- gained="$(git diff --name-only --diff-filter=A "$base" HEAD -- "$archive_dir" 2>/dev/null | head -1)"
367
- [ -z "$gained" ] \
368
- && deny "BLOCKED: папку задачи удалили, но в «${archive_dir}» ветка ничего не добавила. Удалить проще, чем разобрать, — и вместе с папкой пропадает разбор просьбы, единственная запись слов владельца. Перенеси то, что объясняет принятые решения, одним файлом с понятным именем и повтори."
369
-
370
- exit 0
340
+ if printf '%s' "$cmd" | grep -qE "${RT_CMD_BOUND}(gh[[:space:]]+pr[[:space:]]+merge|glab[[:space:]]+mr[[:space:]]+merge|az[[:space:]]+repos[[:space:]]+pr[[:space:]]+update)([[:space:]]|\$)"; then
341
+ rt_delivery_merge_folder
371
342
  fi
372
343
 
373
344
  # --- открытие заявки на слияние ----------------------------------------------------------
374
345
  # Команду ищем от начала строки или после разделителя — по той же причине, что и слияние:
375
346
  # упоминание в кавычках командой не является.
376
347
  printf '%s' "$cmd" \
377
- | grep -qE '(^|[;&|(]|&&|\|\|)[[:space:]]*(gh[[:space:]]+pr[[:space:]]+create|glab[[:space:]]+mr[[:space:]]+create|az[[:space:]]+repos[[:space:]]+pr[[:space:]]+create)([[:space:]]|$)' \
348
+ | grep -qE "${RT_CMD_BOUND}(gh[[:space:]]+pr[[:space:]]+create|glab[[:space:]]+mr[[:space:]]+create|az[[:space:]]+repos[[:space:]]+pr[[:space:]]+create)([[:space:]]|\$)" \
378
349
  || exit 0
379
350
 
380
351
  branch="$(git branch --show-current 2>/dev/null)"
@@ -440,19 +411,32 @@ if [ -n "$remote_main" ] && [ -n "$local_main" ] && [ "$remote_main" != "$local_
440
411
  fault "твоя ссылка origin/${main_branch} отстала от удалённой — ${local_main:0:8} против ${remote_main:0:8}.${age} Гард сравнивает ветку с тем, что лежит в дереве, поэтому молчание первого яруса значит «ссылка не старше ветки», а не «главная ветка влита». Влей и повтори: git fetch origin && git merge origin/${main_branch}."
441
412
  fi
442
413
 
414
+ # Личность вызова. Клиент хостинга держит две записи сразу — залогиненную и ту, чей токен стоит
415
+ # в окружении вызова, — и какая из них откроет заявку, из текста команды не видно ничем, кроме
416
+ # явной подстановки. Читающие вызовы идут от залогиненной и работают, поэтому пишущий выглядит
417
+ # таким же обычным; промах всплывает шагом позже, на назначении ревьювера: автор заявки её
418
+ # ревьювером не бывает. Чинится это только переоткрытием — автора у заявки не сменить.
419
+ #
420
+ # Судится текст команды, а не ответ хостинга: спросить, чьим токеном пойдёт вызов, до его
421
+ # исполнения нельзя. Дерево, не назвавшее переменной токена, требования не получает.
422
+ if [ -n "$pull_token_var" ] \
423
+ && ! printf '%s' "$cmd" | grep -qE "(^|[;&|(]|&&|\|\||[[:space:]])${pull_token_var}="; then
424
+ fault "заявка открывается без токена машинной записи: в команде нет подстановки «${pull_token_var}». Открытая залогиненной записью, она выйдет от владельца — ревьювером его тогда не назначить, и чинится это только переоткрытием.${pull_token_hint:+ Подставь токен: ${pull_token_hint} …}"
425
+ fi
426
+
443
427
  check_task "$number" "заявка с ветки «${branch}»" да
444
428
 
429
+ # Папка задачи разбирается до открытия заявки, а не после одобрения. Прежде здесь стояло
430
+ # напоминание, а требование — на слиянии: считалось, что папка нужна на диске всё время
431
+ # разбора, потому что правку по замечаниям без замысла отбивает гард хода работы. Владелец же
432
+ # вливает, как только видит зелёное, и закрывающему коммиту места не остаётся — трижды подряд
433
+ # папка уехала в главную неразобранной, а гард слияния этого не видел вовсе: кнопку нажимает
434
+ # человек на хостинге, и до него гард не достаёт. Разбор переехал сюда, а замысел после уборки
435
+ # гард хода работы берёт из истории ветки, а не с диска.
436
+ rt_delivery_open_folder
437
+
445
438
  # Всё несошедшееся названо здесь, разом: до этой строки собирались условия, каждое из которых
446
439
  # прежде отбивало вызов в одиночку.
447
440
  deny_faults
448
441
 
449
- # Сейчас папка ещё нужна: правки по замечаниям ревью идут в эту же ветку, а без plan.md их не
450
- # пропустит гард хода работы. Поэтому здесь только напоминание. Требование стоит на слиянии —
451
- # там папка уже не нужна, а вред от неё как раз и наступает.
452
- if [ -n "$tasks_dir" ]; then
453
- lying="$(folder_in_branch "$tasks_dir/$branch")"
454
- [ -n "$lying" ] \
455
- && hint "В ветке лежит папка задачи «${lying}». Разбери её до слияния, этим же PR: потом за неё уже никто не возьмётся. На слиянии это будет отказ, а не напоминание."
456
- fi
457
-
458
442
  exit 0
@@ -14,11 +14,13 @@
14
14
  # Сломанный гард не должен мешать работать.
15
15
 
16
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
17
18
 
18
- input="$(cat 2>/dev/null)"
19
+ rt_hook_read
20
+ input="$RT_HOOK_INPUT"
19
21
  [ -z "$input" ] && exit 0
20
22
 
21
- tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
23
+ tool="$(rt_hook_tool)"
22
24
  # Терминал среды разработки исполняет ту же командную строку и кладёт её в то же поле. Пока
23
25
  # гард проверял только оболочку, весь его смысл обходился сменой инструмента.
24
26
  case "$tool" in
@@ -26,7 +28,7 @@ case "$tool" in
26
28
  *) exit 0 ;;
27
29
  esac
28
30
 
29
- cmd="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null)"
31
+ cmd="$(rt_hook_cmd)"
30
32
 
31
33
  # Универсальный исполнитель среды передаёт настоящую команду вложенной строкой. Разбирать надо
32
34
  # её, а не обёртку: иначе имя команды стоит сразу за кавычкой и ни одно правило до него не
@@ -46,7 +48,7 @@ esac
46
48
 
47
49
  # Коммит выполнится в рабочем каталоге вызова, поэтому и ветку смотрим там же; корень проекта
48
50
  # — запасной вариант, и он важен для отдельного рабочего дерева, где ветка своя.
49
- workdir="$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null)"
51
+ workdir="$(rt_hook_cwd)"
50
52
  [ -z "$workdir" ] && workdir="${CLAUDE_PROJECT_DIR:-.}"
51
53
  cd "$workdir" 2>/dev/null || exit 0
52
54