@rt-tools/agent-kit 0.2.0 → 0.4.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 (203) hide show
  1. package/README.md +235 -18
  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 +286 -0
  9. package/assets/checks/check-board.github.mjs +188 -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 +1007 -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 +100 -0
  21. package/assets/defaults/project.sh +179 -0
  22. package/assets/hooks/browser-device-id.sh +20 -0
  23. package/assets/hooks/browser-guard-device-id.sh +28 -0
  24. package/assets/hooks/browser-guard-no-asking.sh +27 -0
  25. package/assets/hooks/browser-guard-no-listing.sh +18 -0
  26. package/assets/hooks/browser-guard-no-other-drivers.sh +79 -0
  27. package/assets/hooks/browser-guard-require-select.sh +54 -0
  28. package/assets/hooks/commit-msg.sh +26 -0
  29. package/assets/hooks/constitution-index.sh +43 -0
  30. package/assets/hooks/dev-server-guard.sh +115 -0
  31. package/assets/hooks/docs-guard.sh +282 -0
  32. package/assets/hooks/git-guard-delivery.sh +167 -0
  33. package/assets/hooks/git-guard-main.sh +73 -0
  34. package/assets/hooks/git-guard-push-tests.sh +94 -0
  35. package/assets/hooks/glossary-load.sh +23 -0
  36. package/assets/hooks/lint-after-edit.sh +219 -0
  37. package/assets/hooks/qa-dataid-guard.sh +121 -0
  38. package/assets/hooks/reuse-first-guard.sh +154 -0
  39. package/assets/hooks/skill-gate-rearm.sh +23 -0
  40. package/assets/hooks/skill-gate.sh +128 -0
  41. package/assets/hooks/skill-loaded.sh +21 -0
  42. package/assets/hooks/sql-guard.sh +679 -0
  43. package/assets/hooks/task-context-load.sh +100 -0
  44. package/assets/hooks/task-flow-guard.sh +107 -0
  45. package/assets/laws/{access.md → application/access.md} +1 -4
  46. package/assets/laws/{locales.md → application/locales.md} +1 -3
  47. package/assets/laws/application/money.md +41 -0
  48. package/assets/laws/application/ownership.md +32 -0
  49. package/assets/laws/{search-visibility.md → application/search-visibility.md} +1 -1
  50. package/assets/laws/code-structure.md +7 -6
  51. package/assets/laws/delivery.md +53 -3
  52. package/assets/laws/entity-editing.md +49 -55
  53. package/assets/laws/entity-models.md +4 -14
  54. package/assets/laws/frontend-application.md +5 -5
  55. package/assets/laws/lib-imports.md +14 -1
  56. package/assets/laws/lists.md +33 -0
  57. package/assets/laws/navigation.md +40 -0
  58. package/assets/laws/project-documentation.md +17 -8
  59. package/assets/laws/reuse-first.md +26 -21
  60. package/assets/laws/shared-code.md +13 -1
  61. package/assets/laws/verifiability.md +17 -1
  62. package/assets/laws/work-conduct.md +48 -0
  63. package/assets/patterns/admin-lists-screen.md +131 -0
  64. package/assets/patterns/admin-nav-item.md +71 -0
  65. package/assets/patterns/angular-patterns-state.md +101 -0
  66. package/assets/patterns/api-layer-pair.md +88 -0
  67. package/assets/patterns/browser-verification-measure.md +86 -0
  68. package/assets/patterns/browser-verification-stand.md +143 -0
  69. package/assets/patterns/component-structure-new.md +99 -0
  70. package/assets/patterns/dependencies-upgrade.md +65 -0
  71. package/assets/patterns/doc-style-sweep.md +137 -0
  72. package/assets/patterns/doc-style-write.md +109 -0
  73. package/assets/patterns/entity-aside.md +136 -0
  74. package/assets/patterns/entity-models-new.md +124 -0
  75. package/assets/patterns/entity-store.md +91 -0
  76. package/assets/patterns/git-workflow-commit.azure.md +259 -0
  77. package/assets/patterns/git-workflow-commit.github.md +333 -0
  78. package/assets/patterns/git-workflow-commit.gitlab.md +283 -0
  79. package/assets/patterns/git-workflow-merge.md +99 -0
  80. package/assets/patterns/git-workflow-migration.md +88 -0
  81. package/assets/patterns/git-workflow-restart.md +49 -0
  82. package/assets/patterns/lib-layers-move.md +95 -0
  83. package/assets/patterns/lib-layers-new.md +82 -0
  84. package/assets/patterns/ownership-scope-resolve.md +69 -0
  85. package/assets/patterns/permissions-procedure.md +71 -0
  86. package/assets/patterns/platform-access-di.md +84 -0
  87. package/assets/patterns/pricing-quote.md +71 -0
  88. package/assets/patterns/reuse-first-extend.md +73 -0
  89. package/assets/patterns/seo-page.md +104 -0
  90. package/assets/patterns/seo-verify.md +83 -0
  91. package/assets/patterns/shared-code-new.md +86 -0
  92. package/assets/patterns/spec-driven-domain.md +107 -0
  93. package/assets/patterns/spec-driven-rule.md +127 -0
  94. package/assets/patterns/styling-bem-component.md +88 -0
  95. package/assets/patterns/styling-bem-layout.md +73 -0
  96. package/assets/patterns/task-flow-close.md +90 -0
  97. package/assets/patterns/task-flow-resume.md +94 -0
  98. package/assets/patterns/task-flow-start.md +117 -0
  99. package/assets/patterns/testing-e2e.md +92 -0
  100. package/assets/patterns/testing-unit.md +117 -0
  101. package/assets/patterns/translations-key.md +64 -0
  102. package/assets/patterns/ts-procedure.md +65 -0
  103. package/assets/rules/angular-patterns.md +71 -0
  104. package/assets/rules/api-layer.md +71 -0
  105. package/assets/rules/browser-verification.md +87 -0
  106. package/assets/rules/component-structure.md +64 -0
  107. package/assets/rules/dependencies.md +66 -0
  108. package/assets/rules/doc-style.md +103 -0
  109. package/assets/rules/entity-conventions.md +78 -0
  110. package/assets/rules/entity-models.md +70 -0
  111. package/assets/rules/git-workflow.azure.md +116 -0
  112. package/assets/rules/git-workflow.github.md +123 -0
  113. package/assets/rules/git-workflow.gitlab.md +113 -0
  114. package/assets/rules/lib-layers.md +80 -0
  115. package/assets/rules/lists.md +73 -0
  116. package/assets/rules/navigation.md +78 -0
  117. package/assets/rules/ownership-scope.md +63 -0
  118. package/assets/rules/permissions.md +70 -0
  119. package/assets/rules/platform-access.md +77 -0
  120. package/assets/rules/pricing.md +64 -0
  121. package/assets/rules/reuse-first.md +83 -0
  122. package/assets/rules/seo.md +71 -0
  123. package/assets/rules/shared-code.md +70 -0
  124. package/assets/rules/spec-driven.md +135 -0
  125. package/assets/rules/styling-bem.md +74 -0
  126. package/assets/rules/task-flow.md +110 -0
  127. package/assets/rules/testing.md +100 -0
  128. package/assets/rules/translations.md +69 -0
  129. package/assets/rules/typescript-conventions.md +76 -0
  130. package/assets/skills/agent-kit.md +81 -0
  131. package/assets/skills/write-a-skill.md +108 -0
  132. package/assets/templates/gate-map.sh +45 -0
  133. package/assets/templates/implementation.md +44 -0
  134. package/assets/templates/pattern.md +5 -1
  135. package/assets/templates/project.sh +54 -0
  136. package/assets/templates/rule.md +12 -23
  137. package/assets/variants.json +20 -0
  138. package/assets/workflows/feature.js +134 -0
  139. package/assets/workflows/plan.js +150 -0
  140. package/bin/agent-kit.d.ts.map +1 -1
  141. package/bin/agent-kit.js +78 -5
  142. package/bin/agent-kit.js.map +1 -1
  143. package/bin/prompt.d.ts +5 -0
  144. package/bin/prompt.d.ts.map +1 -1
  145. package/bin/prompt.js +19 -7
  146. package/bin/prompt.js.map +1 -1
  147. package/index.d.ts +1 -0
  148. package/index.d.ts.map +1 -1
  149. package/index.js +1 -0
  150. package/index.js.map +1 -1
  151. package/lib/assets.d.ts +14 -1
  152. package/lib/assets.d.ts.map +1 -1
  153. package/lib/assets.js +23 -2
  154. package/lib/assets.js.map +1 -1
  155. package/lib/catalog.d.ts +52 -5
  156. package/lib/catalog.d.ts.map +1 -1
  157. package/lib/catalog.js +104 -16
  158. package/lib/catalog.js.map +1 -1
  159. package/lib/commands.d.ts +22 -1
  160. package/lib/commands.d.ts.map +1 -1
  161. package/lib/commands.js +218 -11
  162. package/lib/commands.js.map +1 -1
  163. package/lib/companion.d.ts +57 -0
  164. package/lib/companion.d.ts.map +1 -0
  165. package/lib/companion.js +60 -0
  166. package/lib/companion.js.map +1 -0
  167. package/lib/config.d.ts +42 -2
  168. package/lib/config.d.ts.map +1 -1
  169. package/lib/config.js +60 -2
  170. package/lib/config.js.map +1 -1
  171. package/lib/freshness.d.ts +14 -0
  172. package/lib/freshness.d.ts.map +1 -0
  173. package/lib/freshness.js +116 -0
  174. package/lib/freshness.js.map +1 -0
  175. package/lib/hooks-map.d.ts +24 -0
  176. package/lib/hooks-map.d.ts.map +1 -0
  177. package/lib/hooks-map.js +72 -0
  178. package/lib/hooks-map.js.map +1 -0
  179. package/lib/integrity.d.ts +36 -0
  180. package/lib/integrity.d.ts.map +1 -0
  181. package/lib/integrity.js +44 -0
  182. package/lib/integrity.js.map +1 -0
  183. package/lib/picker.d.ts +11 -1
  184. package/lib/picker.d.ts.map +1 -1
  185. package/lib/picker.js +44 -6
  186. package/lib/picker.js.map +1 -1
  187. package/lib/stamp.d.ts +2 -5
  188. package/lib/stamp.d.ts.map +1 -1
  189. package/lib/stamp.js +25 -10
  190. package/lib/stamp.js.map +1 -1
  191. package/lib/sync.d.ts +29 -0
  192. package/lib/sync.d.ts.map +1 -1
  193. package/lib/sync.js +78 -4
  194. package/lib/sync.js.map +1 -1
  195. package/lib/variants.d.ts +44 -0
  196. package/lib/variants.d.ts.map +1 -0
  197. package/lib/variants.js +82 -0
  198. package/lib/variants.js.map +1 -0
  199. package/package.json +1 -1
  200. package/rt-tools-agent-kit-0.4.0.tgz +0 -0
  201. package/assets/laws/admin-lists.md +0 -35
  202. package/assets/laws/admin-navigation.md +0 -38
  203. package/rt-tools-agent-kit-0.2.0.tgz +0 -0
@@ -0,0 +1,282 @@
1
+ #!/usr/bin/env bash
2
+ # rt-hook: PreToolUse Edit|Write|MultiEdit|Bash|mcp__webstorm__create_new_file|mcp__webstorm__execute_terminal_command|mcp__webstorm__execute_tool
3
+ # Гард пары «правка и её документ». PreToolUse.
4
+ #
5
+ # Расхождение кода с текстом беззвучно. Ни линтер, ни сборка, ни тесты не читают правила,
6
+ # спеки и README, поэтому текст, описывающий прежнее устройство, живёт дальше и выглядит
7
+ # действующей справкой — тем убедительнее, чем он старше. Ловится это только чтением, и ловит
8
+ # обычно владелец, а не проверка.
9
+ #
10
+ # Гард требует ровно тех пар, где связь механическая и спорить не о чем:
11
+ #
12
+ # правило и его спутник — когда правится раздел с утверждениями, а не «Ловушки»;
13
+ # заведение и удаление либы — README этой либы;
14
+ # переезд файла между либами — README обеих;
15
+ # остальные пары — их называет профиль дерева, функция `rt_docs_pair_for <файл>`.
16
+ #
17
+ # Отдельно — законы. Совпал ли код с законом, машина не знает: правка файла, на который закон
18
+ # ссылается якорем, поэтому не отклоняется, а выносится вопросом владельцу. Тем же вопросом
19
+ # встречается и правка самого закона: закон описывает договорённость о продукте, и менять её
20
+ # молча гард не даёт.
21
+ #
22
+ # Обход — строка `Docs-skip: <причина>` в теле коммита. Причина остаётся в истории и видна при
23
+ # разборе ветки; пустая не принимается.
24
+ #
25
+ # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: не репозиторий, нет разборщика, битый ввод, пустой список файлов —
26
+ # пропуск.
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
+
34
+ decide() {
35
+ jq -n --arg d "$1" --arg r "$2" \
36
+ '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:$d,permissionDecisionReason:$r}}' 2>/dev/null \
37
+ || printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Документ едет тем же коммитом."}}\n'
38
+ exit 0
39
+ }
40
+
41
+ # Профиль дерева: сперва умолчание пакета, поверх него — надстройка проекта, если она есть.
42
+ # Объявленная в надстройке функция замещает умолчание целиком и вправе позвать его обратно
43
+ # суффиксом `_default`.
44
+ rt_hooks_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
45
+ 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
46
+ # shellcheck disable=SC1090
47
+ [ -f "$profile" ] && . "$profile" 2>/dev/null
48
+ done
49
+
50
+ laws_dir="${RT_LAWS_DIR:-docs/constitution}"
51
+ lib_marker="${RT_LIB_MARKER:-project.json}"
52
+
53
+ # ── Правка закона спрашивает владельца ────────────────────────────────────────
54
+ #
55
+ # Спутник рядом с законом — привязка статей к коду, она устаревает при каждом переименовании и
56
+ # правится свободно. Спрашивается только сам текст закона.
57
+ case "$tool" in
58
+ Edit | Write | MultiEdit | NotebookEdit | mcp__webstorm__create_new_file)
59
+ target="$(printf '%s' "$input" | jq -r '.tool_input.file_path // .tool_input.filePath // .tool_input.path // .tool_input.pathInProject // empty' 2>/dev/null)"
60
+ case "$target" in
61
+ */"$laws_dir"/*.implementation.md | "$laws_dir"/*.implementation.md) exit 0 ;;
62
+ */"$laws_dir"/*.md | "$laws_dir"/*.md)
63
+ decide ask "Правка закона: \`${target##*/}\`. Закон описывает договорённость о продукте, а не устройство кода, — назови владельцу, что и почему меняешь, и дождись ответа. Если правка уже согласована, подтверди вызов."
64
+ ;;
65
+ esac
66
+ exit 0
67
+ ;;
68
+ esac
69
+
70
+ # ── Коммит ────────────────────────────────────────────────────────────────────
71
+
72
+ # Терминал среды и универсальный исполнитель кладут команду в то же поле.
73
+ case "$tool" in
74
+ Bash | mcp__webstorm__execute_terminal_command | mcp__webstorm__execute_tool) ;;
75
+ *) exit 0 ;;
76
+ esac
77
+
78
+ cmd="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null)"
79
+
80
+ # Универсальный исполнитель передаёт настоящую команду вложенной строкой. Разбирать надо её,
81
+ # иначе имя команды стоит сразу за кавычкой и ни одно правило до него не дотягивается.
82
+ if [ "$tool" = "mcp__webstorm__execute_tool" ] && command -v perl >/dev/null 2>&1; then
83
+ inner="$(printf '%s' "$cmd" | perl -0ne '
84
+ if (/--command(?:=|\s+)(?:"((?:[^"\\]|\\.)*)"|\x27([^\x27]*)\x27|(.+))/s) {
85
+ print defined $1 ? $1 : (defined $2 ? $2 : $3);
86
+ }
87
+ ' 2>/dev/null)"
88
+ [ -n "$inner" ] && cmd="$inner"
89
+ fi
90
+
91
+ case "$cmd" in
92
+ *git\ commit*) ;;
93
+ *) exit 0 ;;
94
+ esac
95
+
96
+ # Причина обхода остаётся в истории, поэтому обход законен. Пустая строка обходом не считается:
97
+ # «Docs-skip:» без причины — это тот же молчаливый пропуск, только с двоеточием.
98
+ if printf '%s' "$cmd" | grep -qiE 'Docs-skip:[[:space:]]*[^[:space:]"'"'"']{3,}'; then
99
+ exit 0
100
+ fi
101
+
102
+ workdir="$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null)"
103
+ [ -z "$workdir" ] && workdir="${CLAUDE_PROJECT_DIR:-.}"
104
+ cd "$workdir" 2>/dev/null || exit 0
105
+ git rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0
106
+
107
+ # `-a` берёт все отслеживаемые правки, `--amend` — ещё и файлы переписываемого коммита.
108
+ # Считать всегда по индексу значило бы проверять не тот набор, который уедет в историю.
109
+ changes=''
110
+ source_mode='index'
111
+ case "$cmd" in
112
+ *' -a'* | *--all*)
113
+ changes="$(git diff HEAD --name-status -M 2>/dev/null)"
114
+ source_mode='worktree'
115
+ ;;
116
+ *) changes="$(git diff --cached --name-status -M 2>/dev/null)" ;;
117
+ esac
118
+ case "$cmd" in
119
+ *--amend*) changes="$(printf '%s\n%s' "$changes" "$(git show --name-status --format= HEAD 2>/dev/null)")" ;;
120
+ esac
121
+
122
+ changes="$(printf '%s\n' "$changes" | grep -v '^[[:space:]]*$')"
123
+ [ -z "$changes" ] && exit 0
124
+
125
+ # Пути коммита без статусов. Переименование подаётся тремя полями — берём обе стороны.
126
+ paths="$(printf '%s\n' "$changes" | awk -F'\t' '{ for (i = 2; i <= NF; i++) if ($i != "") print $i }' | sort -u)"
127
+
128
+ has_path() { printf '%s\n' "$paths" | grep -qxF "$1"; }
129
+
130
+ # Содержимое таким, каким оно уедет в коммит. Источник тот же, по которому собран список
131
+ # правок: `-a` заберёт рабочее дерево, обычный коммит — индекс. Читать индекс всегда нельзя —
132
+ # при `-a` там лежит прежняя редакция, и гард держал бы коммит, который расхождение и чинит;
133
+ # читать рабочее дерево всегда тоже нельзя — неподготовленная правка в коммит не попадёт.
134
+ content_of() {
135
+ if [ "$source_mode" = 'worktree' ]; then
136
+ cat "$1" 2>/dev/null
137
+ else
138
+ git show ":$1" 2>/dev/null || cat "$1" 2>/dev/null
139
+ fi
140
+ }
141
+
142
+ problems=''
143
+ add() { problems="${problems}
144
+ • $1"; }
145
+
146
+ # ── 1. Правило и его спутник ──────────────────────────────────────────────────
147
+ #
148
+ # Утверждения правила ключуются своим текстом, и переформулировка без правки спутника рвёт
149
+ # связь молча. Сверка спеков это ловит, но гоняется в гейте пуша — здесь та же пара
150
+ # встречается коммитом раньше, пока правка ещё в голове у автора.
151
+ #
152
+ # Раздел с утверждениями — единственное, что связано со спутником; правка «Ловушек» или
153
+ # таблицы «Где это лежит» его не трогает, поэтому сравнивается только этот раздел.
154
+ STATEMENTS='/^## Как закон применяется здесь$/,/^## [^К]/'
155
+ statements_of() { content_of "$1" | awk "$STATEMENTS"; }
156
+
157
+ for rule in $(printf '%s\n' "$paths" | grep -E '/SKILL\.md$'); do
158
+ grep -q '^kind: rule$' "$rule" 2>/dev/null || continue
159
+ companion="${rule%/SKILL.md}/implementation.md"
160
+
161
+ [ -f "$companion" ] || continue
162
+ has_path "$companion" && continue
163
+
164
+ if [ "$(statements_of "$rule")" != "$(git show "HEAD:$rule" 2>/dev/null | awk "$STATEMENTS")" ]; then
165
+ add "утверждения \`$rule\` правятся без спутника — добавь в коммит \`$companion\`"
166
+ fi
167
+ done
168
+
169
+ # ── 2. Пары, которые называет дерево ──────────────────────────────────────────
170
+ #
171
+ # Контракт и спек домена, гард и его сценарии — что именно, знает профиль: связь у каждого
172
+ # дерева своя, а механика одна.
173
+ if command -v rt_docs_pair_for >/dev/null 2>&1; then
174
+ while IFS= read -r file; do
175
+ [ -z "$file" ] && continue
176
+ want="$(rt_docs_pair_for "$file" 2>/dev/null)"
177
+ [ -z "$want" ] && continue
178
+ # Пара считается приехавшей, если хоть один файл коммита подходит под образец.
179
+ printf '%s\n' "$paths" | grep -qE "$want" && continue
180
+ add "\`$file\` правится без документа — тем же коммитом ждёт \`$want\`"
181
+ done <<EOF
182
+ $paths
183
+ EOF
184
+ fi
185
+
186
+ # ── 3. Либа и её README ───────────────────────────────────────────────────────
187
+
188
+ lib_root() {
189
+ dir="${1%/*}"
190
+ while [ -n "$dir" ] && [ "$dir" != "." ]; do
191
+ [ -f "$dir/$lib_marker" ] && { printf '%s' "$dir"; return 0; }
192
+ case "$dir" in */*) dir="${dir%/*}" ;; *) dir='' ;; esac
193
+ done
194
+
195
+ return 1
196
+ }
197
+
198
+ need_readme=''
199
+ want_readme() {
200
+ root="$1"
201
+ [ -n "$root" ] || return 0
202
+ case " $need_readme " in *" $root "*) return 0 ;; esac
203
+ need_readme="$need_readme $root"
204
+ }
205
+
206
+ # Заведение и удаление либы: README — единственное место, где написано, что в ней лежит и кто
207
+ # её зовёт, и раскладку она переживает только вместе с правкой этого текста.
208
+ while IFS="$(printf '\t')" read -r status first second; do
209
+ case "$status" in
210
+ A* | D*)
211
+ case "$first" in */"$lib_marker") want_readme "${first%/"$lib_marker"}" ;; esac
212
+ ;;
213
+ R*)
214
+ # Переезд файла меняет обе стороны: у одной либы он пропал из состава, у другой
215
+ # появился.
216
+ from="$(lib_root "$first")" && to="$(lib_root "$second")"
217
+ if [ -n "$from" ] && [ -n "$to" ] && [ "$from" != "$to" ]; then
218
+ want_readme "$from"
219
+ want_readme "$to"
220
+ fi
221
+ ;;
222
+ esac
223
+ done <<EOF
224
+ $changes
225
+ EOF
226
+
227
+ # Переезд, поданный парой «удалено там, добавлено тут»: переименованием считается не всё, что
228
+ # им является, — правка содержимого при переносе сбивает поиск.
229
+ moved_added="$(printf '%s\n' "$changes" | awk -F'\t' '$1 ~ /^A/ { n = split($2, p, "/"); print p[n] "\t" $2 }' | sort -u)"
230
+ moved_deleted="$(printf '%s\n' "$changes" | awk -F'\t' '$1 ~ /^D/ { n = split($2, p, "/"); print p[n] "\t" $2 }' | sort -u)"
231
+ if [ -n "$moved_added" ] && [ -n "$moved_deleted" ]; then
232
+ while IFS="$(printf '\t')" read -r base added; do
233
+ [ -n "$base" ] || continue
234
+ gone="$(printf '%s\n' "$moved_deleted" | awk -F'\t' -v b="$base" '$1 == b { print $2; exit }')"
235
+ [ -n "$gone" ] || continue
236
+ to="$(lib_root "$added")" || continue
237
+ from="$(lib_root "$gone")" || continue
238
+ [ "$from" = "$to" ] && continue
239
+ want_readme "$from"
240
+ want_readme "$to"
241
+ done <<EOF
242
+ $moved_added
243
+ EOF
244
+ fi
245
+
246
+ for root in $need_readme; do
247
+ [ -d "$root" ] || continue # либу удалили целиком — править в ней нечего
248
+ has_path "$root/README.md" || add "состав либы \`$root\` изменился без правки \`$root/README.md\`"
249
+ done
250
+
251
+ if [ -n "$problems" ]; then
252
+ decide deny "BLOCKED: коммит правит код, но не документ, который его описывает.
253
+ ${problems}
254
+
255
+ Текст правится в той же ветке, что и код: документ, разошедшийся с деревом, выглядит действующей справкой и уводит следующего читателя. Если правка документа здесь действительно не нужна — назови причину строкой \`Docs-skip: <причина>\` в теле коммита, она останется в истории."
256
+ fi
257
+
258
+ # ── 4. Законы ─────────────────────────────────────────────────────────────────
259
+ #
260
+ # Отклонять нечего: сошлась ли правка с законом, видно только чтением. Но место, где статья
261
+ # закона исполняется, названо якорем, и правка ровно этого файла — единственный момент, когда
262
+ # сверку ещё дёшево сделать.
263
+ #
264
+ # Слоёв законов два: общий в корне, закон приложения — в каталоге под ним. Образец принимает
265
+ # оба, иначе правка предметного закона считалась бы правкой мимо законов.
266
+ if [ -d "$laws_dir" ] && ! printf '%s\n' "$paths" | grep -qE "^${laws_dir}/([^/]+/)?[^/]+\.md$"; then
267
+ touched=''
268
+ for anchor in $(grep -ohE '`[A-Za-z0-9_./@-]+\.(ts|mjs|html|scss|prisma|proto|json)(:[A-Za-z0-9_#.-]+)?`' "$laws_dir"/*.implementation.md 2>/dev/null \
269
+ | tr -d '`' | sed 's/:.*//' | grep '/' | sort -u); do
270
+ has_path "$anchor" && touched="${touched}
271
+ • \`$anchor\`"
272
+ done
273
+
274
+ if [ -n "$touched" ]; then
275
+ decide ask "Правка задевает места, где исполняются статьи законов:
276
+ ${touched}
277
+
278
+ Законы лежат в \`${laws_dir}/\`, привязка — в спутниках рядом. Прочитай закон и скажи владельцу, сошлась ли с ним правка: если правка его уточняет или ему противоречит, закон правится в этой же ветке, но только с его ведома. Если закон не изменился — подтверди вызов."
279
+ fi
280
+ fi
281
+
282
+ exit 0
@@ -0,0 +1,167 @@
1
+ #!/usr/bin/env bash
2
+ # rt-hook: PreToolUse Bash|mcp__webstorm__execute_terminal_command|mcp__webstorm__execute_tool
3
+ # Гард поставки. PreToolUse на заведении ветки и открытии заявки на слияние.
4
+ #
5
+ # Закон о поставке требует трёх вещей, которых обычно не проверяет ничто: правка начинается с
6
+ # задачи, видимой в очереди работ; задача, ветка и отчёт несут один номер; у задачи есть
7
+ # исполнитель. Держатся они памятью — и не удерживаются: задачи стоят вне очереди, исполнитель
8
+ # не проставлен, а большинство влитых заявок приходит с веток, за которыми задачи не стояло
9
+ # вовсе.
10
+ #
11
+ # Гард стоит в двух точках, и в каждой требует того, что в этот момент исправимо:
12
+ #
13
+ # заведение ветки — имя с номером разбирается на месте; имя без номера пропускается:
14
+ # локальная ветка под пробу законна, в главную она не поедет, потому что заявка с неё
15
+ # не откроется;
16
+ # открытие заявки — ветка обязана нести номер, заголовок обязан начинаться с того же номера,
17
+ # а задача — быть открытой, стоять в очереди работ и иметь исполнителя.
18
+ #
19
+ # Два яруса. Формат — номер в имени ветки, номер в заголовке, их совпадение — читается из
20
+ # текста команды и работает всегда. Состояние задачи требует сети: нет её, нет помощника или
21
+ # нет токена — ярус пропускается, потому что проверять нечем.
22
+ #
23
+ # Что здесь чем зовётся, знает профиль дерева:
24
+ # rt_task_branch_ok — форма имени ветки под задачу;
25
+ # RT_TASK_TITLE_RE — форма номера в заголовке заявки;
26
+ # rt_task_state — состояние задачи одним объектом (existsize, open, onBoard, assigned,
27
+ # numbered); молчание значит «спросить некого»;
28
+ # RT_TASK_NEW_CMD — чем заводится задача;
29
+ # RT_BOARD_CHECK_CMD — чем сверяется очередь работ;
30
+ # RT_TASK_BOT — учётная запись, которую ставят исполнителем.
31
+ # Отказ называет и то, что не так, и чем это чинится: отказ без действия обходят, а не исполняют.
32
+ #
33
+ # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: не репозиторий, нет разборщика, битый ввод, нет профиля — пропуск.
34
+
35
+ input="$(cat 2>/dev/null)"
36
+ [ -z "$input" ] && exit 0
37
+ command -v jq >/dev/null 2>&1 || exit 0
38
+
39
+ tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
40
+ case "$tool" in
41
+ # Терминал среды и универсальный исполнитель кладут команду в то же поле.
42
+ Bash | mcp__webstorm__execute_terminal_command | mcp__webstorm__execute_tool) ;;
43
+ *) exit 0 ;;
44
+ esac
45
+
46
+ cmd="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null)"
47
+ [ -z "$cmd" ] && exit 0
48
+
49
+ # Универсальный исполнитель передаёт настоящую команду вложенной строкой. Разбирать надо её,
50
+ # а не обёртку.
51
+ if [ "$tool" = "mcp__webstorm__execute_tool" ] && command -v perl >/dev/null 2>&1; then
52
+ inner="$(printf '%s' "$cmd" | perl -0ne '
53
+ if (/--command(?:=|\s+)(?:"((?:[^"\\]|\\.)*)"|\x27([^\x27]*)\x27|(.+))/s) {
54
+ print defined $1 ? $1 : (defined $2 ? $2 : $3);
55
+ }
56
+ ' 2>/dev/null)"
57
+ [ -n "$inner" ] && cmd="$inner"
58
+ fi
59
+
60
+ workdir="$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null)"
61
+ [ -z "$workdir" ] && workdir="${CLAUDE_PROJECT_DIR:-.}"
62
+ cd "$workdir" 2>/dev/null || exit 0
63
+ git rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0
64
+
65
+ root="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null)}"
66
+
67
+ # Профиль дерева: сперва умолчание пакета, поверх него — надстройка проекта, если она есть.
68
+ # Объявленная в надстройке функция замещает умолчание целиком и вправе позвать его обратно
69
+ # суффиксом `_default`. Нет ни того ни другого — хук пропускает: пустой гард лучше гарда,
70
+ # отбивающего наугад.
71
+ rt_hooks_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
72
+ for profile in "$rt_hooks_dir/../rt-kit/defaults/project.sh" "$rt_hooks_dir/../defaults/project.sh" "$root/.claude/rt-kit/defaults/project.sh" "$root/.claude/rt-kit/project.sh"; do
73
+ # shellcheck disable=SC1090
74
+ [ -f "$profile" ] && . "$profile" 2>/dev/null
75
+ done
76
+ command -v rt_task_branch_ok >/dev/null 2>&1 || exit 0
77
+
78
+ title_re="${RT_TASK_TITLE_RE:-^\[[A-Za-z]+-[0-9]+\][[:space:]]+[^[:space:]]}"
79
+ task_new="${RT_TASK_NEW_CMD:-npm run task:new}"
80
+ board_check="${RT_BOARD_CHECK_CMD:-npm run check:board}"
81
+ task_bot="${RT_TASK_BOT:-}"
82
+
83
+ deny() {
84
+ jq -n --arg r "$1" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$r}}' 2>/dev/null \
85
+ || printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Гард поставки."}}\n'
86
+ exit 0
87
+ }
88
+
89
+ check_task() {
90
+ number="$1"
91
+ where="$2"
92
+ command -v rt_task_state >/dev/null 2>&1 || return 0
93
+ state="$(cd "$root" && rt_task_state "$number" 2>/dev/null)" || return 0
94
+ [ -z "$state" ] && return 0
95
+
96
+ printf '%s' "$state" | jq -e '.exists' >/dev/null 2>&1 \
97
+ || deny "BLOCKED: ${where} ссылается на задачу #${number}, которой нет. Проверь номер или заведи задачу — ${task_new}."
98
+ printf '%s' "$state" | jq -e '.open' >/dev/null 2>&1 \
99
+ || deny "BLOCKED: задача #${number} закрыта, а у задачи одна ветка. Работа за закрытой задачей заводится новой задачей — ${task_new}."
100
+ printf '%s' "$state" | jq -e '.onBoard' >/dev/null 2>&1 \
101
+ || deny "BLOCKED: задачи #${number} нет в очереди работ — правка за ней не видна. Очередь к репозиторию не привязана и задачу сама не забирает; добавь её и сверь — ${board_check}."
102
+ printf '%s' "$state" | jq -e '.assigned' >/dev/null 2>&1 \
103
+ || deny "BLOCKED: у задачи #${number} нет исполнителя — по очереди работ не видно, кто её взял. Поставь исполнителя${task_bot:+: }${task_bot}."
104
+ printf '%s' "$state" | jq -e '.numbered' >/dev/null 2>&1 \
105
+ || deny "BLOCKED: заголовок задачи #${number} не начинается с её номера — одну работу придётся узнавать по тексту названия. Поправь заголовок и сверь очередь — ${board_check}."
106
+
107
+ return 0
108
+ }
109
+
110
+ # --- заведение ветки ---------------------------------------------------------------------
111
+ branch_arg=''
112
+ if printf '%s' "$cmd" | grep -qE '(^|[;&|[:space:]])git[[:space:]]+(checkout[[:space:]]+-b|switch[[:space:]]+-c)[[:space:]]'; then
113
+ branch_arg="$(printf '%s' "$cmd" | sed -nE 's/.*git[[:space:]]+(checkout[[:space:]]+-b|switch[[:space:]]+-c)[[:space:]]+([^[:space:];&|]+).*/\2/p' | head -1)"
114
+ branch_arg="${branch_arg%\'}"; branch_arg="${branch_arg#\'}"
115
+ branch_arg="${branch_arg%\"}"; branch_arg="${branch_arg#\"}"
116
+ fi
117
+
118
+ if [ -n "$branch_arg" ]; then
119
+ # Имя, притворяющееся веткой под задачу, но не совпадающее с формой, — это промах в имени,
120
+ # а не осознанная беззадачная ветка. Ловится до первого коммита.
121
+ if printf '%s' "$branch_arg" | grep -qiE '^[A-Za-z]+-[0-9]+'; then
122
+ rt_task_branch_ok "$branch_arg" \
123
+ || deny "BLOCKED: имя ветки «${branch_arg}» не той формы, что принята здесь. Номер у ветки тот же, что у задачи и у заголовка заявки на слияние."
124
+ check_task "$(printf '%s' "$branch_arg" | sed -nE 's/^[A-Za-z]+-([0-9]+).*/\1/p')" "ветка «${branch_arg}»"
125
+ fi
126
+ # Ветка без номера законна и живёт локально: заявка с неё не откроется.
127
+ exit 0
128
+ fi
129
+
130
+ # --- открытие заявки на слияние ----------------------------------------------------------
131
+ case "$cmd" in
132
+ *gh\ pr\ create* | *glab\ mr\ create* | *az\ repos\ pr\ create*) ;;
133
+ *) exit 0 ;;
134
+ esac
135
+
136
+ branch="$(git branch --show-current 2>/dev/null)"
137
+ [ -z "$branch" ] && exit 0 # открепившийся HEAD — не про этот случай
138
+
139
+ # Локальная ветка без номера законна, а заявка с неё — уже нет: правка, доезжающая до главной
140
+ # ветки, начинается с задачи. Это единственное место, где беззадачная ветка упирается.
141
+ rt_task_branch_ok "$branch" \
142
+ || deny "BLOCKED: заявка с ветки «${branch}», за которой не стоит задачи. Правка начинается с задачи, видимой в очереди работ: заведи её — ${task_new} — и перенеси работу в ветку с её номером."
143
+
144
+ number="$(printf '%s' "$branch" | sed -nE 's/^[A-Za-z]+-([0-9]+).*/\1/p')"
145
+
146
+ title=''
147
+ if command -v perl >/dev/null 2>&1; then
148
+ title="$(printf '%s' "$cmd" | perl -0ne '
149
+ if (/(?:--title|-t)(?:=|\s+)(?:"((?:[^"\\]|\\.)*)"|\x27([^\x27]*)\x27|(\S+))/s) {
150
+ print defined $1 ? $1 : (defined $2 ? $2 : $3);
151
+ }
152
+ ' 2>/dev/null)"
153
+ fi
154
+
155
+ if [ -n "$title" ]; then
156
+ printf '%s' "$title" | grep -qE "$title_re" \
157
+ || deny "BLOCKED: заголовок заявки не начинается с номера задачи. В списке заявок тела не видно, а строка связи живёт именно там — без номера в заголовке отчёт с задачей не сопоставить."
158
+ title_number="$(printf '%s' "$title" | sed -nE 's/^\[[A-Za-z]+-([0-9]+)\].*/\1/p')"
159
+ if [ -n "$number" ] && [ -n "$title_number" ]; then
160
+ [ "$title_number" = "$number" ] \
161
+ || deny "BLOCKED: в заголовке заявки номер ${title_number}, у ветки — ${number}. Задача, ветка и отчёт несут один и тот же номер."
162
+ fi
163
+ fi
164
+
165
+ check_task "$number" "заявка с ветки «${branch}»"
166
+
167
+ exit 0
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env bash
2
+ # rt-hook: PreToolUse Bash|mcp__webstorm__execute_terminal_command|mcp__webstorm__execute_tool
3
+ # Гард главной ветки. PreToolUse на вызове коммита.
4
+ #
5
+ # Коммит в главную ветку минует ветку, PR и разбор, а поставка построена на них целиком —
6
+ # правило `git-workflow`. Прямой коммит туда почти всегда промах: «остался на главной после
7
+ # слияния предыдущего PR».
8
+ #
9
+ # Имя главной ветки не зашито строкой: сначала спрашивается указатель удалённого репозитория,
10
+ # затем пробуются существующие `origin/main` и `origin/master`, и лишь в конце берётся `main`.
11
+ #
12
+ # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: не репозиторий, нет гита, открепившийся HEAD, битый ввод — пропуск.
13
+ # Сломанный гард не должен мешать работать.
14
+
15
+ input="$(cat 2>/dev/null)"
16
+ [ -z "$input" ] && exit 0
17
+
18
+ tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
19
+ # Терминал среды разработки исполняет ту же командную строку и кладёт её в то же поле. Пока
20
+ # гард проверял только оболочку, весь его смысл обходился сменой инструмента.
21
+ case "$tool" in
22
+ Bash | mcp__webstorm__execute_terminal_command | mcp__webstorm__execute_tool) ;;
23
+ *) exit 0 ;;
24
+ esac
25
+
26
+ cmd="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null)"
27
+
28
+ # Универсальный исполнитель среды передаёт настоящую команду вложенной строкой. Разбирать надо
29
+ # её, а не обёртку: иначе имя команды стоит сразу за кавычкой и ни одно правило до него не
30
+ # дотягивается.
31
+ if [ "$tool" = "mcp__webstorm__execute_tool" ] && command -v perl >/dev/null 2>&1; then
32
+ inner="$(printf '%s' "$cmd" | perl -0ne '
33
+ if (/--command(?:=|\s+)(?:"((?:[^"\\]|\\.)*)"|\x27([^\x27]*)\x27|(.+))/s) {
34
+ print defined $1 ? $1 : (defined $2 ? $2 : $3);
35
+ }
36
+ ' 2>/dev/null)"
37
+ [ -n "$inner" ] && cmd="$inner"
38
+ fi
39
+ case "$cmd" in
40
+ *git\ commit*) ;;
41
+ *) exit 0 ;;
42
+ esac
43
+
44
+ # Коммит выполнится в рабочем каталоге вызова, поэтому и ветку смотрим там же; корень проекта
45
+ # — запасной вариант, и он важен для отдельного рабочего дерева, где ветка своя.
46
+ workdir="$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null)"
47
+ [ -z "$workdir" ] && workdir="${CLAUDE_PROJECT_DIR:-.}"
48
+ cd "$workdir" 2>/dev/null || exit 0
49
+
50
+ git rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0
51
+
52
+ branch="$(git branch --show-current 2>/dev/null)"
53
+ [ -z "$branch" ] && exit 0 # открепившийся HEAD — не про этот случай
54
+
55
+ default="$(git symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null | sed 's#^origin/##')"
56
+ if [ -z "$default" ]; then
57
+ for candidate in main master; do
58
+ if git show-ref --verify --quiet "refs/remotes/origin/$candidate" 2>/dev/null; then
59
+ default="$candidate"
60
+ break
61
+ fi
62
+ done
63
+ fi
64
+ [ -z "$default" ] && default="main"
65
+
66
+ [ "$branch" = "$default" ] || exit 0
67
+
68
+ reason="Отбито: коммит прямо в «${default}». Работа едет через ветку и PR — правило git-workflow. Заведи ветку отдельным вызовом и коммить в неё: подготовленные изменения при этом сохранятся. Если коммит в ${default} действительно нужен — спроси владельца, сам не обходи."
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
+ # rt-hook: PreToolUse Bash|mcp__webstorm__execute_terminal_command|mcp__webstorm__execute_tool
3
+ # Гард проверок перед пушем. PreToolUse на вызове пуша.
4
+ #
5
+ # Пуш — это вход в конвейер: слияние в главную ветку запускает выкатку, и всё, что не
6
+ # проверено локально, проверяется уже на проде. Сюда дважды подряд уезжают правки, зелёные в
7
+ # выборочном прогоне и красные в конвейере: один раз прогонялись только сквозные спеки, другой
8
+ # — только затронутый проект.
9
+ #
10
+ # Гард не верит на слово: он сам гоняет то, что перечислил профиль дерева, и пускает пуш только
11
+ # при нулевом коде возврата. Если прогонщик кэширует результат, набор на неизменившемся дереве
12
+ # занимает секунды, а после правки гоняется заново.
13
+ #
14
+ # Что гонять, знает профиль: функция `rt_push_checks <база>` — по команде на строку. База —
15
+ # ветка, относительно которой считается вклад; пустая означает, что удалённого нет. Функция
16
+ # зовётся из каталога, откуда идёт пуш, и вправе решать по нему сама: шаг, проверки которого в
17
+ # дереве нет, она просто не печатает. Нет профиля или нет функции — гард пропускает: список
18
+ # проверок пакет выдумать не может.
19
+ #
20
+ # База берётся у удалённого, а не у локальной главной ветки: локальная отстаёт или расходится
21
+ # молча. Это уже случалось — локальная стояла на слиянии, стёртом из истории силовым пушем, и
22
+ # набор от неё посчитался бы не тот. Нет сети или нет удалённого — база пустая, и профиль
23
+ # откатывается на полный прогон: гейт может оказаться строже нужного, но НИКОГДА не слабее.
24
+ #
25
+ # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: не репозиторий, битый ввод, нет профиля — пропуск.
26
+
27
+ input="$(cat 2>/dev/null)"
28
+ [ -z "$input" ] && exit 0
29
+
30
+ tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
31
+ case "$tool" in
32
+ # Терминал среды и универсальный исполнитель кладут команду в то же поле.
33
+ Bash | mcp__webstorm__execute_terminal_command | mcp__webstorm__execute_tool) ;;
34
+ *) exit 0 ;;
35
+ esac
36
+
37
+ cmd="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null)"
38
+ case "$cmd" in
39
+ *git\ push*) ;;
40
+ *) exit 0 ;;
41
+ esac
42
+
43
+ # Пробный пуш ничего не отправляет: гонять ради него весь набор незачем.
44
+ case "$cmd" in
45
+ *--dry-run*) exit 0 ;;
46
+ esac
47
+
48
+ workdir="$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null)"
49
+ [ -z "$workdir" ] && workdir="${CLAUDE_PROJECT_DIR:-.}"
50
+ cd "$workdir" 2>/dev/null || exit 0
51
+ git rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0
52
+ [ -f package.json ] || exit 0
53
+
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
63
+ command -v rt_push_checks >/dev/null 2>&1 || exit 0
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
+
71
+ failed=""
72
+ output=""
73
+ while IFS= read -r check; do
74
+ [ -z "$check" ] && continue
75
+ out="$(eval "$check" 2>&1)" && continue
76
+ failed="$check"
77
+ output="$out"
78
+ break
79
+ done <<EOF
80
+ $(rt_push_checks "$base")
81
+ EOF
82
+
83
+ [ -z "$failed" ] && exit 0
84
+
85
+ # Хвост вывода, а не весь: у прогонщика он длинный, а нужна причина отказа.
86
+ tail_out="$(printf '%s' "$output" | tail -n 40 | tr -d '\000')"
87
+ reason="BLOCKED: пуш без зелёного локального прогона. «${failed}» упала — почини и пушь снова, обходить гард нельзя. Пуш — вход в конвейер, и красное отсюда проверяется уже на проде. Хвост вывода:
88
+
89
+ ${tail_out}"
90
+
91
+ jq -n --arg r "$reason" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$r}}' 2>/dev/null \
92
+ || printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Проверки перед пушем не прошли."}}\n'
93
+
94
+ exit 0