@rt-tools/agent-kit 0.3.0 → 0.5.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 (202) hide show
  1. package/README.md +194 -30
  2. package/assets/agents/business-analyst.md +74 -0
  3. package/assets/agents/project-manager.md +70 -0
  4. package/assets/agents/qa-engineer.md +72 -0
  5. package/assets/agents/skill-curator.md +110 -0
  6. package/assets/agents/spec-critic.md +44 -0
  7. package/assets/agents/spec-writer.md +50 -0
  8. package/assets/checks/board.github.mjs +329 -0
  9. package/assets/checks/check-board.github.mjs +181 -0
  10. package/assets/checks/check-doc-paths.mjs +163 -0
  11. package/assets/checks/check-dupes.mjs +277 -0
  12. package/assets/checks/check-lib-layers.mjs +573 -0
  13. package/assets/checks/check-reuse.mjs +208 -0
  14. package/assets/checks/check-schema-drift.mjs +186 -0
  15. package/assets/checks/check-specs.mjs +1086 -0
  16. package/assets/checks/check-styles.mjs +109 -0
  17. package/assets/checks/rt-kit-checks.config.mjs +134 -0
  18. package/assets/checks/task-new.github.mjs +198 -0
  19. package/assets/commands/skill-curator.md +70 -0
  20. package/assets/defaults/gate-map.sh +106 -0
  21. package/assets/defaults/project.sh +204 -0
  22. package/assets/hooks/browser-device-id.sh +0 -0
  23. package/assets/hooks/browser-guard-device-id.sh +2 -1
  24. package/assets/hooks/browser-guard-no-asking.sh +27 -0
  25. package/assets/hooks/browser-guard-no-listing.sh +2 -1
  26. package/assets/hooks/browser-guard-no-other-drivers.sh +2 -1
  27. package/assets/hooks/browser-guard-require-select.sh +2 -1
  28. package/assets/hooks/commit-msg.sh +1 -1
  29. package/assets/hooks/constitution-index.sh +5 -4
  30. package/assets/hooks/dev-server-guard.sh +8 -6
  31. package/assets/hooks/docs-guard.sh +223 -37
  32. package/assets/hooks/git-guard-delivery.sh +171 -31
  33. package/assets/hooks/git-guard-main.sh +1 -0
  34. package/assets/hooks/git-guard-push-tests.sh +34 -13
  35. package/assets/hooks/glossary-load.sh +23 -0
  36. package/assets/hooks/grill-gate.sh +96 -0
  37. package/assets/hooks/lint-after-edit.sh +155 -30
  38. package/assets/hooks/qa-dataid-guard.sh +72 -32
  39. package/assets/hooks/reuse-first-guard.sh +105 -34
  40. package/assets/hooks/skill-gate-rearm.sh +1 -0
  41. package/assets/hooks/skill-gate.sh +75 -15
  42. package/assets/hooks/skill-loaded.sh +1 -0
  43. package/assets/hooks/sql-guard.sh +606 -56
  44. package/assets/hooks/task-context-load.sh +100 -0
  45. package/assets/hooks/task-flow-guard.sh +118 -0
  46. package/assets/laws/{access.md → application/access.md} +1 -4
  47. package/assets/laws/{locales.md → application/locales.md} +1 -3
  48. package/assets/laws/application/money.md +41 -0
  49. package/assets/laws/application/ownership.md +32 -0
  50. package/assets/laws/{search-visibility.md → application/search-visibility.md} +1 -1
  51. package/assets/laws/code-structure.md +7 -6
  52. package/assets/laws/delivery.md +53 -3
  53. package/assets/laws/entity-editing.md +49 -55
  54. package/assets/laws/entity-models.md +4 -14
  55. package/assets/laws/frontend-application.md +5 -5
  56. package/assets/laws/lib-imports.md +14 -1
  57. package/assets/laws/lists.md +33 -0
  58. package/assets/laws/navigation.md +40 -0
  59. package/assets/laws/project-documentation.md +27 -8
  60. package/assets/laws/reuse-first.md +26 -21
  61. package/assets/laws/shared-code.md +13 -1
  62. package/assets/laws/verifiability.md +30 -1
  63. package/assets/laws/work-conduct.md +59 -0
  64. package/assets/patterns/admin-lists-screen.md +131 -0
  65. package/assets/patterns/admin-nav-item.md +71 -0
  66. package/assets/patterns/angular-patterns-state.md +29 -22
  67. package/assets/patterns/api-layer-pair.md +40 -30
  68. package/assets/patterns/browser-verification-measure.md +41 -38
  69. package/assets/patterns/browser-verification-stand.md +106 -42
  70. package/assets/patterns/component-structure-new.md +33 -32
  71. package/assets/patterns/dependencies-upgrade.md +65 -0
  72. package/assets/patterns/doc-style-sweep.md +65 -28
  73. package/assets/patterns/doc-style-write.md +36 -33
  74. package/assets/patterns/entity-aside.md +136 -0
  75. package/assets/patterns/entity-models-new.md +124 -0
  76. package/assets/patterns/entity-store.md +91 -0
  77. package/assets/patterns/git-workflow-commit.azure.md +259 -0
  78. package/assets/patterns/git-workflow-commit.github.md +337 -0
  79. package/assets/patterns/git-workflow-commit.gitlab.md +283 -0
  80. package/assets/patterns/git-workflow-merge.md +42 -25
  81. package/assets/patterns/git-workflow-migration.md +61 -31
  82. package/assets/patterns/git-workflow-restart.md +20 -20
  83. package/assets/patterns/lib-layers-move.md +50 -32
  84. package/assets/patterns/lib-layers-new.md +41 -29
  85. package/assets/patterns/ownership-scope-resolve.md +69 -0
  86. package/assets/patterns/permissions-procedure.md +35 -33
  87. package/assets/patterns/platform-access-di.md +39 -25
  88. package/assets/patterns/pricing-quote.md +71 -0
  89. package/assets/patterns/reuse-first-extend.md +22 -22
  90. package/assets/patterns/seo-page.md +52 -40
  91. package/assets/patterns/seo-verify.md +48 -29
  92. package/assets/patterns/shared-code-new.md +37 -31
  93. package/assets/patterns/spec-driven-domain.md +60 -37
  94. package/assets/patterns/spec-driven-rule.md +55 -40
  95. package/assets/patterns/styling-bem-component.md +43 -32
  96. package/assets/patterns/styling-bem-layout.md +30 -24
  97. package/assets/patterns/task-flow-close.md +154 -0
  98. package/assets/patterns/task-flow-resume.md +94 -0
  99. package/assets/patterns/task-flow-start.md +129 -0
  100. package/assets/patterns/testing-e2e.md +53 -51
  101. package/assets/patterns/testing-unit.md +70 -46
  102. package/assets/patterns/translations-key.md +32 -19
  103. package/assets/patterns/ts-procedure.md +24 -25
  104. package/assets/rules/angular-patterns.md +50 -27
  105. package/assets/rules/api-layer.md +46 -28
  106. package/assets/rules/browser-verification.md +67 -48
  107. package/assets/rules/component-structure.md +43 -27
  108. package/assets/rules/dependencies.md +66 -0
  109. package/assets/rules/doc-style.md +95 -39
  110. package/assets/rules/entity-conventions.md +78 -0
  111. package/assets/rules/entity-models.md +70 -0
  112. package/assets/rules/git-workflow.azure.md +116 -0
  113. package/assets/rules/git-workflow.github.md +123 -0
  114. package/assets/rules/git-workflow.gitlab.md +113 -0
  115. package/assets/rules/lib-layers.md +56 -30
  116. package/assets/rules/lists.md +73 -0
  117. package/assets/rules/navigation.md +78 -0
  118. package/assets/rules/ownership-scope.md +63 -0
  119. package/assets/rules/permissions.md +43 -25
  120. package/assets/rules/platform-access.md +57 -29
  121. package/assets/rules/pricing.md +64 -0
  122. package/assets/rules/reuse-first.md +57 -43
  123. package/assets/rules/seo.md +51 -30
  124. package/assets/rules/shared-code.md +51 -26
  125. package/assets/rules/spec-driven.md +107 -51
  126. package/assets/rules/styling-bem.md +54 -39
  127. package/assets/rules/task-flow.md +150 -0
  128. package/assets/rules/testing.md +78 -47
  129. package/assets/rules/translations.md +48 -31
  130. package/assets/rules/typescript-conventions.md +57 -27
  131. package/assets/skills/agent-kit.md +85 -0
  132. package/assets/skills/write-a-skill.md +108 -0
  133. package/assets/templates/gate-map.sh +23 -15
  134. package/assets/templates/implementation.md +14 -8
  135. package/assets/templates/pattern.md +1 -1
  136. package/assets/templates/project.sh +32 -19
  137. package/assets/templates/rule.md +2 -2
  138. package/assets/variants.json +20 -0
  139. package/assets/workflows/feature.js +134 -0
  140. package/assets/workflows/plan.js +150 -0
  141. package/bin/agent-kit.d.ts.map +1 -1
  142. package/bin/agent-kit.js +78 -5
  143. package/bin/agent-kit.js.map +1 -1
  144. package/bin/prompt.d.ts +5 -0
  145. package/bin/prompt.d.ts.map +1 -1
  146. package/bin/prompt.js +19 -7
  147. package/bin/prompt.js.map +1 -1
  148. package/index.d.ts +1 -0
  149. package/index.d.ts.map +1 -1
  150. package/index.js +1 -0
  151. package/index.js.map +1 -1
  152. package/lib/assets.d.ts +8 -3
  153. package/lib/assets.d.ts.map +1 -1
  154. package/lib/assets.js +13 -3
  155. package/lib/assets.js.map +1 -1
  156. package/lib/catalog.d.ts +52 -5
  157. package/lib/catalog.d.ts.map +1 -1
  158. package/lib/catalog.js +104 -16
  159. package/lib/catalog.js.map +1 -1
  160. package/lib/commands.d.ts +22 -1
  161. package/lib/commands.d.ts.map +1 -1
  162. package/lib/commands.js +202 -14
  163. package/lib/commands.js.map +1 -1
  164. package/lib/companion.d.ts +5 -1
  165. package/lib/companion.d.ts.map +1 -1
  166. package/lib/companion.js +29 -2
  167. package/lib/companion.js.map +1 -1
  168. package/lib/config.d.ts +26 -9
  169. package/lib/config.d.ts.map +1 -1
  170. package/lib/config.js +41 -15
  171. package/lib/config.js.map +1 -1
  172. package/lib/freshness.d.ts +14 -0
  173. package/lib/freshness.d.ts.map +1 -0
  174. package/lib/freshness.js +116 -0
  175. package/lib/freshness.js.map +1 -0
  176. package/lib/hooks-map.d.ts +27 -0
  177. package/lib/hooks-map.d.ts.map +1 -0
  178. package/lib/hooks-map.js +77 -0
  179. package/lib/hooks-map.js.map +1 -0
  180. package/lib/integrity.d.ts +36 -0
  181. package/lib/integrity.d.ts.map +1 -0
  182. package/lib/integrity.js +44 -0
  183. package/lib/integrity.js.map +1 -0
  184. package/lib/picker.d.ts +11 -1
  185. package/lib/picker.d.ts.map +1 -1
  186. package/lib/picker.js +44 -6
  187. package/lib/picker.js.map +1 -1
  188. package/lib/sync.d.ts +26 -0
  189. package/lib/sync.d.ts.map +1 -1
  190. package/lib/sync.js +59 -4
  191. package/lib/sync.js.map +1 -1
  192. package/lib/variants.d.ts +44 -0
  193. package/lib/variants.d.ts.map +1 -0
  194. package/lib/variants.js +82 -0
  195. package/lib/variants.js.map +1 -0
  196. package/package.json +1 -1
  197. package/rt-tools-agent-kit-0.5.0.tgz +0 -0
  198. package/assets/laws/admin-lists.md +0 -35
  199. package/assets/laws/admin-navigation.md +0 -38
  200. package/assets/patterns/git-workflow-commit.md +0 -175
  201. package/assets/rules/git-workflow.md +0 -106
  202. package/rt-tools-agent-kit-0.3.0.tgz +0 -0
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env bash
2
+ # rt-hook: PreToolUse Bash|mcp__webstorm__execute_terminal_command|mcp__webstorm__execute_tool
2
3
  # Гард проверок перед пушем. PreToolUse на вызове пуша.
3
4
  #
4
5
  # Пуш — это вход в конвейер: слияние в главную ветку запускает выкатку, и всё, что не
@@ -6,13 +7,20 @@
6
7
  # выборочном прогоне и красные в конвейере: один раз прогонялись только сквозные спеки, другой
7
8
  # — только затронутый проект.
8
9
  #
9
- # Гард не верит на слово: он сам гоняет то, что перечислил профиль проекта, и пускает пуш
10
- # только при нулевом коде возврата. Если прогонщик кэширует результат, набор на неизменившемся
11
- # дереве занимает секунды, а после правки гоняется заново.
10
+ # Гард не верит на слово: он сам гоняет то, что перечислил профиль дерева, и пускает пуш только
11
+ # при нулевом коде возврата. Если прогонщик кэширует результат, набор на неизменившемся дереве
12
+ # занимает секунды, а после правки гоняется заново.
12
13
  #
13
- # Что гонять, знает профиль: {{projectProfile}}, функция `rt_push_checks` — по команде на
14
- # строку. Нет профиля или нет функции гард пропускает: список проверок пакет выдумать не
15
- # может.
14
+ # Что гонять, знает профиль: функция `rt_push_checks <база>` — по команде на строку. База —
15
+ # ветка, относительно которой считается вклад; пустая означает, что удалённого нет. Функция
16
+ # зовётся из каталога, откуда идёт пуш, и вправе решать по нему сама: шаг, проверки которого в
17
+ # дереве нет, она просто не печатает. Нет профиля или нет функции — гард пропускает: список
18
+ # проверок пакет выдумать не может.
19
+ #
20
+ # База берётся у удалённого, а не у локальной главной ветки: локальная отстаёт или расходится
21
+ # молча. Это уже случалось — локальная стояла на слиянии, стёртом из истории силовым пушем, и
22
+ # набор от неё посчитался бы не тот. Нет сети или нет удалённого — база пустая, и профиль
23
+ # откатывается на полный прогон: гейт может оказаться строже нужного, но НИКОГДА не слабее.
16
24
  #
17
25
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: не репозиторий, битый ввод, нет профиля — пропуск.
18
26
 
@@ -21,6 +29,7 @@ input="$(cat 2>/dev/null)"
21
29
 
22
30
  tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
23
31
  case "$tool" in
32
+ # Терминал среды и универсальный исполнитель кладут команду в то же поле.
24
33
  Bash | mcp__webstorm__execute_terminal_command | mcp__webstorm__execute_tool) ;;
25
34
  *) exit 0 ;;
26
35
  esac
@@ -40,13 +49,25 @@ workdir="$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null)"
40
49
  [ -z "$workdir" ] && workdir="${CLAUDE_PROJECT_DIR:-.}"
41
50
  cd "$workdir" 2>/dev/null || exit 0
42
51
  git rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0
52
+ [ -f package.json ] || exit 0
43
53
 
44
- profile="${CLAUDE_PROJECT_DIR:-.}/{{projectProfile}}"
45
- [ -f "$profile" ] || exit 0
46
- # shellcheck disable=SC1090
47
- . "$profile" 2>/dev/null || exit 0
54
+ # Профиль дерева: сперва умолчание пакета, поверх него — надстройка проекта, если она есть.
55
+ # Объявленная в надстройке функция замещает умолчание целиком и вправе позвать его обратно
56
+ # суффиксом `_default`. Нет ни того ни другого — хук пропускает: пустой гард лучше гарда,
57
+ # отбивающего наугад.
58
+ rt_hooks_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
59
+ 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
60
+ # shellcheck disable=SC1090
61
+ [ -f "$profile" ] && . "$profile" 2>/dev/null
62
+ done
48
63
  command -v rt_push_checks >/dev/null 2>&1 || exit 0
49
64
 
65
+ main_branch="${RT_MAIN_BRANCH:-main}"
66
+ base=''
67
+ if git fetch --quiet origin "$main_branch" 2>/dev/null && git rev-parse --verify --quiet "origin/$main_branch" >/dev/null 2>&1; then
68
+ base="origin/$main_branch"
69
+ fi
70
+
50
71
  failed=""
51
72
  output=""
52
73
  while IFS= read -r check; do
@@ -56,14 +77,14 @@ while IFS= read -r check; do
56
77
  output="$out"
57
78
  break
58
79
  done <<EOF
59
- $(rt_push_checks)
80
+ $(rt_push_checks "$base")
60
81
  EOF
61
82
 
62
83
  [ -z "$failed" ] && exit 0
63
84
 
64
85
  # Хвост вывода, а не весь: у прогонщика он длинный, а нужна причина отказа.
65
- tail_out="$(printf '%s' "$output" | tail -n 40)"
66
- reason="Отбито перед пушем: «${failed}» упала. Пуш — вход в конвейер, и красное отсюда проверяется уже на проде. Почини и повтори.
86
+ tail_out="$(printf '%s' "$output" | tail -n 40 | tr -d '\000')"
87
+ reason="BLOCKED: пуш без зелёного локального прогона. «${failed}» упала — почини и пушь снова, обходить гард нельзя. Пуш — вход в конвейер, и красное отсюда проверяется уже на проде. Хвост вывода:
67
88
 
68
89
  ${tail_out}"
69
90
 
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env bash
2
+ # rt-hook: SessionStart startup|resume|compact|clear
3
+ # SessionStart: словарь проекта уезжает в контекст целиком, на каждом запуске сессии.
4
+ #
5
+ # Памятью это не держится: словарь читают перед тем, как написать текст, а разговор с
6
+ # владельцем идёт без чтения — и слово из левой колонки «Так не пишем» всплывает в ответе,
7
+ # хотя в файлах его уже нет. Здесь словарь приходит до первой реплики и действует и на
8
+ # тексты, и на ответы.
9
+ #
10
+ # FAIL-OPEN: нет файла или нет `jq` — выходим молча. Сессия важнее словаря.
11
+
12
+ GLOSSARY="${CLAUDE_PROJECT_DIR:-.}/docs/GLOSSARY.md"
13
+ [ -f "$GLOSSARY" ] || exit 0
14
+ command -v jq >/dev/null 2>&1 || exit 0
15
+
16
+ {
17
+ printf 'СЛОВАРЬ ПРОЕКТА — `docs/GLOSSARY.md`, ниже целиком.\n\n'
18
+ printf 'Слово отсюда употребляется в том значении, что здесь: и в файлах, и в ответах владельцу.\n'
19
+ printf 'Раздел «Так не пишем» действует так же — левая колонка не пишется и не произносится нигде.\n'
20
+ printf 'Слова, которого здесь нет, у читателя нет тоже: оно либо заводится здесь же той же правкой,\n'
21
+ printf 'либо заменяется тем, что уже есть.\n\n'
22
+ cat "$GLOSSARY"
23
+ } | jq -Rs '{hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:.}}'
@@ -0,0 +1,96 @@
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
+ # РАЗРЕШАЕТСЯ (exit 0). Сломанный гард не имеет права заклинить разговор.
20
+
21
+ input="$(cat 2>/dev/null)"
22
+ [ -z "$input" ] && exit 0
23
+
24
+ command -v jq >/dev/null 2>&1 || exit 0
25
+
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
+ rt_hooks_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
38
+ 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
39
+ # shellcheck disable=SC1090
40
+ [ -f "$profile" ] && . "$profile" 2>/dev/null
41
+ done
42
+
43
+ # Подстановка без двоеточия намеренно: заданное пустым — это отказ дерева от требования, и
44
+ # подменять его умолчанием нельзя. Умолчание достаётся только тому, кто не задал переменной
45
+ # вовсе.
46
+ laws_dir="${RT_LAWS_DIR-docs/constitution}"
47
+ rules_dir="${RT_RULES_DIR-.claude/skills}"
48
+ specs_dir="${RT_SPECS_DIR-docs/specs}"
49
+
50
+ # Дерево, у которого нет ни законов, ни правил, требования не получает: читать нечего.
51
+ [ -z "$laws_dir" ] && [ -z "$rules_dir" ] && exit 0
52
+
53
+ # Образец, по которому вызов инструмента считается чтением правил. Каталоги идут в него как
54
+ # есть: точка в `.claude` совпадает с любым знаком и лишнего сюда не приводит.
55
+ read_re="$(printf '%s' "$laws_dir|$rules_dir|$specs_dir" | sed 's/^|*//; s/|*$//; s/||*/|/g')"
56
+ [ -z "$read_re" ] && exit 0
57
+
58
+ # Ход — это всё, что записано после последнего настоящего ввода владельца. Ответ инструмента
59
+ # приходит той же ролью `user`, поэтому строки с `tool_result` вводом не считаются: иначе ходом
60
+ # оказался бы кусок после последнего вызова инструмента, и чтение правил в его начале потерялось
61
+ # бы.
62
+ #
63
+ # Хвост в 400 строк: запись хода растёт всю сессию, а судится только последний ход.
64
+ verdict="$(tail -n 400 "$transcript" 2>/dev/null | jq -s -r --arg re "$read_re" '
65
+ def is_input:
66
+ .type == "user"
67
+ and (((.message.content // []) | if type == "array"
68
+ then ([.[] | select(.type == "tool_result")] | length)
69
+ else 0 end) == 0);
70
+
71
+ (map(is_input) | rindex(true)) as $i
72
+ | (if $i == null then . else .[$i + 1:] end) as $turn
73
+ | [$turn[] | select(.type == "assistant") | (.message.content // [])[] | select(.type == "text") | .text] as $texts
74
+ | [$turn[] | select(.type == "assistant") | (.message.content // [])[] | select(.type == "tool_use")] as $uses
75
+ | ($uses | map(
76
+ (.name == "Skill")
77
+ or ((.name // "") | test("^(Read|Grep|Glob)$")) and ((.input | tostring) | test($re))
78
+ or ((.name == "Bash") and ((.input.command // "") | test($re)))
79
+ ) | any) as $read
80
+ | (($texts | join("\n")) | test("\\?[[:space:]]*$"; "m")) as $asked_prose
81
+ | ($uses | map(.name == "AskUserQuestion") | any) as $asked_menu
82
+ | if ($asked_prose or $asked_menu) and ($read | not) then "ask" else "pass" end
83
+ ' 2>/dev/null)"
84
+
85
+ [ "$verdict" = "ask" ] || exit 0
86
+
87
+ reason="BLOCKED by grill-gate: в ответе есть вопрос владельцу, а законы и правила за этот ход не читались. Вопрос, ответ на который уже записан, владельцу не задаётся — правило ведения работы. Прогони поиск по словам темы и ответь по найденному; спрашивай только то, что документацией не покрыто:
88
+
89
+ grep -rn -i \"<слово темы>\" $laws_dir $rules_dir $specs_dir
90
+
91
+ Гард судит один ход: следующий заход не отбивается."
92
+
93
+ jq -n --arg r "$reason" '{decision:"block",reason:$r}' 2>/dev/null \
94
+ || printf '{"decision":"block","reason":"grill-gate: прочитай законы и правила по теме, прежде чем спрашивать владельца."}\n'
95
+
96
+ exit 0
@@ -1,11 +1,12 @@
1
1
  #!/usr/bin/env bash
2
+ # rt-hook: PostToolUse Edit|Write|MultiEdit|Bash|mcp__webstorm__create_new_file
2
3
  # Линтер по следам правки. PostToolUse.
3
4
  #
4
5
  # Два входа. Правка файла — линтуется один изменённый файл. Перенос файла командой — линтуются
5
- # файлы по адресу назначения: перенос меняет либу, а вместе с ней и границы, и импорт,
6
- # законный на прежнем месте, на новом уже запрещён. Ровно так запрещённый импорт уезжает в
7
- # общую либу молча: файлы перекладываются командой, хук на неё не смотрит, и правило границ
8
- # сработало бы — но его никто не запустил.
6
+ # файлы по адресу назначения: перенос меняет либу, а вместе с ней и границы, и импорт, законный
7
+ # на прежнем месте, на новом уже запрещён. Ровно так запрещённый импорт уезжает в общую либу
8
+ # молча: файлы перекладываются командой, хук на неё не смотрит, и правило границ сработало бы —
9
+ # но его никто не запустил.
9
10
  #
10
11
  # Полный набор гоняет гард на пуше, но это конец работы: к моменту, когда правило срабатывает,
11
12
  # поверх нарушения лежит десяток правок, и разбор превращается в археологию. Здесь тот же
@@ -15,11 +16,15 @@
15
16
  # незаконченный промежуточный файл имеет право быть красным. Чинить их надо до конца задачи —
16
17
  # все, включая лежавшие в файле раньше.
17
18
  #
18
- # Чем линтуется файл, знает профиль проекта: {{projectProfile}}, функция `rt_lint_for <файл>`.
19
+ # Что здесь чем зовётся, знает профиль дерева:
20
+ # rt_lint_for — чем линтуется этот файл;
21
+ # rt_is_app_code — где лежит код, к которому линтеры вообще относятся;
22
+ # RT_LINT_SKIP_RE — что из него исключено;
23
+ # rt_push_checks — что гоняет гейт пуша. По нему же решается, догонит ли замечание позже:
24
+ # линтер, которого в гейте нет, ловит только этот хук.
19
25
  # Нет профиля или нет функции — хук молчит.
20
26
  #
21
- # ОТКАЗ В ПОЛЬЗУ РАБОТЫ и молча: нет линтера, файл вне проекта, зелёный результат — пустой
22
- # вывод.
27
+ # ОТКАЗ В ПОЛЬЗУ РАБОТЫ и молча: нет линтера, файл вне дерева, зелёный результат — пустой вывод.
23
28
 
24
29
  # Сколько файлов линтуется за один перенос. Переезд либы трогает десятки файлов, и прогон по
25
30
  # каждому превратил бы хук в минутную паузу; на нарушение границы хватает первых.
@@ -49,46 +54,166 @@ fi
49
54
  workdir="$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null)"
50
55
  [ -z "$workdir" ] && workdir="${CLAUDE_PROJECT_DIR:-.}"
51
56
  cd "$workdir" 2>/dev/null || exit 0
57
+ [ -f package.json ] || exit 0
52
58
 
53
- profile="${CLAUDE_PROJECT_DIR:-.}/{{projectProfile}}"
54
- [ -f "$profile" ] || exit 0
55
- # shellcheck disable=SC1090
56
- . "$profile" 2>/dev/null || exit 0
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
57
65
  command -v rt_lint_for >/dev/null 2>&1 || exit 0
58
66
 
59
- files=""
67
+ # --- какие файлы проверяем -------------------------------------------------------------
68
+
69
+ # Пути назначения всех переносов в команде. Команда бывает составной, поэтому режется по
70
+ # разделителям, и каждый кусок разбирается отдельно.
71
+ collect_moved() {
72
+ # Перевод строки в конце обязателен: чтение без него теряет последний кусок, а команда
73
+ # чаще всего состоит ровно из одного.
74
+ printf '%s\n' "$1" | tr ';&\n' '\n\n\n' | while IFS= read -r segment; do
75
+ case "$segment" in
76
+ *"git mv "*) ;;
77
+ *) continue ;;
78
+ esac
79
+
80
+ # shellcheck disable=SC2086 # разбиение по пробелам здесь и нужно
81
+ set -- ${segment#*git mv }
82
+ args=''
83
+ for arg in "$@"; do
84
+ case "$arg" in
85
+ -*) continue ;;
86
+ esac
87
+ args="${args}${args:+ }${arg}"
88
+ done
89
+
90
+ # shellcheck disable=SC2086
91
+ set -- $args
92
+ [ "$#" -lt 2 ] && continue
93
+
94
+ dest=''
95
+ for arg in "$@"; do dest="$arg"; done
96
+
97
+ # Назначение — каталог: имя файла остаётся прежним, меняется только путь.
98
+ if [ -d "$dest" ]; then
99
+ for arg in "$@"; do
100
+ [ "$arg" = "$dest" ] && continue
101
+ printf '%s/%s\n' "${dest%/}" "${arg##*/}"
102
+ done
103
+ else
104
+ printf '%s\n' "$dest"
105
+ fi
106
+ done
107
+ }
108
+
109
+ # Каталог разворачивается в лежащие в нём файлы: перенос умеет двигать целые слои.
110
+ expand() {
111
+ if [ -d "$1" ]; then
112
+ find "$1" -type f \( -name '*.ts' -o -name '*.html' -o -name '*.scss' \) 2>/dev/null
113
+ elif [ -f "$1" ]; then
114
+ printf '%s\n' "$1"
115
+ fi
116
+ }
117
+
60
118
  if [ "$mode" = "edit" ]; then
61
- files="$(printf '%s' "$input" | jq -r '.tool_input.file_path // .tool_input.pathInProject // empty' 2>/dev/null)"
119
+ path="$(printf '%s' "$input" | jq -r '.tool_input.file_path // .tool_input.pathInProject // empty' 2>/dev/null)"
120
+ [ -z "$path" ] && exit 0
121
+ # Путь от корня дерева приводится к абсолютному один раз, чтобы образцы не двоились.
122
+ case "$path" in
123
+ /*) ;;
124
+ *) path="${CLAUDE_PROJECT_DIR:-.}/$path" ;;
125
+ esac
126
+ candidates="$(expand "$path")"
62
127
  else
63
- # Пути назначения всех переносов в команде. Команда бывает составной, поэтому режется по
64
- # разделителям, и каждый кусок разбирается отдельно. Перевод строки в конце обязателен:
65
- # чтение без него теряет последний кусок, а команда чаще всего состоит ровно из одного.
66
- files="$(printf '%s\n' "$command_text" | tr ';&\n' '\n\n\n' | awk '
67
- /git mv / {
68
- sub(/.*git mv /, "");
69
- n = split($0, parts, /[[:space:]]+/);
70
- for (i = n; i >= 1; i--) {
71
- if (parts[i] !~ /^-/ && parts[i] != "") { print parts[i]; break }
72
- }
73
- }' | head -n "$MAX_FILES")"
128
+ candidates="$(collect_moved "$command_text" | while IFS= read -r moved; do expand "$moved"; done)"
74
129
  fi
75
130
 
76
- [ -z "$files" ] && exit 0
131
+ [ -z "$candidates" ] && exit 0
132
+
133
+ # --- отсев того, что линтерами дерева не покрыто ----------------------------------------
134
+
135
+ lintable() {
136
+ if command -v rt_is_app_code >/dev/null 2>&1; then
137
+ rt_is_app_code "$1" || return 1
138
+ fi
139
+ if [ -n "${RT_LINT_SKIP_RE:-}" ] && printf '%s' "$1" | grep -qE "$RT_LINT_SKIP_RE"; then
140
+ return 1
141
+ fi
142
+
143
+ return 0
144
+ }
145
+
146
+ # Имя линтера в команде — первое слово, которое не запускатель. Оно идёт в заголовок и по нему
147
+ # же ищется гейт пуша.
148
+ linter_name() {
149
+ printf '%s' "$1" | awk '{
150
+ for (i = 1; i <= NF; i++) {
151
+ if ($i ~ /^(npx|pnpm|yarn|bun|npm|exec|run|dlx|--no-install)$/) { continue }
152
+ print $i; exit
153
+ }
154
+ }'
155
+ }
156
+
157
+ # --- прогон -----------------------------------------------------------------------------
158
+
159
+ push_checks="$(command -v rt_push_checks >/dev/null 2>&1 && rt_push_checks 2>/dev/null)"
77
160
 
78
161
  report=""
162
+ linters=""
163
+ covered=1
164
+ checked=0
165
+
79
166
  while IFS= read -r file; do
80
167
  [ -z "$file" ] && continue
81
- [ -f "$file" ] || continue
168
+ lintable "$file" || continue
169
+ [ "$checked" -ge "$MAX_FILES" ] && break
170
+
82
171
  lint="$(rt_lint_for "$file" 2>/dev/null)"
83
172
  [ -z "$lint" ] && continue
84
- out="$(eval "$lint" 2>&1)" || report="${report}${out}"$'\n'
173
+ checked=$((checked + 1))
174
+
175
+ out="$(eval "$lint" 2>&1)" && continue
176
+
177
+ # Линтера нет или он упал сам по себе — это не замечание к файлу, молчим.
178
+ printf '%s' "$out" | grep -qiE 'command not found|could not determine executable|Cannot find module' && continue
179
+
180
+ linter="$(linter_name "$lint")"
181
+ case " $linters " in
182
+ *" $linter "*) ;;
183
+ *) linters="${linters}${linters:+ }${linter}" ;;
184
+ esac
185
+ printf '%s' "$push_checks" | grep -qF "$linter" || covered=0
186
+
187
+ report="${report}
188
+ ${file}:
189
+ $(printf '%s' "$out" | head -c 2500 | tr -d '\000')
190
+ "
85
191
  done <<EOF
86
- $files
192
+ $candidates
87
193
  EOF
88
194
 
89
195
  [ -z "$report" ] && exit 0
90
196
 
91
- jq -n --arg r "Замечания линтера по только что тронутым файлам чинить до конца задачи, включая те, что лежали в файле раньше:"$'\n'"$report" \
92
- '{hookSpecificOutput:{hookEventName:"PostToolUse",additionalContext:$r}}' 2>/dev/null
197
+ # Длинный вывод режем: важен факт и первые нарушения, остальное видно при полном прогоне.
198
+ report="$(printf '%s' "$report" | head -c 6000)"
199
+
200
+ if [ "$covered" -eq 1 ]; then
201
+ tail_line="Пуш всё равно не пройдёт, пока набор красный."
202
+ else
203
+ tail_line="Почини их сейчас: этот линтер в гейт пуша не входит, и отложенное замечание уедет в главную ветку молча."
204
+ fi
205
+
206
+ if [ "$mode" = "move" ]; then
207
+ head_line="ЛИНТЕР (${linters}) НАШЁЛ ЗАМЕЧАНИЯ ПОСЛЕ ПЕРЕНОСА. Перенос меняет либу, а вместе с ней границы: импорт, законный на прежнем месте, на новом может быть запрещён."
208
+ else
209
+ head_line="ЛИНТЕР (${linters}) НАШЁЛ ЗАМЕЧАНИЯ:"
210
+ fi
211
+
212
+ ctx="${head_line}
213
+ ${report}
214
+
215
+ Почини их до конца задачи — правятся ВСЕ замечания в затронутом файле, и новые, и лежавшие раньше: накопленные нарушения глушат сигнал о свежих. ${tail_line}"
216
+
217
+ jq -n --arg c "$ctx" '{hookSpecificOutput:{hookEventName:"PostToolUse",additionalContext:$c}}' 2>/dev/null
93
218
 
94
219
  exit 0
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env bash
2
- # Гард якоря для спек. PreToolUse на правке шаблонов.
2
+ # rt-hook: PreToolUse Edit|Write|MultiEdit|mcp__webstorm__create_new_file
3
+ # Гард якоря для спек. PreToolUse на правке разметки.
3
4
  #
4
5
  # Спеки адресуют элементы только через этот атрибут. Классы оформления меняются вместе с
5
6
  # вёрсткой, а поиск по роли и тексту ломается на переводах — приложение живёт во многих
@@ -8,17 +9,18 @@
8
9
  # того, как кто-то заметит его отсутствие.
9
10
  #
10
11
  # Правило инвертировано: интерактивным считается ЛЮБОЙ составной тег (компонент) и любой тег с
11
- # привязкой события, кроме помеченных пропуском. Перечислять интерактивные поимённо не выходит
12
- # — компонентов в источнике вида сотня, и список устаревает молча: забытый в нём компонент
13
- # проходит без якоря.
12
+ # привязкой события, кроме помеченных декоративными. Перечислять интерактивные поимённо не
13
+ # выходит — компонентов в ките больше сотни, и список устаревает молча: забытый в нём
14
+ # компонент проходит без якоря.
14
15
  #
15
16
  # Осознанный выход: атрибут пропуска НА САМОМ ТЕГЕ. Маркер, который ищется во всей правке,
16
17
  # выключал бы проверку целиком — декоративная ссылка уносила бы с собой кнопку отправки из той
17
18
  # же правки.
18
19
  #
19
- # Что здесь не проверяется, знает профиль проекта: {{projectProfile}}, переменная RT_QA_SKIP_RE
20
- # образец путей, у которых якорь не требуется (сам источник вида, витрина, корневая
21
- # разметка).
20
+ # Что здесь чем зовётся, знает профиль дерева:
21
+ # RT_QA_SKIP_RE пути, где якорь не требуется: витрина, корневая разметка, спеки;
22
+ # RT_QA_COMPONENT_RE — по какому имени тег опознаётся компонентом этого дерева;
23
+ # rt_qa_decorative — имена тегов, которые спека не нажимает.
22
24
  #
23
25
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: нет разборщика, битый ввод, чужой инструмент — пропуск.
24
26
 
@@ -29,51 +31,89 @@ command -v perl >/dev/null 2>&1 || exit 0
29
31
 
30
32
  tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
31
33
  case "$tool" in
34
+ # Инструмент среды заводит файл теми же двумя данными, только называет их иначе — без этой
35
+ # ветки разметка заводилась мимо всех проверок.
32
36
  Edit | Write | MultiEdit | mcp__webstorm__create_new_file) ;;
33
37
  *) exit 0 ;;
34
38
  esac
35
39
 
36
- target="$(printf '%s' "$input" | jq -r '.tool_input.file_path // .tool_input.pathInProject // empty' 2>/dev/null)"
37
- case "$target" in
38
- *.component.html|*.html) ;;
40
+ path="$(printf '%s' "$input" | jq -r '.tool_input.file_path // .tool_input.pathInProject // empty' 2>/dev/null)"
41
+ # Путь из среды приходит относительным корню дерева, а все образцы ниже написаны от каталога
42
+ # приложения. Приводим к одному виду один раз, чтобы правила не двоились.
43
+ case "$path" in
44
+ /*) ;;
45
+ ?*) path="${CLAUDE_PROJECT_DIR:-.}/$path" ;;
46
+ esac
47
+ case "$path" in
48
+ *.html) ;;
39
49
  *) exit 0 ;;
40
50
  esac
41
51
 
42
- profile="${CLAUDE_PROJECT_DIR:-.}/{{projectProfile}}"
43
- if [ -f "$profile" ]; then
52
+ # Профиль дерева: сперва умолчание пакета, поверх него — надстройка проекта, если она есть.
53
+ rt_hooks_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
54
+ 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
44
55
  # shellcheck disable=SC1090
45
- . "$profile" 2>/dev/null
46
- if [ -n "${RT_QA_SKIP_RE:-}" ] && printf '%s' "$target" | grep -qE "$RT_QA_SKIP_RE"; then
47
- exit 0
48
- fi
56
+ [ -f "$profile" ] && . "$profile" 2>/dev/null
57
+ done
58
+
59
+ # Разметка приложения живёт там же, где его код. Без этой положительной проверки гард
60
+ # распространялся на любой файл разметки на диске — черновик вне дерева отклонялся требованием
61
+ # проставить якоря.
62
+ if command -v rt_is_app_code >/dev/null 2>&1; then
63
+ rt_is_app_code "$path" || exit 0
64
+ fi
65
+
66
+ if [ -n "${RT_QA_SKIP_RE:-}" ] && printf '%s' "$path" | grep -qE "$RT_QA_SKIP_RE"; then
67
+ exit 0
49
68
  fi
50
69
 
51
70
  # Проверяется только НОВЫЙ текст: то, что уже лежало в файле, эта правка не заводила, и
52
71
  # требовать якорь у чужой строки — значит отбивать правку соседней.
53
72
  added="$(printf '%s' "$input" | jq -r '
54
- [.tool_input.content? // empty,
55
- .tool_input.text? // empty,
56
- .tool_input.new_string? // empty,
57
- (.tool_input.edits? // [] | map(.new_string) | join("\n"))] | join("\n")' 2>/dev/null)"
73
+ [ .tool_input.content?, .tool_input.text?, .tool_input.new_string?, (.tool_input.edits[]?.new_string) ]
74
+ | map(select(. != null)) | join("\n")
75
+ ' 2>/dev/null)"
58
76
  [ -z "$added" ] && exit 0
59
77
 
60
- # Открывающие теги без якоря: составной тег (компонент) или тег с привязкой события. Тег с
61
- # атрибутом пропуска или с самим якорем пропускается.
62
- offenders="$(printf '%s' "$added" | perl -0ne '
63
- my @bad;
64
- while (/<([a-zA-Z][a-zA-Z0-9-]*)((?:"[^"]*"|\x27[^\x27]*\x27|[^>"\x27])*)>/gs) {
78
+ decorative="$(command -v rt_qa_decorative >/dev/null 2>&1 && rt_qa_decorative 2>/dev/null)"
79
+ component_re="${RT_QA_COMPONENT_RE:--}"
80
+
81
+ # Открывающие теги разбираются ЦЕЛИКОМ: тег занимает несколько строк, и якорь часто стоит не в
82
+ # первой из них — построчная проверка врала бы.
83
+ missing="$(printf '%s' "$added" | RT_QA_DECORATIVE="$decorative" RT_QA_COMPONENT_RE="$component_re" perl -0777 -ne '
84
+ # Комментарии вырезаются до разбора: в них часто лежит разметка-пример («было <button …>,
85
+ # заменено на переход»). Без этого правка отклонялась из-за элемента, которого в дереве
86
+ # документа не будет, а обойти отказ можно было только испортив текст самого комментария.
87
+ s/<!--.*?-->//gs;
88
+ # Декоративное и структурное: спека такое не нажимает и по нему не проверяет состояние.
89
+ # Всё остальное считается интерактивным по умолчанию — кит растёт, и умолчание должно быть
90
+ # в сторону якоря, а не в сторону тишины. Попадание в список НЕ выдаёт индульгенцию: тег с
91
+ # привязкой события проверяется наравне с прочими.
92
+ my %decorative = map { $_ => 1 } split /\s+/, ($ENV{RT_QA_DECORATIVE} // "");
93
+ my $component = $ENV{RT_QA_COMPONENT_RE} || "-";
94
+ my %seen;
95
+ while (/<([a-zA-Z][\w-]*)((?:[^<>"\x27]|"[^"]*"|\x27[^\x27]*\x27)*)>/gs) {
65
96
  my ($tag, $attrs) = ($1, $2);
66
- next if $attrs =~ /qa-dataid/ || $attrs =~ /qa-skip/;
67
- my $is_component = $tag =~ /-/;
68
- my $has_event = $attrs =~ /\(\s*[a-zA-Z][\w.]*\s*\)\s*=/;
69
- push @bad, "<$tag" if $is_component || $has_event;
97
+ # Структурные теги каркаса узла документа не порождают: поставленный на них якорь не
98
+ # найдёт ни один селектор спеки. Требовать его — значит требовать неработающего.
99
+ next if $tag =~ /^(ng-container|ng-template|ng-content)$/;
100
+ next if $attrs =~ /\bqa-dataid\b/;
101
+ # Осознанный отказ помечается на самом теге, а не где-то в той же правке.
102
+ next if $attrs =~ /\bqa-skip\b/;
103
+ my $interactive =
104
+ $attrs =~ /\(\s*[a-zA-Z][\w.:-]*\s*\)\s*=/
105
+ || $attrs =~ /\b(routerLink|rtButton|href)\b/
106
+ || $tag =~ /^(button|a|input|select|textarea|form|dialog)$/
107
+ || ($tag =~ /$component/ && !$decorative{$tag});
108
+ next unless $interactive;
109
+ next if $seen{$tag}++;
110
+ print "<$tag> ";
70
111
  }
71
- print join(", ", @bad) if @bad;
72
112
  ' 2>/dev/null)"
73
113
 
74
- [ -z "$offenders" ] && exit 0
114
+ [ -z "$missing" ] && exit 0
75
115
 
76
- reason="Интерактивный элемент без якоря для спек: ${offenders}. Спеки адресуют элементы только им классы меняются вместе с вёрсткой, а поиск по тексту ломается на переводах. Значение — через дефис, по смыслу элемента; повторяющиеся элементы списка носят один якорь и различаются атрибутами данных. Декоративный элемент помечается атрибутом пропуска на самом теге."
116
+ reason="BLOCKED: интерактивные элементы без якоря для спек в ${path##*/}: ${missing}. Спеки адресуют элементы только через этот атрибут: классы оформления меняются вместе с вёрсткой, а поиск по роли и тексту ломается на переводахоба вида селекторов делают сквозные спеки хрупкими. Проставь якорь через дефис по смыслу элемента; повторяющиеся элементы списка носят ОДИН якорь и различаются атрибутами данных. Если элемент чисто декоративный и спека его никогда не тронет — поставь атрибут пропуска НА САМ ЭТОТ ТЕГ, соседние элементы правки при этом продолжают проверяться."
77
117
 
78
118
  jq -n --arg r "$reason" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$r}}' 2>/dev/null \
79
119
  || printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Интерактивный элемент без якоря для спек."}}\n'