@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.
- package/README.md +235 -18
- package/assets/agents/business-analyst.md +74 -0
- package/assets/agents/project-manager.md +70 -0
- package/assets/agents/qa-engineer.md +72 -0
- package/assets/agents/skill-curator.md +110 -0
- package/assets/agents/spec-critic.md +44 -0
- package/assets/agents/spec-writer.md +50 -0
- package/assets/checks/board.github.mjs +286 -0
- package/assets/checks/check-board.github.mjs +188 -0
- package/assets/checks/check-doc-paths.mjs +163 -0
- package/assets/checks/check-dupes.mjs +277 -0
- package/assets/checks/check-lib-layers.mjs +573 -0
- package/assets/checks/check-reuse.mjs +208 -0
- package/assets/checks/check-schema-drift.mjs +186 -0
- package/assets/checks/check-specs.mjs +1007 -0
- package/assets/checks/check-styles.mjs +109 -0
- package/assets/checks/rt-kit-checks.config.mjs +134 -0
- package/assets/checks/task-new.github.mjs +198 -0
- package/assets/commands/skill-curator.md +70 -0
- package/assets/defaults/gate-map.sh +100 -0
- package/assets/defaults/project.sh +179 -0
- package/assets/hooks/browser-device-id.sh +20 -0
- package/assets/hooks/browser-guard-device-id.sh +28 -0
- package/assets/hooks/browser-guard-no-asking.sh +27 -0
- package/assets/hooks/browser-guard-no-listing.sh +18 -0
- package/assets/hooks/browser-guard-no-other-drivers.sh +79 -0
- package/assets/hooks/browser-guard-require-select.sh +54 -0
- package/assets/hooks/commit-msg.sh +26 -0
- package/assets/hooks/constitution-index.sh +43 -0
- package/assets/hooks/dev-server-guard.sh +115 -0
- package/assets/hooks/docs-guard.sh +282 -0
- package/assets/hooks/git-guard-delivery.sh +167 -0
- package/assets/hooks/git-guard-main.sh +73 -0
- package/assets/hooks/git-guard-push-tests.sh +94 -0
- package/assets/hooks/glossary-load.sh +23 -0
- package/assets/hooks/lint-after-edit.sh +219 -0
- package/assets/hooks/qa-dataid-guard.sh +121 -0
- package/assets/hooks/reuse-first-guard.sh +154 -0
- package/assets/hooks/skill-gate-rearm.sh +23 -0
- package/assets/hooks/skill-gate.sh +128 -0
- package/assets/hooks/skill-loaded.sh +21 -0
- package/assets/hooks/sql-guard.sh +679 -0
- package/assets/hooks/task-context-load.sh +100 -0
- package/assets/hooks/task-flow-guard.sh +107 -0
- package/assets/laws/{access.md → application/access.md} +1 -4
- package/assets/laws/{locales.md → application/locales.md} +1 -3
- package/assets/laws/application/money.md +41 -0
- package/assets/laws/application/ownership.md +32 -0
- package/assets/laws/{search-visibility.md → application/search-visibility.md} +1 -1
- package/assets/laws/code-structure.md +7 -6
- package/assets/laws/delivery.md +53 -3
- package/assets/laws/entity-editing.md +49 -55
- package/assets/laws/entity-models.md +4 -14
- package/assets/laws/frontend-application.md +5 -5
- package/assets/laws/lib-imports.md +14 -1
- package/assets/laws/lists.md +33 -0
- package/assets/laws/navigation.md +40 -0
- package/assets/laws/project-documentation.md +17 -8
- package/assets/laws/reuse-first.md +26 -21
- package/assets/laws/shared-code.md +13 -1
- package/assets/laws/verifiability.md +17 -1
- package/assets/laws/work-conduct.md +48 -0
- package/assets/patterns/admin-lists-screen.md +131 -0
- package/assets/patterns/admin-nav-item.md +71 -0
- package/assets/patterns/angular-patterns-state.md +101 -0
- package/assets/patterns/api-layer-pair.md +88 -0
- package/assets/patterns/browser-verification-measure.md +86 -0
- package/assets/patterns/browser-verification-stand.md +143 -0
- package/assets/patterns/component-structure-new.md +99 -0
- package/assets/patterns/dependencies-upgrade.md +65 -0
- package/assets/patterns/doc-style-sweep.md +137 -0
- package/assets/patterns/doc-style-write.md +109 -0
- package/assets/patterns/entity-aside.md +136 -0
- package/assets/patterns/entity-models-new.md +124 -0
- package/assets/patterns/entity-store.md +91 -0
- package/assets/patterns/git-workflow-commit.azure.md +259 -0
- package/assets/patterns/git-workflow-commit.github.md +333 -0
- package/assets/patterns/git-workflow-commit.gitlab.md +283 -0
- package/assets/patterns/git-workflow-merge.md +99 -0
- package/assets/patterns/git-workflow-migration.md +88 -0
- package/assets/patterns/git-workflow-restart.md +49 -0
- package/assets/patterns/lib-layers-move.md +95 -0
- package/assets/patterns/lib-layers-new.md +82 -0
- package/assets/patterns/ownership-scope-resolve.md +69 -0
- package/assets/patterns/permissions-procedure.md +71 -0
- package/assets/patterns/platform-access-di.md +84 -0
- package/assets/patterns/pricing-quote.md +71 -0
- package/assets/patterns/reuse-first-extend.md +73 -0
- package/assets/patterns/seo-page.md +104 -0
- package/assets/patterns/seo-verify.md +83 -0
- package/assets/patterns/shared-code-new.md +86 -0
- package/assets/patterns/spec-driven-domain.md +107 -0
- package/assets/patterns/spec-driven-rule.md +127 -0
- package/assets/patterns/styling-bem-component.md +88 -0
- package/assets/patterns/styling-bem-layout.md +73 -0
- package/assets/patterns/task-flow-close.md +90 -0
- package/assets/patterns/task-flow-resume.md +94 -0
- package/assets/patterns/task-flow-start.md +117 -0
- package/assets/patterns/testing-e2e.md +92 -0
- package/assets/patterns/testing-unit.md +117 -0
- package/assets/patterns/translations-key.md +64 -0
- package/assets/patterns/ts-procedure.md +65 -0
- package/assets/rules/angular-patterns.md +71 -0
- package/assets/rules/api-layer.md +71 -0
- package/assets/rules/browser-verification.md +87 -0
- package/assets/rules/component-structure.md +64 -0
- package/assets/rules/dependencies.md +66 -0
- package/assets/rules/doc-style.md +103 -0
- package/assets/rules/entity-conventions.md +78 -0
- package/assets/rules/entity-models.md +70 -0
- package/assets/rules/git-workflow.azure.md +116 -0
- package/assets/rules/git-workflow.github.md +123 -0
- package/assets/rules/git-workflow.gitlab.md +113 -0
- package/assets/rules/lib-layers.md +80 -0
- package/assets/rules/lists.md +73 -0
- package/assets/rules/navigation.md +78 -0
- package/assets/rules/ownership-scope.md +63 -0
- package/assets/rules/permissions.md +70 -0
- package/assets/rules/platform-access.md +77 -0
- package/assets/rules/pricing.md +64 -0
- package/assets/rules/reuse-first.md +83 -0
- package/assets/rules/seo.md +71 -0
- package/assets/rules/shared-code.md +70 -0
- package/assets/rules/spec-driven.md +135 -0
- package/assets/rules/styling-bem.md +74 -0
- package/assets/rules/task-flow.md +110 -0
- package/assets/rules/testing.md +100 -0
- package/assets/rules/translations.md +69 -0
- package/assets/rules/typescript-conventions.md +76 -0
- package/assets/skills/agent-kit.md +81 -0
- package/assets/skills/write-a-skill.md +108 -0
- package/assets/templates/gate-map.sh +45 -0
- package/assets/templates/implementation.md +44 -0
- package/assets/templates/pattern.md +5 -1
- package/assets/templates/project.sh +54 -0
- package/assets/templates/rule.md +12 -23
- package/assets/variants.json +20 -0
- package/assets/workflows/feature.js +134 -0
- package/assets/workflows/plan.js +150 -0
- package/bin/agent-kit.d.ts.map +1 -1
- package/bin/agent-kit.js +78 -5
- package/bin/agent-kit.js.map +1 -1
- package/bin/prompt.d.ts +5 -0
- package/bin/prompt.d.ts.map +1 -1
- package/bin/prompt.js +19 -7
- package/bin/prompt.js.map +1 -1
- package/index.d.ts +1 -0
- package/index.d.ts.map +1 -1
- package/index.js +1 -0
- package/index.js.map +1 -1
- package/lib/assets.d.ts +14 -1
- package/lib/assets.d.ts.map +1 -1
- package/lib/assets.js +23 -2
- package/lib/assets.js.map +1 -1
- package/lib/catalog.d.ts +52 -5
- package/lib/catalog.d.ts.map +1 -1
- package/lib/catalog.js +104 -16
- package/lib/catalog.js.map +1 -1
- package/lib/commands.d.ts +22 -1
- package/lib/commands.d.ts.map +1 -1
- package/lib/commands.js +218 -11
- package/lib/commands.js.map +1 -1
- package/lib/companion.d.ts +57 -0
- package/lib/companion.d.ts.map +1 -0
- package/lib/companion.js +60 -0
- package/lib/companion.js.map +1 -0
- package/lib/config.d.ts +42 -2
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +60 -2
- package/lib/config.js.map +1 -1
- package/lib/freshness.d.ts +14 -0
- package/lib/freshness.d.ts.map +1 -0
- package/lib/freshness.js +116 -0
- package/lib/freshness.js.map +1 -0
- package/lib/hooks-map.d.ts +24 -0
- package/lib/hooks-map.d.ts.map +1 -0
- package/lib/hooks-map.js +72 -0
- package/lib/hooks-map.js.map +1 -0
- package/lib/integrity.d.ts +36 -0
- package/lib/integrity.d.ts.map +1 -0
- package/lib/integrity.js +44 -0
- package/lib/integrity.js.map +1 -0
- package/lib/picker.d.ts +11 -1
- package/lib/picker.d.ts.map +1 -1
- package/lib/picker.js +44 -6
- package/lib/picker.js.map +1 -1
- package/lib/stamp.d.ts +2 -5
- package/lib/stamp.d.ts.map +1 -1
- package/lib/stamp.js +25 -10
- package/lib/stamp.js.map +1 -1
- package/lib/sync.d.ts +29 -0
- package/lib/sync.d.ts.map +1 -1
- package/lib/sync.js +78 -4
- package/lib/sync.js.map +1 -1
- package/lib/variants.d.ts +44 -0
- package/lib/variants.d.ts.map +1 -0
- package/lib/variants.js +82 -0
- package/lib/variants.js.map +1 -0
- package/package.json +1 -1
- package/rt-tools-agent-kit-0.4.0.tgz +0 -0
- package/assets/laws/admin-lists.md +0 -35
- package/assets/laws/admin-navigation.md +0 -38
- 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
|