@rt-tools/agent-kit 0.3.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 (201) 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 +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 +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 +86 -29
  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/lint-after-edit.sh +155 -30
  37. package/assets/hooks/qa-dataid-guard.sh +72 -32
  38. package/assets/hooks/reuse-first-guard.sh +105 -34
  39. package/assets/hooks/skill-gate-rearm.sh +1 -0
  40. package/assets/hooks/skill-gate.sh +75 -15
  41. package/assets/hooks/skill-loaded.sh +1 -0
  42. package/assets/hooks/sql-guard.sh +606 -56
  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 +29 -22
  66. package/assets/patterns/api-layer-pair.md +40 -30
  67. package/assets/patterns/browser-verification-measure.md +41 -38
  68. package/assets/patterns/browser-verification-stand.md +106 -42
  69. package/assets/patterns/component-structure-new.md +33 -32
  70. package/assets/patterns/dependencies-upgrade.md +65 -0
  71. package/assets/patterns/doc-style-sweep.md +65 -28
  72. package/assets/patterns/doc-style-write.md +36 -33
  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 +42 -25
  80. package/assets/patterns/git-workflow-migration.md +61 -31
  81. package/assets/patterns/git-workflow-restart.md +20 -20
  82. package/assets/patterns/lib-layers-move.md +50 -32
  83. package/assets/patterns/lib-layers-new.md +41 -29
  84. package/assets/patterns/ownership-scope-resolve.md +69 -0
  85. package/assets/patterns/permissions-procedure.md +35 -33
  86. package/assets/patterns/platform-access-di.md +39 -25
  87. package/assets/patterns/pricing-quote.md +71 -0
  88. package/assets/patterns/reuse-first-extend.md +22 -22
  89. package/assets/patterns/seo-page.md +52 -40
  90. package/assets/patterns/seo-verify.md +48 -29
  91. package/assets/patterns/shared-code-new.md +37 -31
  92. package/assets/patterns/spec-driven-domain.md +44 -37
  93. package/assets/patterns/spec-driven-rule.md +55 -40
  94. package/assets/patterns/styling-bem-component.md +43 -32
  95. package/assets/patterns/styling-bem-layout.md +30 -24
  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 +53 -51
  100. package/assets/patterns/testing-unit.md +70 -46
  101. package/assets/patterns/translations-key.md +32 -19
  102. package/assets/patterns/ts-procedure.md +24 -25
  103. package/assets/rules/angular-patterns.md +46 -27
  104. package/assets/rules/api-layer.md +46 -28
  105. package/assets/rules/browser-verification.md +66 -48
  106. package/assets/rules/component-structure.md +43 -27
  107. package/assets/rules/dependencies.md +66 -0
  108. package/assets/rules/doc-style.md +81 -39
  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 +56 -30
  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 +43 -25
  119. package/assets/rules/platform-access.md +57 -29
  120. package/assets/rules/pricing.md +64 -0
  121. package/assets/rules/reuse-first.md +57 -43
  122. package/assets/rules/seo.md +51 -30
  123. package/assets/rules/shared-code.md +51 -26
  124. package/assets/rules/spec-driven.md +96 -50
  125. package/assets/rules/styling-bem.md +54 -39
  126. package/assets/rules/task-flow.md +110 -0
  127. package/assets/rules/testing.md +78 -47
  128. package/assets/rules/translations.md +48 -31
  129. package/assets/rules/typescript-conventions.md +57 -27
  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 +23 -15
  133. package/assets/templates/implementation.md +14 -8
  134. package/assets/templates/pattern.md +1 -1
  135. package/assets/templates/project.sh +32 -19
  136. package/assets/templates/rule.md +1 -1
  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 +8 -3
  152. package/lib/assets.d.ts.map +1 -1
  153. package/lib/assets.js +13 -3
  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 +202 -14
  162. package/lib/commands.js.map +1 -1
  163. package/lib/companion.d.ts +5 -1
  164. package/lib/companion.d.ts.map +1 -1
  165. package/lib/companion.js +29 -2
  166. package/lib/companion.js.map +1 -1
  167. package/lib/config.d.ts +26 -9
  168. package/lib/config.d.ts.map +1 -1
  169. package/lib/config.js +41 -15
  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/sync.d.ts +26 -0
  188. package/lib/sync.d.ts.map +1 -1
  189. package/lib/sync.js +59 -4
  190. package/lib/sync.js.map +1 -1
  191. package/lib/variants.d.ts +44 -0
  192. package/lib/variants.d.ts.map +1 -0
  193. package/lib/variants.js +82 -0
  194. package/lib/variants.js.map +1 -0
  195. package/package.json +1 -1
  196. package/rt-tools-agent-kit-0.4.0.tgz +0 -0
  197. package/assets/laws/admin-lists.md +0 -35
  198. package/assets/laws/admin-navigation.md +0 -38
  199. package/assets/patterns/git-workflow-commit.md +0 -175
  200. package/assets/rules/git-workflow.md +0 -106
  201. 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 Edit|Write|MultiEdit|Bash|mcp__webstorm__create_new_file|mcp__webstorm__execute_terminal_command|mcp__webstorm__execute_tool
2
3
  # Гард пары «правка и её документ». PreToolUse.
3
4
  #
4
5
  # Расхождение кода с текстом беззвучно. Ни линтер, ни сборка, ни тесты не читают правила,
@@ -6,13 +7,17 @@
6
7
  # действующей справкой — тем убедительнее, чем он старше. Ловится это только чтением, и ловит
7
8
  # обычно владелец, а не проверка.
8
9
  #
9
- # Гард требует ровно тех пар, где связь механическая и спорить не о чем. Какие это пары, знает
10
- # профиль проекта: {{projectProfile}}, функция `rt_docs_pair_for <файл>` — печатает образец
11
- # пути, который обязан ехать тем же коммитом, или молчит.
10
+ # Гард требует ровно тех пар, где связь механическая и спорить не о чем:
12
11
  #
13
- # Отдельнозаконы. Совпал ли код с законом, машина не знает, поэтому правка самого закона не
14
- # отклоняется, а выносится вопросом владельцу: закон описывает договорённость о продукте, и
15
- # менять её молча гард не даёт.
12
+ # правило и его спутник когда правится раздел с утверждениями, а не «Ловушки»;
13
+ # заведение и удаление либы — README этой либы;
14
+ # переезд файла между либами README обеих;
15
+ # остальные пары — их называет профиль дерева, функция `rt_docs_pair_for <файл>`.
16
+ #
17
+ # Отдельно — законы. Совпал ли код с законом, машина не знает: правка файла, на который закон
18
+ # ссылается якорем, поэтому не отклоняется, а выносится вопросом владельцу. Тем же вопросом
19
+ # встречается и правка самого закона: закон описывает договорённость о продукте, и менять её
20
+ # молча гард не даёт.
16
21
  #
17
22
  # Обход — строка `Docs-skip: <причина>` в теле коммита. Причина остаётся в истории и видна при
18
23
  # разборе ветки; пустая не принимается.
@@ -33,32 +38,64 @@ decide() {
33
38
  exit 0
34
39
  }
35
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
+
36
53
  # ── Правка закона спрашивает владельца ────────────────────────────────────────
54
+ #
55
+ # Спутник рядом с законом — привязка статей к коду, она устаревает при каждом переименовании и
56
+ # правится свободно. Спрашивается только сам текст закона.
37
57
  case "$tool" in
38
- Edit | Write | MultiEdit | mcp__webstorm__create_new_file)
39
- target="$(printf '%s' "$input" | jq -r '.tool_input.file_path // .tool_input.pathInProject // empty' 2>/dev/null)"
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)"
40
60
  case "$target" in
41
- */{{lawsDir}}/*.md | {{lawsDir}}/*.md)
42
- decide ask "Правка закона: \`${target##*/}\`. Закон описывает договорённость о продукте, а не устройство кода, — назови владельцу, что и почему меняешь, и дождись ответа. Если правка уже согласована, подтверди вызов." ;;
61
+ */"$laws_dir"/*.implementation.md | "$laws_dir"/*.implementation.md) exit 0 ;;
62
+ */"$laws_dir"/*.md | "$laws_dir"/*.md)
63
+ decide ask "Правка закона: \`${target##*/}\`. Закон описывает договорённость о продукте, а не устройство кода, — назови владельцу, что и почему меняешь, и дождись ответа. Если правка уже согласована, подтверди вызов."
64
+ ;;
43
65
  esac
44
- exit 0 ;;
66
+ exit 0
67
+ ;;
45
68
  esac
46
69
 
47
- # ── Коммит: пара обязана ехать тем же коммитом ───────────────────────────────
70
+ # ── Коммит ────────────────────────────────────────────────────────────────────
71
+
72
+ # Терминал среды и универсальный исполнитель кладут команду в то же поле.
48
73
  case "$tool" in
49
74
  Bash | mcp__webstorm__execute_terminal_command | mcp__webstorm__execute_tool) ;;
50
75
  *) exit 0 ;;
51
76
  esac
52
77
 
53
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
+
54
91
  case "$cmd" in
55
92
  *git\ commit*) ;;
56
93
  *) exit 0 ;;
57
94
  esac
58
95
 
59
- # Обход с причиной. Пустая причина не принимается: «Docs-skip:» без слов означает только то,
60
- # что строку дописали, чтобы пройти гард.
61
- if printf '%s' "$cmd" | grep -qE 'Docs-skip:[[:space:]]*[^[:space:]]'; then
96
+ # Причина обхода остаётся в истории, поэтому обход законен. Пустая строка обходом не считается:
97
+ # «Docs-skip:» без причины это тот же молчаливый пропуск, только с двоеточием.
98
+ if printf '%s' "$cmd" | grep -qiE 'Docs-skip:[[:space:]]*[^[:space:]"'"'"']{3,}'; then
62
99
  exit 0
63
100
  fi
64
101
 
@@ -67,30 +104,179 @@ workdir="$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null)"
67
104
  cd "$workdir" 2>/dev/null || exit 0
68
105
  git rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0
69
106
 
70
- profile="${CLAUDE_PROJECT_DIR:-.}/{{projectProfile}}"
71
- [ -f "$profile" ] || exit 0
72
- # shellcheck disable=SC1090
73
- . "$profile" 2>/dev/null || exit 0
74
- command -v rt_docs_pair_for >/dev/null 2>&1 || exit 0
75
-
76
- staged="$(git diff --cached --name-only 2>/dev/null)"
77
- [ -z "$staged" ] && exit 0
78
-
79
- missing=""
80
- while IFS= read -r file; do
81
- [ -z "$file" ] && continue
82
- want="$(rt_docs_pair_for "$file" 2>/dev/null)"
83
- [ -z "$want" ] && continue
84
- # Пара считается приехавшей, если хоть один подготовленный файл подходит под образец.
85
- printf '%s\n' "$staged" | grep -qE "$want" && continue
86
- missing="${missing} ${file} ждёт документ: ${want}"$'\n'
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
87
223
  done <<EOF
88
- $staged
224
+ $changes
89
225
  EOF
90
226
 
91
- [ -z "$missing" ] && exit 0
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
92
257
 
93
- decide deny "Документ едет в том же коммите, что и правка, которую он описывает. Не хватает пар:
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
94
281
 
95
- ${missing}
96
- Если документа здесь правда не нужно — строка «Docs-skip: <причина>» в теле коммита; пустая причина не принимается."
282
+ exit 0
@@ -1,23 +1,36 @@
1
1
  #!/usr/bin/env bash
2
- # Гард поставки. PreToolUse на заведении ветки и открытии PR.
2
+ # rt-hook: PreToolUse Bash|mcp__webstorm__execute_terminal_command|mcp__webstorm__execute_tool
3
+ # Гард поставки. PreToolUse на заведении ветки и открытии заявки на слияние.
3
4
  #
4
5
  # Закон о поставке требует трёх вещей, которых обычно не проверяет ничто: правка начинается с
5
6
  # задачи, видимой в очереди работ; задача, ветка и отчёт несут один номер; у задачи есть
6
7
  # исполнитель. Держатся они памятью — и не удерживаются: задачи стоят вне очереди, исполнитель
7
- # не проставлен, а большинство влитых PR приходит с веток, за которыми задачи не стояло вовсе.
8
+ # не проставлен, а большинство влитых заявок приходит с веток, за которыми задачи не стояло
9
+ # вовсе.
8
10
  #
9
11
  # Гард стоит в двух точках, и в каждой требует того, что в этот момент исправимо:
10
12
  #
11
13
  # заведение ветки — имя с номером разбирается на месте; имя без номера пропускается:
12
- # локальная ветка под пробу законна, в главную она не поедет, потому что PR с неё не
13
- # откроется;
14
- # открытие PR — ветка обязана нести номер, а заголовок начинаться с того же номера.
14
+ # локальная ветка под пробу законна, в главную она не поедет, потому что заявка с неё
15
+ # не откроется;
16
+ # открытие заявки — ветка обязана нести номер, заголовок обязан начинаться с того же номера,
17
+ # а задача — быть открытой, стоять в очереди работ и иметь исполнителя.
15
18
  #
16
- # Форму имени ветки и номер в заголовке знает профиль проекта: {{projectProfile}}, функция
17
- # `rt_task_branch_ok <имя>` и переменная RT_TASK_TITLE_RE. Нет профиля гард пропускает: форму
18
- # имени пакет выдумать не может.
19
+ # Два яруса. Формат номер в имени ветки, номер в заголовке, их совпадение — читается из
20
+ # текста команды и работает всегда. Состояние задачи требует сети: нет её, нет помощника или
21
+ # нет токена ярус пропускается, потому что проверять нечем.
19
22
  #
20
- # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: не репозиторий, нет разборщика, битый ввод — пропуск.
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
+ # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: не репозиторий, нет разборщика, битый ввод, нет профиля — пропуск.
21
34
 
22
35
  input="$(cat 2>/dev/null)"
23
36
  [ -z "$input" ] && exit 0
@@ -25,6 +38,7 @@ command -v jq >/dev/null 2>&1 || exit 0
25
38
 
26
39
  tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
27
40
  case "$tool" in
41
+ # Терминал среды и универсальный исполнитель кладут команду в то же поле.
28
42
  Bash | mcp__webstorm__execute_terminal_command | mcp__webstorm__execute_tool) ;;
29
43
  *) exit 0 ;;
30
44
  esac
@@ -32,6 +46,8 @@ esac
32
46
  cmd="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null)"
33
47
  [ -z "$cmd" ] && exit 0
34
48
 
49
+ # Универсальный исполнитель передаёт настоящую команду вложенной строкой. Разбирать надо её,
50
+ # а не обёртку.
35
51
  if [ "$tool" = "mcp__webstorm__execute_tool" ] && command -v perl >/dev/null 2>&1; then
36
52
  inner="$(printf '%s' "$cmd" | perl -0ne '
37
53
  if (/--command(?:=|\s+)(?:"((?:[^"\\]|\\.)*)"|\x27([^\x27]*)\x27|(.+))/s) {
@@ -41,18 +57,56 @@ if [ "$tool" = "mcp__webstorm__execute_tool" ] && command -v perl >/dev/null 2>&
41
57
  [ -n "$inner" ] && cmd="$inner"
42
58
  fi
43
59
 
44
- profile="${CLAUDE_PROJECT_DIR:-.}/{{projectProfile}}"
45
- [ -f "$profile" ] || exit 0
46
- # shellcheck disable=SC1090
47
- . "$profile" 2>/dev/null || exit 0
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
48
76
  command -v rt_task_branch_ok >/dev/null 2>&1 || exit 0
49
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
+
50
83
  deny() {
51
84
  jq -n --arg r "$1" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$r}}' 2>/dev/null \
52
85
  || printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Гард поставки."}}\n'
53
86
  exit 0
54
87
  }
55
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
+
56
110
  # --- заведение ветки ---------------------------------------------------------------------
57
111
  branch_arg=''
58
112
  if printf '%s' "$cmd" | grep -qE '(^|[;&|[:space:]])git[[:space:]]+(checkout[[:space:]]+-b|switch[[:space:]]+-c)[[:space:]]'; then
@@ -64,47 +118,50 @@ fi
64
118
  if [ -n "$branch_arg" ]; then
65
119
  # Имя, притворяющееся веткой под задачу, но не совпадающее с формой, — это промах в имени,
66
120
  # а не осознанная беззадачная ветка. Ловится до первого коммита.
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."
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}»"
69
125
  fi
126
+ # Ветка без номера законна и живёт локально: заявка с неё не откроется.
70
127
  exit 0
71
128
  fi
72
129
 
73
- # --- открытие PR -------------------------------------------------------------------------
130
+ # --- открытие заявки на слияние ----------------------------------------------------------
74
131
  case "$cmd" in
75
- *gh\ pr\ create*) ;;
132
+ *gh\ pr\ create* | *glab\ mr\ create* | *az\ repos\ pr\ create*) ;;
76
133
  *) exit 0 ;;
77
134
  esac
78
135
 
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
136
  branch="$(git branch --show-current 2>/dev/null)"
84
137
  [ -z "$branch" ] && exit 0 # открепившийся HEAD — не про этот случай
85
138
 
86
- # Локальная ветка без номера законна, а PR с неё — уже нет: правка, доезжающая до главной
139
+ # Локальная ветка без номера законна, а заявка с неё — уже нет: правка, доезжающая до главной
87
140
  # ветки, начинается с задачи. Это единственное место, где беззадачная ветка упирается.
88
141
  rt_task_branch_ok "$branch" \
89
- || deny "PR с ветки «${branch}», за которой не стоит задачи. Правка начинается с задачи, видимой в очереди работ: заведи её и перенеси работу в ветку с её номером."
142
+ || deny "BLOCKED: заявка с ветки «${branch}», за которой не стоит задачи. Правка начинается с задачи, видимой в очереди работ: заведи её — ${task_new} — и перенеси работу в ветку с её номером."
90
143
 
91
144
  number="$(printf '%s' "$branch" | sed -nE 's/^[A-Za-z]+-([0-9]+).*/\1/p')"
92
145
 
93
146
  title=''
94
147
  if command -v perl >/dev/null 2>&1; then
95
148
  title="$(printf '%s' "$cmd" | perl -0ne '
96
- if (/--title(?:=|\s+)(?:"((?:[^"\\]|\\.)*)"|\x27([^\x27]*)\x27|([^\s]+))/s) {
149
+ if (/(?:--title|-t)(?:=|\s+)(?:"((?:[^"\\]|\\.)*)"|\x27([^\x27]*)\x27|(\S+))/s) {
97
150
  print defined $1 ? $1 : (defined $2 ? $2 : $3);
98
151
  }
99
152
  ' 2>/dev/null)"
100
153
  fi
101
154
 
102
- if [ -n "$title" ] && [ -n "$number" ]; then
155
+ if [ -n "$title" ]; then
156
+ printf '%s' "$title" | grep -qE "$title_re" \
157
+ || deny "BLOCKED: заголовок заявки не начинается с номера задачи. В списке заявок тела не видно, а строка связи живёт именно там — без номера в заголовке отчёт с задачей не сопоставить."
103
158
  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}. Задача, ветка и отчёт несут один и тот же номер."
159
+ if [ -n "$number" ] && [ -n "$title_number" ]; then
160
+ [ "$title_number" = "$number" ] \
161
+ || deny "BLOCKED: в заголовке заявки номер ${title_number}, у ветки — ${number}. Задача, ветка и отчёт несут один и тот же номер."
162
+ fi
108
163
  fi
109
164
 
165
+ check_task "$number" "заявка с ветки «${branch}»"
166
+
110
167
  exit 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
  # Коммит в главную ветку минует ветку, PR и разбор, а поставка построена на них целиком —
@@ -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:.}}'