@rt-tools/agent-kit 0.1.0 → 0.3.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 (131) hide show
  1. package/README.md +88 -9
  2. package/assets/hooks/browser-device-id.sh +20 -0
  3. package/assets/hooks/browser-guard-device-id.sh +27 -0
  4. package/assets/hooks/browser-guard-no-listing.sh +17 -0
  5. package/assets/hooks/browser-guard-no-other-drivers.sh +78 -0
  6. package/assets/hooks/browser-guard-require-select.sh +53 -0
  7. package/assets/hooks/commit-msg.sh +26 -0
  8. package/assets/hooks/constitution-index.sh +42 -0
  9. package/assets/hooks/dev-server-guard.sh +113 -0
  10. package/assets/hooks/docs-guard.sh +96 -0
  11. package/assets/hooks/git-guard-delivery.sh +110 -0
  12. package/assets/hooks/git-guard-main.sh +72 -0
  13. package/assets/hooks/git-guard-push-tests.sh +73 -0
  14. package/assets/hooks/lint-after-edit.sh +94 -0
  15. package/assets/hooks/qa-dataid-guard.sh +81 -0
  16. package/assets/hooks/reuse-first-guard.sh +83 -0
  17. package/assets/hooks/skill-gate-rearm.sh +22 -0
  18. package/assets/hooks/skill-gate.sh +68 -0
  19. package/assets/hooks/skill-loaded.sh +20 -0
  20. package/assets/hooks/sql-guard.sh +129 -0
  21. package/assets/laws/access.md +3 -12
  22. package/assets/laws/admin-lists.md +9 -21
  23. package/assets/laws/admin-navigation.md +3 -15
  24. package/assets/laws/code-structure.md +5 -16
  25. package/assets/laws/delivery.md +2 -16
  26. package/assets/laws/entity-editing.md +7 -20
  27. package/assets/laws/entity-models.md +10 -17
  28. package/assets/laws/frontend-application.md +3 -14
  29. package/assets/laws/lib-imports.md +0 -13
  30. package/assets/laws/locales.md +2 -11
  31. package/assets/laws/project-documentation.md +7 -20
  32. package/assets/laws/reuse-first.md +0 -9
  33. package/assets/laws/search-visibility.md +0 -13
  34. package/assets/laws/shared-code.md +0 -13
  35. package/assets/laws/verifiability.md +0 -13
  36. package/assets/patterns/angular-patterns-state.md +94 -0
  37. package/assets/patterns/api-layer-pair.md +78 -0
  38. package/assets/patterns/browser-verification-measure.md +83 -0
  39. package/assets/patterns/browser-verification-stand.md +79 -0
  40. package/assets/patterns/component-structure-new.md +98 -0
  41. package/assets/patterns/doc-style-sweep.md +100 -0
  42. package/assets/patterns/doc-style-write.md +106 -0
  43. package/assets/patterns/git-workflow-commit.md +175 -0
  44. package/assets/patterns/git-workflow-merge.md +82 -0
  45. package/assets/patterns/git-workflow-migration.md +58 -0
  46. package/assets/patterns/git-workflow-restart.md +49 -0
  47. package/assets/patterns/lib-layers-move.md +77 -0
  48. package/assets/patterns/lib-layers-new.md +70 -0
  49. package/assets/patterns/permissions-procedure.md +69 -0
  50. package/assets/patterns/platform-access-di.md +70 -0
  51. package/assets/patterns/reuse-first-extend.md +73 -0
  52. package/assets/patterns/seo-page.md +92 -0
  53. package/assets/patterns/seo-verify.md +64 -0
  54. package/assets/patterns/shared-code-new.md +80 -0
  55. package/assets/patterns/spec-driven-domain.md +100 -0
  56. package/assets/patterns/spec-driven-rule.md +112 -0
  57. package/assets/patterns/styling-bem-component.md +77 -0
  58. package/assets/patterns/styling-bem-layout.md +67 -0
  59. package/assets/patterns/testing-e2e.md +90 -0
  60. package/assets/patterns/testing-unit.md +93 -0
  61. package/assets/patterns/translations-key.md +51 -0
  62. package/assets/patterns/ts-procedure.md +66 -0
  63. package/assets/rules/angular-patterns.md +52 -0
  64. package/assets/rules/api-layer.md +53 -0
  65. package/assets/rules/browser-verification.md +69 -0
  66. package/assets/rules/component-structure.md +48 -0
  67. package/assets/rules/doc-style.md +61 -0
  68. package/assets/rules/git-workflow.md +106 -0
  69. package/assets/rules/lib-layers.md +54 -0
  70. package/assets/rules/permissions.md +52 -0
  71. package/assets/rules/platform-access.md +49 -0
  72. package/assets/rules/reuse-first.md +69 -0
  73. package/assets/rules/seo.md +50 -0
  74. package/assets/rules/shared-code.md +45 -0
  75. package/assets/rules/spec-driven.md +89 -0
  76. package/assets/rules/styling-bem.md +59 -0
  77. package/assets/rules/testing.md +69 -0
  78. package/assets/rules/translations.md +52 -0
  79. package/assets/rules/typescript-conventions.md +46 -0
  80. package/assets/templates/gate-map.sh +37 -0
  81. package/assets/templates/implementation.md +38 -0
  82. package/assets/templates/pattern.md +4 -0
  83. package/assets/templates/project.sh +41 -0
  84. package/assets/templates/rule.md +12 -23
  85. package/bin/agent-kit.d.ts +1 -1
  86. package/bin/agent-kit.d.ts.map +1 -1
  87. package/bin/agent-kit.js +63 -7
  88. package/bin/agent-kit.js.map +1 -1
  89. package/bin/prompt.d.ts +9 -0
  90. package/bin/prompt.d.ts.map +1 -0
  91. package/bin/prompt.js +57 -0
  92. package/bin/prompt.js.map +1 -0
  93. package/index.d.ts +2 -0
  94. package/index.d.ts.map +1 -1
  95. package/index.js +2 -0
  96. package/index.js.map +1 -1
  97. package/lib/assets.d.ts +10 -2
  98. package/lib/assets.d.ts.map +1 -1
  99. package/lib/assets.js +24 -28
  100. package/lib/assets.js.map +1 -1
  101. package/lib/catalog.d.ts +44 -0
  102. package/lib/catalog.d.ts.map +1 -0
  103. package/lib/catalog.js +86 -0
  104. package/lib/catalog.js.map +1 -0
  105. package/lib/commands.d.ts +13 -1
  106. package/lib/commands.d.ts.map +1 -1
  107. package/lib/commands.js +106 -11
  108. package/lib/commands.js.map +1 -1
  109. package/lib/companion.d.ts +53 -0
  110. package/lib/companion.d.ts.map +1 -0
  111. package/lib/companion.js +33 -0
  112. package/lib/companion.js.map +1 -0
  113. package/lib/config.d.ts +29 -1
  114. package/lib/config.d.ts.map +1 -1
  115. package/lib/config.js +43 -6
  116. package/lib/config.js.map +1 -1
  117. package/lib/picker.d.ts +47 -0
  118. package/lib/picker.d.ts.map +1 -0
  119. package/lib/picker.js +112 -0
  120. package/lib/picker.js.map +1 -0
  121. package/lib/stamp.d.ts +2 -5
  122. package/lib/stamp.d.ts.map +1 -1
  123. package/lib/stamp.js +25 -10
  124. package/lib/stamp.js.map +1 -1
  125. package/lib/sync.d.ts +3 -0
  126. package/lib/sync.d.ts.map +1 -1
  127. package/lib/sync.js +20 -1
  128. package/lib/sync.js.map +1 -1
  129. package/package.json +1 -1
  130. package/rt-tools-agent-kit-0.3.0.tgz +0 -0
  131. package/rt-tools-agent-kit-0.1.0.tgz +0 -0
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env bash
2
+ # Гард поставки. PreToolUse на заведении ветки и открытии PR.
3
+ #
4
+ # Закон о поставке требует трёх вещей, которых обычно не проверяет ничто: правка начинается с
5
+ # задачи, видимой в очереди работ; задача, ветка и отчёт несут один номер; у задачи есть
6
+ # исполнитель. Держатся они памятью — и не удерживаются: задачи стоят вне очереди, исполнитель
7
+ # не проставлен, а большинство влитых PR приходит с веток, за которыми задачи не стояло вовсе.
8
+ #
9
+ # Гард стоит в двух точках, и в каждой требует того, что в этот момент исправимо:
10
+ #
11
+ # заведение ветки — имя с номером разбирается на месте; имя без номера пропускается:
12
+ # локальная ветка под пробу законна, в главную она не поедет, потому что PR с неё не
13
+ # откроется;
14
+ # открытие PR — ветка обязана нести номер, а заголовок — начинаться с того же номера.
15
+ #
16
+ # Форму имени ветки и номер в заголовке знает профиль проекта: {{projectProfile}}, функция
17
+ # `rt_task_branch_ok <имя>` и переменная RT_TASK_TITLE_RE. Нет профиля — гард пропускает: форму
18
+ # имени пакет выдумать не может.
19
+ #
20
+ # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: не репозиторий, нет разборщика, битый ввод — пропуск.
21
+
22
+ input="$(cat 2>/dev/null)"
23
+ [ -z "$input" ] && exit 0
24
+ command -v jq >/dev/null 2>&1 || exit 0
25
+
26
+ tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
27
+ case "$tool" in
28
+ Bash | mcp__webstorm__execute_terminal_command | mcp__webstorm__execute_tool) ;;
29
+ *) exit 0 ;;
30
+ esac
31
+
32
+ cmd="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null)"
33
+ [ -z "$cmd" ] && exit 0
34
+
35
+ if [ "$tool" = "mcp__webstorm__execute_tool" ] && command -v perl >/dev/null 2>&1; then
36
+ inner="$(printf '%s' "$cmd" | perl -0ne '
37
+ if (/--command(?:=|\s+)(?:"((?:[^"\\]|\\.)*)"|\x27([^\x27]*)\x27|(.+))/s) {
38
+ print defined $1 ? $1 : (defined $2 ? $2 : $3);
39
+ }
40
+ ' 2>/dev/null)"
41
+ [ -n "$inner" ] && cmd="$inner"
42
+ fi
43
+
44
+ profile="${CLAUDE_PROJECT_DIR:-.}/{{projectProfile}}"
45
+ [ -f "$profile" ] || exit 0
46
+ # shellcheck disable=SC1090
47
+ . "$profile" 2>/dev/null || exit 0
48
+ command -v rt_task_branch_ok >/dev/null 2>&1 || exit 0
49
+
50
+ deny() {
51
+ jq -n --arg r "$1" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$r}}' 2>/dev/null \
52
+ || printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Гард поставки."}}\n'
53
+ exit 0
54
+ }
55
+
56
+ # --- заведение ветки ---------------------------------------------------------------------
57
+ branch_arg=''
58
+ if printf '%s' "$cmd" | grep -qE '(^|[;&|[:space:]])git[[:space:]]+(checkout[[:space:]]+-b|switch[[:space:]]+-c)[[:space:]]'; then
59
+ branch_arg="$(printf '%s' "$cmd" | sed -nE 's/.*git[[:space:]]+(checkout[[:space:]]+-b|switch[[:space:]]+-c)[[:space:]]+([^[:space:];&|]+).*/\2/p' | head -1)"
60
+ branch_arg="${branch_arg%\'}"; branch_arg="${branch_arg#\'}"
61
+ branch_arg="${branch_arg%\"}"; branch_arg="${branch_arg#\"}"
62
+ fi
63
+
64
+ if [ -n "$branch_arg" ]; then
65
+ # Имя, притворяющееся веткой под задачу, но не совпадающее с формой, — это промах в имени,
66
+ # а не осознанная беззадачная ветка. Ловится до первого коммита.
67
+ if printf '%s' "$branch_arg" | grep -qE '^[A-Za-z]+-[0-9]+' && ! rt_task_branch_ok "$branch_arg"; then
68
+ deny "Имя ветки «${branch_arg}» не той формы, что принята здесь. Номер у ветки тот же, что у задачи и у заголовка PR."
69
+ fi
70
+ exit 0
71
+ fi
72
+
73
+ # --- открытие PR -------------------------------------------------------------------------
74
+ case "$cmd" in
75
+ *gh\ pr\ create*) ;;
76
+ *) exit 0 ;;
77
+ esac
78
+
79
+ workdir="$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null)"
80
+ [ -z "$workdir" ] && workdir="${CLAUDE_PROJECT_DIR:-.}"
81
+ cd "$workdir" 2>/dev/null || exit 0
82
+
83
+ branch="$(git branch --show-current 2>/dev/null)"
84
+ [ -z "$branch" ] && exit 0 # открепившийся HEAD — не про этот случай
85
+
86
+ # Локальная ветка без номера законна, а PR с неё — уже нет: правка, доезжающая до главной
87
+ # ветки, начинается с задачи. Это единственное место, где беззадачная ветка упирается.
88
+ rt_task_branch_ok "$branch" \
89
+ || deny "PR с ветки «${branch}», за которой не стоит задачи. Правка начинается с задачи, видимой в очереди работ: заведи её и перенеси работу в ветку с её номером."
90
+
91
+ number="$(printf '%s' "$branch" | sed -nE 's/^[A-Za-z]+-([0-9]+).*/\1/p')"
92
+
93
+ title=''
94
+ if command -v perl >/dev/null 2>&1; then
95
+ title="$(printf '%s' "$cmd" | perl -0ne '
96
+ if (/--title(?:=|\s+)(?:"((?:[^"\\]|\\.)*)"|\x27([^\x27]*)\x27|([^\s]+))/s) {
97
+ print defined $1 ? $1 : (defined $2 ? $2 : $3);
98
+ }
99
+ ' 2>/dev/null)"
100
+ fi
101
+
102
+ if [ -n "$title" ] && [ -n "$number" ]; then
103
+ title_number="$(printf '%s' "$title" | sed -nE 's/^\[[A-Za-z]+-([0-9]+)\].*/\1/p')"
104
+ [ -z "$title_number" ] \
105
+ && deny "Заголовок PR не начинается с номера задачи. В списке PR тела не видно, а строка связи живёт именно там — без номера в заголовке отчёт с задачей не сопоставить."
106
+ [ "$title_number" = "$number" ] \
107
+ || deny "В заголовке PR номер ${title_number}, у ветки — ${number}. Задача, ветка и отчёт несут один и тот же номер."
108
+ fi
109
+
110
+ exit 0
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env bash
2
+ # Гард главной ветки. PreToolUse на вызове коммита.
3
+ #
4
+ # Коммит в главную ветку минует ветку, PR и разбор, а поставка построена на них целиком —
5
+ # правило `git-workflow`. Прямой коммит туда почти всегда промах: «остался на главной после
6
+ # слияния предыдущего PR».
7
+ #
8
+ # Имя главной ветки не зашито строкой: сначала спрашивается указатель удалённого репозитория,
9
+ # затем пробуются существующие `origin/main` и `origin/master`, и лишь в конце берётся `main`.
10
+ #
11
+ # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: не репозиторий, нет гита, открепившийся HEAD, битый ввод — пропуск.
12
+ # Сломанный гард не должен мешать работать.
13
+
14
+ input="$(cat 2>/dev/null)"
15
+ [ -z "$input" ] && exit 0
16
+
17
+ tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
18
+ # Терминал среды разработки исполняет ту же командную строку и кладёт её в то же поле. Пока
19
+ # гард проверял только оболочку, весь его смысл обходился сменой инструмента.
20
+ case "$tool" in
21
+ Bash | mcp__webstorm__execute_terminal_command | mcp__webstorm__execute_tool) ;;
22
+ *) exit 0 ;;
23
+ esac
24
+
25
+ cmd="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null)"
26
+
27
+ # Универсальный исполнитель среды передаёт настоящую команду вложенной строкой. Разбирать надо
28
+ # её, а не обёртку: иначе имя команды стоит сразу за кавычкой и ни одно правило до него не
29
+ # дотягивается.
30
+ if [ "$tool" = "mcp__webstorm__execute_tool" ] && command -v perl >/dev/null 2>&1; then
31
+ inner="$(printf '%s' "$cmd" | perl -0ne '
32
+ if (/--command(?:=|\s+)(?:"((?:[^"\\]|\\.)*)"|\x27([^\x27]*)\x27|(.+))/s) {
33
+ print defined $1 ? $1 : (defined $2 ? $2 : $3);
34
+ }
35
+ ' 2>/dev/null)"
36
+ [ -n "$inner" ] && cmd="$inner"
37
+ fi
38
+ case "$cmd" in
39
+ *git\ commit*) ;;
40
+ *) exit 0 ;;
41
+ esac
42
+
43
+ # Коммит выполнится в рабочем каталоге вызова, поэтому и ветку смотрим там же; корень проекта
44
+ # — запасной вариант, и он важен для отдельного рабочего дерева, где ветка своя.
45
+ workdir="$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null)"
46
+ [ -z "$workdir" ] && workdir="${CLAUDE_PROJECT_DIR:-.}"
47
+ cd "$workdir" 2>/dev/null || exit 0
48
+
49
+ git rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0
50
+
51
+ branch="$(git branch --show-current 2>/dev/null)"
52
+ [ -z "$branch" ] && exit 0 # открепившийся HEAD — не про этот случай
53
+
54
+ default="$(git symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null | sed 's#^origin/##')"
55
+ if [ -z "$default" ]; then
56
+ for candidate in main master; do
57
+ if git show-ref --verify --quiet "refs/remotes/origin/$candidate" 2>/dev/null; then
58
+ default="$candidate"
59
+ break
60
+ fi
61
+ done
62
+ fi
63
+ [ -z "$default" ] && default="main"
64
+
65
+ [ "$branch" = "$default" ] || exit 0
66
+
67
+ reason="Отбито: коммит прямо в «${default}». Работа едет через ветку и PR — правило git-workflow. Заведи ветку отдельным вызовом и коммить в неё: подготовленные изменения при этом сохранятся. Если коммит в ${default} действительно нужен — спроси владельца, сам не обходи."
68
+
69
+ jq -n --arg r "$reason" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$r}}' 2>/dev/null \
70
+ || printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Коммит в главную ветку отбит. Заведи ветку."}}\n'
71
+
72
+ exit 0
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env bash
2
+ # Гард проверок перед пушем. PreToolUse на вызове пуша.
3
+ #
4
+ # Пуш — это вход в конвейер: слияние в главную ветку запускает выкатку, и всё, что не
5
+ # проверено локально, проверяется уже на проде. Сюда дважды подряд уезжают правки, зелёные в
6
+ # выборочном прогоне и красные в конвейере: один раз прогонялись только сквозные спеки, другой
7
+ # — только затронутый проект.
8
+ #
9
+ # Гард не верит на слово: он сам гоняет то, что перечислил профиль проекта, и пускает пуш
10
+ # только при нулевом коде возврата. Если прогонщик кэширует результат, набор на неизменившемся
11
+ # дереве занимает секунды, а после правки гоняется заново.
12
+ #
13
+ # Что гонять, знает профиль: {{projectProfile}}, функция `rt_push_checks` — по команде на
14
+ # строку. Нет профиля или нет функции — гард пропускает: список проверок пакет выдумать не
15
+ # может.
16
+ #
17
+ # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: не репозиторий, битый ввод, нет профиля — пропуск.
18
+
19
+ input="$(cat 2>/dev/null)"
20
+ [ -z "$input" ] && exit 0
21
+
22
+ tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
23
+ case "$tool" in
24
+ Bash | mcp__webstorm__execute_terminal_command | mcp__webstorm__execute_tool) ;;
25
+ *) exit 0 ;;
26
+ esac
27
+
28
+ cmd="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null)"
29
+ case "$cmd" in
30
+ *git\ push*) ;;
31
+ *) exit 0 ;;
32
+ esac
33
+
34
+ # Пробный пуш ничего не отправляет: гонять ради него весь набор незачем.
35
+ case "$cmd" in
36
+ *--dry-run*) exit 0 ;;
37
+ esac
38
+
39
+ workdir="$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null)"
40
+ [ -z "$workdir" ] && workdir="${CLAUDE_PROJECT_DIR:-.}"
41
+ cd "$workdir" 2>/dev/null || exit 0
42
+ git rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0
43
+
44
+ profile="${CLAUDE_PROJECT_DIR:-.}/{{projectProfile}}"
45
+ [ -f "$profile" ] || exit 0
46
+ # shellcheck disable=SC1090
47
+ . "$profile" 2>/dev/null || exit 0
48
+ command -v rt_push_checks >/dev/null 2>&1 || exit 0
49
+
50
+ failed=""
51
+ output=""
52
+ while IFS= read -r check; do
53
+ [ -z "$check" ] && continue
54
+ out="$(eval "$check" 2>&1)" && continue
55
+ failed="$check"
56
+ output="$out"
57
+ break
58
+ done <<EOF
59
+ $(rt_push_checks)
60
+ EOF
61
+
62
+ [ -z "$failed" ] && exit 0
63
+
64
+ # Хвост вывода, а не весь: у прогонщика он длинный, а нужна причина отказа.
65
+ tail_out="$(printf '%s' "$output" | tail -n 40)"
66
+ reason="Отбито перед пушем: «${failed}» упала. Пуш — вход в конвейер, и красное отсюда проверяется уже на проде. Почини и повтори.
67
+
68
+ ${tail_out}"
69
+
70
+ jq -n --arg r "$reason" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$r}}' 2>/dev/null \
71
+ || printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Проверки перед пушем не прошли."}}\n'
72
+
73
+ exit 0
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env bash
2
+ # Линтер по следам правки. PostToolUse.
3
+ #
4
+ # Два входа. Правка файла — линтуется один изменённый файл. Перенос файла командой — линтуются
5
+ # файлы по адресу назначения: перенос меняет либу, а вместе с ней и границы, и импорт,
6
+ # законный на прежнем месте, на новом уже запрещён. Ровно так запрещённый импорт уезжает в
7
+ # общую либу молча: файлы перекладываются командой, хук на неё не смотрит, и правило границ
8
+ # сработало бы — но его никто не запустил.
9
+ #
10
+ # Полный набор гоняет гард на пуше, но это конец работы: к моменту, когда правило срабатывает,
11
+ # поверх нарушения лежит десяток правок, и разбор превращается в археологию. Здесь тот же
12
+ # линтер, но по затронутым файлам — секунды, сразу после действия, пока контекст ещё свой.
13
+ #
14
+ # Замечания возвращаются добавленным контекстом, а не отказом: действие уже применено, и
15
+ # незаконченный промежуточный файл имеет право быть красным. Чинить их надо до конца задачи —
16
+ # все, включая лежавшие в файле раньше.
17
+ #
18
+ # Чем линтуется файл, знает профиль проекта: {{projectProfile}}, функция `rt_lint_for <файл>`.
19
+ # Нет профиля или нет функции — хук молчит.
20
+ #
21
+ # ОТКАЗ В ПОЛЬЗУ РАБОТЫ и молча: нет линтера, файл вне проекта, зелёный результат — пустой
22
+ # вывод.
23
+
24
+ # Сколько файлов линтуется за один перенос. Переезд либы трогает десятки файлов, и прогон по
25
+ # каждому превратил бы хук в минутную паузу; на нарушение границы хватает первых.
26
+ MAX_FILES=12
27
+
28
+ input="$(cat 2>/dev/null)"
29
+ [ -z "$input" ] && exit 0
30
+ command -v jq >/dev/null 2>&1 || exit 0
31
+
32
+ tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
33
+ case "$tool" in
34
+ Edit | Write | MultiEdit | mcp__webstorm__create_new_file) mode="edit" ;;
35
+ Bash) mode="move" ;;
36
+ *) exit 0 ;;
37
+ esac
38
+
39
+ # Отсев до всякой работы: хук висит на каждой команде оболочки, а перенос среди них — редкость.
40
+ command_text=""
41
+ if [ "$mode" = "move" ]; then
42
+ command_text="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null)"
43
+ case "$command_text" in
44
+ *"git mv "*) ;;
45
+ *) exit 0 ;;
46
+ esac
47
+ fi
48
+
49
+ workdir="$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null)"
50
+ [ -z "$workdir" ] && workdir="${CLAUDE_PROJECT_DIR:-.}"
51
+ cd "$workdir" 2>/dev/null || exit 0
52
+
53
+ profile="${CLAUDE_PROJECT_DIR:-.}/{{projectProfile}}"
54
+ [ -f "$profile" ] || exit 0
55
+ # shellcheck disable=SC1090
56
+ . "$profile" 2>/dev/null || exit 0
57
+ command -v rt_lint_for >/dev/null 2>&1 || exit 0
58
+
59
+ files=""
60
+ if [ "$mode" = "edit" ]; then
61
+ files="$(printf '%s' "$input" | jq -r '.tool_input.file_path // .tool_input.pathInProject // empty' 2>/dev/null)"
62
+ 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")"
74
+ fi
75
+
76
+ [ -z "$files" ] && exit 0
77
+
78
+ report=""
79
+ while IFS= read -r file; do
80
+ [ -z "$file" ] && continue
81
+ [ -f "$file" ] || continue
82
+ lint="$(rt_lint_for "$file" 2>/dev/null)"
83
+ [ -z "$lint" ] && continue
84
+ out="$(eval "$lint" 2>&1)" || report="${report}${out}"$'\n'
85
+ done <<EOF
86
+ $files
87
+ EOF
88
+
89
+ [ -z "$report" ] && exit 0
90
+
91
+ jq -n --arg r "Замечания линтера по только что тронутым файлам — чинить до конца задачи, включая те, что лежали в файле раньше:"$'\n'"$report" \
92
+ '{hookSpecificOutput:{hookEventName:"PostToolUse",additionalContext:$r}}' 2>/dev/null
93
+
94
+ exit 0
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env bash
2
+ # Гард якоря для спек. PreToolUse на правке шаблонов.
3
+ #
4
+ # Спеки адресуют элементы только через этот атрибут. Классы оформления меняются вместе с
5
+ # вёрсткой, а поиск по роли и тексту ломается на переводах — приложение живёт во многих
6
+ # локалях. Оба вида селекторов делают сквозные спеки хрупкими, поэтому якорь один и ставится
7
+ # по умолчанию, а не дописывается потом: элемент без атрибута попадает в спеку только после
8
+ # того, как кто-то заметит его отсутствие.
9
+ #
10
+ # Правило инвертировано: интерактивным считается ЛЮБОЙ составной тег (компонент) и любой тег с
11
+ # привязкой события, кроме помеченных пропуском. Перечислять интерактивные поимённо не выходит
12
+ # — компонентов в источнике вида сотня, и список устаревает молча: забытый в нём компонент
13
+ # проходит без якоря.
14
+ #
15
+ # Осознанный выход: атрибут пропуска НА САМОМ ТЕГЕ. Маркер, который ищется во всей правке,
16
+ # выключал бы проверку целиком — декоративная ссылка уносила бы с собой кнопку отправки из той
17
+ # же правки.
18
+ #
19
+ # Что здесь не проверяется, знает профиль проекта: {{projectProfile}}, переменная RT_QA_SKIP_RE
20
+ # — образец путей, у которых якорь не требуется (сам источник вида, витрина, корневая
21
+ # разметка).
22
+ #
23
+ # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: нет разборщика, битый ввод, чужой инструмент — пропуск.
24
+
25
+ input="$(cat 2>/dev/null)"
26
+ [ -z "$input" ] && exit 0
27
+ command -v jq >/dev/null 2>&1 || exit 0
28
+ command -v perl >/dev/null 2>&1 || exit 0
29
+
30
+ tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
31
+ case "$tool" in
32
+ Edit | Write | MultiEdit | mcp__webstorm__create_new_file) ;;
33
+ *) exit 0 ;;
34
+ esac
35
+
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) ;;
39
+ *) exit 0 ;;
40
+ esac
41
+
42
+ profile="${CLAUDE_PROJECT_DIR:-.}/{{projectProfile}}"
43
+ if [ -f "$profile" ]; then
44
+ # 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
49
+ fi
50
+
51
+ # Проверяется только НОВЫЙ текст: то, что уже лежало в файле, эта правка не заводила, и
52
+ # требовать якорь у чужой строки — значит отбивать правку соседней.
53
+ 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)"
58
+ [ -z "$added" ] && exit 0
59
+
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) {
65
+ 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;
70
+ }
71
+ print join(", ", @bad) if @bad;
72
+ ' 2>/dev/null)"
73
+
74
+ [ -z "$offenders" ] && exit 0
75
+
76
+ reason="Интерактивный элемент без якоря для спек: ${offenders}. Спеки адресуют элементы только им — классы меняются вместе с вёрсткой, а поиск по тексту ломается на переводах. Значение — через дефис, по смыслу элемента; повторяющиеся элементы списка носят один якорь и различаются атрибутами данных. Декоративный элемент помечается атрибутом пропуска на самом теге."
77
+
78
+ jq -n --arg r "$reason" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$r}}' 2>/dev/null \
79
+ || printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Интерактивный элемент без якоря для спек."}}\n'
80
+
81
+ exit 0
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env bash
2
+ # Гард «ничего не пишется с нуля». PreToolUse на правке кода и разметки.
3
+ #
4
+ # Линтеры знают правила, но не знают ИНВЕНТАРЬ: линтер стилей поймает сырой цвет, линтер кода
5
+ # — нетипизированное значение, но ни один из них не знает, что готовое сообщение уже написано,
6
+ # и молча пропустит свою область оповещения с текстом, кнопкой и стилями. Гард закрывает ровно
7
+ # эту дыру: сверяет то, что пишется, с тем, что в дереве уже есть.
8
+ #
9
+ # Правило целиком — `reuse-first`.
10
+ #
11
+ # Что считается переизобретением, знает профиль проекта: {{projectProfile}}, функция
12
+ # `rt_reinvented_in <файл>` — печатает по строке «образец<таб>чем заменить». Нет профиля или
13
+ # нет функции — гард пропускает: инвентарь дерева пакет знать не может.
14
+ #
15
+ # Осознанный выход: строка с маркером отступления пропускается — для случаев, которых в
16
+ # готовом действительно нет. Маркер объясняет, чего именно не хватает; «эти строки были здесь
17
+ # раньше» причиной не считается, и оттого проверяется только НОВЫЙ текст.
18
+ #
19
+ # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: нет разборщика, битый ввод, чужой инструмент — пропуск.
20
+
21
+ input="$(cat 2>/dev/null)"
22
+ [ -z "$input" ] && exit 0
23
+ command -v jq >/dev/null 2>&1 || exit 0
24
+
25
+ tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
26
+ case "$tool" in
27
+ Edit | Write | MultiEdit | mcp__webstorm__create_new_file) ;;
28
+ *) exit 0 ;;
29
+ esac
30
+
31
+ path="$(printf '%s' "$input" | jq -r '.tool_input.file_path // .tool_input.pathInProject // empty' 2>/dev/null)"
32
+ # Путь от корня проекта приводится к абсолютному один раз, чтобы образцы не двоились.
33
+ case "$path" in
34
+ /*) ;;
35
+ ?*) path="${CLAUDE_PROJECT_DIR:-.}/$path" ;;
36
+ esac
37
+ case "$path" in
38
+ *.html | *.scss | *.ts) ;;
39
+ *) exit 0 ;;
40
+ esac
41
+
42
+ # Правила этого дерева действуют на файлы этого дерева: без проверки корня гард требовал бы
43
+ # верстать готовым и в черновике за пределами репозитория.
44
+ case "$path" in
45
+ "${CLAUDE_PROJECT_DIR:-.}"/*) ;;
46
+ *) exit 0 ;;
47
+ esac
48
+
49
+ profile="${CLAUDE_PROJECT_DIR:-.}/{{projectProfile}}"
50
+ [ -f "$profile" ] || exit 0
51
+ # shellcheck disable=SC1090
52
+ . "$profile" 2>/dev/null || exit 0
53
+ command -v rt_reinvented_in >/dev/null 2>&1 || exit 0
54
+
55
+ # Только новый текст: строка, уже лежавшая в файле, этой правкой не заводилась.
56
+ added="$(printf '%s' "$input" | jq -r '
57
+ [.tool_input.content? // empty,
58
+ .tool_input.text? // empty,
59
+ .tool_input.new_string? // empty,
60
+ (.tool_input.edits? // [] | map(.new_string) | join("\n"))] | join("\n")' 2>/dev/null)"
61
+ [ -z "$added" ] && exit 0
62
+
63
+ found=""
64
+ while IFS="$(printf '\t')" read -r pattern replacement; do
65
+ [ -z "$pattern" ] && continue
66
+ # Строка с маркером отступления снимается до сверки — вместе с ней снимается и повод.
67
+ printf '%s\n' "$added" | grep -v 'native-ok' | grep -qE "$pattern" || continue
68
+ found="${found} ${pattern} — есть готовое: ${replacement}"$'\n'
69
+ done <<EOF
70
+ $(rt_reinvented_in "$path")
71
+ EOF
72
+
73
+ [ -z "$found" ] && exit 0
74
+
75
+ reason="Написано своё там, где готовое уже есть:
76
+
77
+ ${found}
78
+ Работа начинается с чтения готового, а не с чистого файла — правило reuse-first. Недостающий вариант заводится у готового, а не клонируется рядом: клон забирает правки на себя и расходится с оригиналом с первой же. Если готового здесь правда не хватает, поставь маркер отступления в той же строке и объясни, чего именно нет."
79
+
80
+ jq -n --arg r "$reason" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$r}}' 2>/dev/null \
81
+ || printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Написано своё там, где готовое уже есть."}}\n'
82
+
83
+ exit 0
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env bash
2
+ # Взвод гейта заново. SessionStart(compact|clear).
3
+ #
4
+ # Гейт помнит загруженное правило по идентификатору сессии и дальше пропускает эту область
5
+ # молча. Сжатие контекста и очистка выносят из контекста САМ ТЕКСТ правила, но идентификатор
6
+ # сессии оставляют прежним — без этого хука гейт продолжал бы пропускать, пока агент работает
7
+ # по пересказу вместо самого правила.
8
+ #
9
+ # Снятие записи заставляет каждую область загрузить своё правило заново.
10
+ #
11
+ # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: любая ошибка пропускает. Хук удаляет временный файл и ничего не
12
+ # отбивает.
13
+
14
+ input="$(cat 2>/dev/null)"
15
+ [ -z "$input" ] && exit 0
16
+
17
+ sid="$(printf '%s' "$input" | jq -r '.session_id // empty' 2>/dev/null)"
18
+ [ -z "$sid" ] && exit 0
19
+
20
+ rm -f "${TMPDIR:-/tmp}/claude-skill-gate/${sid}.loaded" 2>/dev/null
21
+
22
+ exit 0
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env bash
2
+ # Гейт правил: не даёт править файл, пока не загружено правило, под которое он подпадает.
3
+ #
4
+ # Закон и правило, которых никто не открывает, не действуют. Напоминание в подсказке помогает
5
+ # ровно до первой спешки, поэтому требование держит хук: правка отбивается ОДИН раз на сессию
6
+ # для каждой области, а после загрузки правила та же область проходит молча — повторов нет, и
7
+ # стоит это почти ничего.
8
+ #
9
+ # Карта «файл — правило» живёт не здесь: она знает имена этого дерева, а хук их знать не может.
10
+ # Её пишет проект в {{gateMap}} — функцией `skill_for <род вызова> <цель>`, печатающей имя
11
+ # правила. Нет карты — гейт пропускает всё: пустой гейт лучше гейта, отбивающего наугад.
12
+ #
13
+ # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: любая ошибка и любой неопознанный путь пропускают правку (exit 0).
14
+ # Сломанный гейт не имеет права остановить работу совсем.
15
+
16
+ input="$(cat 2>/dev/null)"
17
+ [ -z "$input" ] && exit 0
18
+
19
+ sid="$(printf '%s' "$input" | jq -r '.session_id // "nosession"' 2>/dev/null)"
20
+ tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
21
+
22
+ map="${CLAUDE_PROJECT_DIR:-.}/{{gateMap}}"
23
+ [ -f "$map" ] || exit 0
24
+ # shellcheck disable=SC1090
25
+ . "$map" 2>/dev/null || exit 0
26
+ command -v skill_for >/dev/null 2>&1 || exit 0
27
+
28
+ req=""
29
+ target=""
30
+ case "$tool" in
31
+ Edit|Write|MultiEdit)
32
+ target="$(printf '%s' "$input" | jq -r '.tool_input.file_path // .tool_input.pathInProject // empty' 2>/dev/null)"
33
+ [ -z "$target" ] && exit 0
34
+ # Путь от корня проекта приводится к абсолютному один раз: иначе карту пришлось бы
35
+ # писать в двух формах, и вторая расходилась бы с первой молча.
36
+ case "$target" in
37
+ /*) ;;
38
+ ?*) target="${CLAUDE_PROJECT_DIR:-.}/$target" ;;
39
+ esac
40
+ # Правила этого дерева действуют на файлы этого дерева. Без проверки корня гейт ловил
41
+ # бы и соседний репозиторий на той же машине.
42
+ case "$target" in
43
+ "${CLAUDE_PROJECT_DIR:-.}"/*) ;;
44
+ *) exit 0 ;;
45
+ esac
46
+ req="$(skill_for edit "$target" 2>/dev/null)"
47
+ ;;
48
+ Bash)
49
+ target="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null)"
50
+ [ -z "$target" ] && exit 0
51
+ req="$(skill_for bash "$target" 2>/dev/null)"
52
+ ;;
53
+ *) exit 0 ;;
54
+ esac
55
+
56
+ [ -z "$req" ] && exit 0
57
+
58
+ # Принимается и голое имя правила, и имя с областью каталога («<каталог>:<имя>»).
59
+ loaded="${TMPDIR:-/tmp}/claude-skill-gate/${sid}.loaded"
60
+ if [ -f "$loaded" ] && grep -qE "^([^:]*:)?$(printf '%s' "$req" | sed 's/[][\.*^$/]/\\&/g')$" "$loaded" 2>/dev/null; then
61
+ exit 0
62
+ fi
63
+
64
+ reason="Отбито гейтом правил: загрузи правило «${req}» инструментом Skill и повтори действие. Для этой области это происходит один раз за сессию."
65
+ jq -n --arg r "$reason" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$r}}' 2>/dev/null \
66
+ || printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Загрузи правило %s и повтори."}}\n' "$req"
67
+
68
+ exit 0
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env bash
2
+ # Запись о загруженном правиле. PostToolUse на инструменте `Skill`.
3
+ #
4
+ # Гейт обязан знать, загружено правило или нет, а спросить об этом ему некого: инструмент о
5
+ # своих прошлых вызовах не рассказывает. Поэтому загрузка записывается здесь, по сессии.
6
+ #
7
+ # Хук только наблюдает: он всегда пропускает и ничего не отбивает.
8
+
9
+ input="$(cat 2>/dev/null)"
10
+ [ -z "$input" ] && exit 0
11
+
12
+ sid="$(printf '%s' "$input" | jq -r '.session_id // "nosession"' 2>/dev/null)"
13
+ skill="$(printf '%s' "$input" | jq -r '.tool_input.skill // empty' 2>/dev/null)"
14
+ [ -z "$skill" ] && exit 0
15
+
16
+ dir="${TMPDIR:-/tmp}/claude-skill-gate"
17
+ mkdir -p "$dir" 2>/dev/null || exit 0
18
+ printf '%s\n' "$skill" >> "$dir/${sid}.loaded" 2>/dev/null
19
+
20
+ exit 0