@rt-tools/agent-kit 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +194 -30
- 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 +329 -0
- package/assets/checks/check-board.github.mjs +181 -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 +1086 -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 +106 -0
- package/assets/defaults/project.sh +204 -0
- package/assets/hooks/browser-device-id.sh +0 -0
- package/assets/hooks/browser-guard-device-id.sh +2 -1
- package/assets/hooks/browser-guard-no-asking.sh +27 -0
- package/assets/hooks/browser-guard-no-listing.sh +2 -1
- package/assets/hooks/browser-guard-no-other-drivers.sh +2 -1
- package/assets/hooks/browser-guard-require-select.sh +2 -1
- package/assets/hooks/commit-msg.sh +1 -1
- package/assets/hooks/constitution-index.sh +5 -4
- package/assets/hooks/dev-server-guard.sh +8 -6
- package/assets/hooks/docs-guard.sh +223 -37
- package/assets/hooks/git-guard-delivery.sh +171 -31
- package/assets/hooks/git-guard-main.sh +1 -0
- package/assets/hooks/git-guard-push-tests.sh +34 -13
- package/assets/hooks/glossary-load.sh +23 -0
- package/assets/hooks/grill-gate.sh +96 -0
- package/assets/hooks/lint-after-edit.sh +155 -30
- package/assets/hooks/qa-dataid-guard.sh +72 -32
- package/assets/hooks/reuse-first-guard.sh +105 -34
- package/assets/hooks/skill-gate-rearm.sh +1 -0
- package/assets/hooks/skill-gate.sh +75 -15
- package/assets/hooks/skill-loaded.sh +1 -0
- package/assets/hooks/sql-guard.sh +606 -56
- package/assets/hooks/task-context-load.sh +100 -0
- package/assets/hooks/task-flow-guard.sh +118 -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 +27 -8
- package/assets/laws/reuse-first.md +26 -21
- package/assets/laws/shared-code.md +13 -1
- package/assets/laws/verifiability.md +30 -1
- package/assets/laws/work-conduct.md +59 -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 +29 -22
- package/assets/patterns/api-layer-pair.md +40 -30
- package/assets/patterns/browser-verification-measure.md +41 -38
- package/assets/patterns/browser-verification-stand.md +106 -42
- package/assets/patterns/component-structure-new.md +33 -32
- package/assets/patterns/dependencies-upgrade.md +65 -0
- package/assets/patterns/doc-style-sweep.md +65 -28
- package/assets/patterns/doc-style-write.md +36 -33
- 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 +337 -0
- package/assets/patterns/git-workflow-commit.gitlab.md +283 -0
- package/assets/patterns/git-workflow-merge.md +42 -25
- package/assets/patterns/git-workflow-migration.md +61 -31
- package/assets/patterns/git-workflow-restart.md +20 -20
- package/assets/patterns/lib-layers-move.md +50 -32
- package/assets/patterns/lib-layers-new.md +41 -29
- package/assets/patterns/ownership-scope-resolve.md +69 -0
- package/assets/patterns/permissions-procedure.md +35 -33
- package/assets/patterns/platform-access-di.md +39 -25
- package/assets/patterns/pricing-quote.md +71 -0
- package/assets/patterns/reuse-first-extend.md +22 -22
- package/assets/patterns/seo-page.md +52 -40
- package/assets/patterns/seo-verify.md +48 -29
- package/assets/patterns/shared-code-new.md +37 -31
- package/assets/patterns/spec-driven-domain.md +60 -37
- package/assets/patterns/spec-driven-rule.md +55 -40
- package/assets/patterns/styling-bem-component.md +43 -32
- package/assets/patterns/styling-bem-layout.md +30 -24
- package/assets/patterns/task-flow-close.md +154 -0
- package/assets/patterns/task-flow-resume.md +94 -0
- package/assets/patterns/task-flow-start.md +129 -0
- package/assets/patterns/testing-e2e.md +53 -51
- package/assets/patterns/testing-unit.md +70 -46
- package/assets/patterns/translations-key.md +32 -19
- package/assets/patterns/ts-procedure.md +24 -25
- package/assets/rules/angular-patterns.md +50 -27
- package/assets/rules/api-layer.md +46 -28
- package/assets/rules/browser-verification.md +67 -48
- package/assets/rules/component-structure.md +43 -27
- package/assets/rules/dependencies.md +66 -0
- package/assets/rules/doc-style.md +95 -39
- 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 +56 -30
- 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 +43 -25
- package/assets/rules/platform-access.md +57 -29
- package/assets/rules/pricing.md +64 -0
- package/assets/rules/reuse-first.md +57 -43
- package/assets/rules/seo.md +51 -30
- package/assets/rules/shared-code.md +51 -26
- package/assets/rules/spec-driven.md +107 -51
- package/assets/rules/styling-bem.md +54 -39
- package/assets/rules/task-flow.md +150 -0
- package/assets/rules/testing.md +78 -47
- package/assets/rules/translations.md +48 -31
- package/assets/rules/typescript-conventions.md +57 -27
- package/assets/skills/agent-kit.md +85 -0
- package/assets/skills/write-a-skill.md +108 -0
- package/assets/templates/gate-map.sh +23 -15
- package/assets/templates/implementation.md +14 -8
- package/assets/templates/pattern.md +1 -1
- package/assets/templates/project.sh +32 -19
- package/assets/templates/rule.md +2 -2
- 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 +8 -3
- package/lib/assets.d.ts.map +1 -1
- package/lib/assets.js +13 -3
- 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 +202 -14
- package/lib/commands.js.map +1 -1
- package/lib/companion.d.ts +5 -1
- package/lib/companion.d.ts.map +1 -1
- package/lib/companion.js +29 -2
- package/lib/companion.js.map +1 -1
- package/lib/config.d.ts +26 -9
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +41 -15
- 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 +27 -0
- package/lib/hooks-map.d.ts.map +1 -0
- package/lib/hooks-map.js +77 -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/sync.d.ts +26 -0
- package/lib/sync.d.ts.map +1 -1
- package/lib/sync.js +59 -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.5.0.tgz +0 -0
- package/assets/laws/admin-lists.md +0 -35
- package/assets/laws/admin-navigation.md +0 -38
- package/assets/patterns/git-workflow-commit.md +0 -175
- package/assets/rules/git-workflow.md +0 -106
- 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
|
-
*/
|
|
42
|
-
|
|
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
|
-
#
|
|
60
|
-
#
|
|
61
|
-
if printf '%s' "$cmd" | grep -
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
$
|
|
224
|
+
$changes
|
|
89
225
|
EOF
|
|
90
226
|
|
|
91
|
-
|
|
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
|
-
|
|
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
|
-
|
|
96
|
-
Если документа здесь правда не нужно — строка «Docs-skip: <причина>» в теле коммита; пустая причина не принимается."
|
|
282
|
+
exit 0
|
|
@@ -1,23 +1,36 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
#
|
|
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
|
-
# не проставлен, а большинство влитых
|
|
8
|
+
# не проставлен, а большинство влитых заявок приходит с веток, за которыми задачи не стояло
|
|
9
|
+
# вовсе.
|
|
8
10
|
#
|
|
9
11
|
# Гард стоит в двух точках, и в каждой требует того, что в этот момент исправимо:
|
|
10
12
|
#
|
|
11
13
|
# заведение ветки — имя с номером разбирается на месте; имя без номера пропускается:
|
|
12
|
-
# локальная ветка под пробу законна, в главную она не поедет, потому что
|
|
13
|
-
# откроется;
|
|
14
|
-
# открытие
|
|
14
|
+
# локальная ветка под пробу законна, в главную она не поедет, потому что заявка с неё
|
|
15
|
+
# не откроется;
|
|
16
|
+
# открытие заявки — ветка обязана нести номер, заголовок обязан начинаться с того же номера,
|
|
17
|
+
# а задача — быть открытой, стоять в очереди работ и иметь исполнителя.
|
|
15
18
|
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
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,79 @@ if [ "$tool" = "mcp__webstorm__execute_tool" ] && command -v perl >/dev/null 2>&
|
|
|
41
57
|
[ -n "$inner" ] && cmd="$inner"
|
|
42
58
|
fi
|
|
43
59
|
|
|
44
|
-
|
|
45
|
-
[ -
|
|
46
|
-
|
|
47
|
-
|
|
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
|
+
tasks_dir="${RT_TASKS_DIR:-}"
|
|
83
|
+
archive_dir="${RT_ARCHIVE_DIR:-}"
|
|
84
|
+
main_branch="${RT_MAIN_BRANCH:-main}"
|
|
85
|
+
|
|
86
|
+
# Обход требования: строка с причиной. Причина видна тому, кто вливает, поэтому обход законен.
|
|
87
|
+
# Без причины это просто молчаливый пропуск, поэтому она обязательна. Порог в три знака — тот
|
|
88
|
+
# же, что у гарда документа: если сделать по-разному, две формы одного обхода разойдутся.
|
|
89
|
+
folder_skip_re='Task-folder-skip:[[:space:]]*[^[:space:]"'"'"']{3,}'
|
|
90
|
+
|
|
50
91
|
deny() {
|
|
51
92
|
jq -n --arg r "$1" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$r}}' 2>/dev/null \
|
|
52
93
|
|| printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Гард поставки."}}\n'
|
|
53
94
|
exit 0
|
|
54
95
|
}
|
|
55
96
|
|
|
97
|
+
# Подсказка вместо отказа: на открытии отчёта папка ещё нужна. Решения подсказка не несёт,
|
|
98
|
+
# команда идёт дальше своим ходом.
|
|
99
|
+
hint() {
|
|
100
|
+
jq -n --arg c "$1" '{hookSpecificOutput:{hookEventName:"PreToolUse",additionalContext:$c}}' 2>/dev/null
|
|
101
|
+
exit 0
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# Есть ли папка задачи в ветке. Смотрим содержимое ветки, а не рабочее дерево: если папку
|
|
105
|
+
# удалили, но не закоммитили, проверка прошла бы, а папка всё равно уехала бы в main. Имя
|
|
106
|
+
# ветки подставляем целиком, вместе с косой: у ветки вида `chore/312-slug` папка лежит во
|
|
107
|
+
# вложенном каталоге.
|
|
108
|
+
folder_in_branch() {
|
|
109
|
+
git ls-tree -d --name-only HEAD -- "$1" 2>/dev/null | head -1
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
check_task() {
|
|
113
|
+
number="$1"
|
|
114
|
+
where="$2"
|
|
115
|
+
command -v rt_task_state >/dev/null 2>&1 || return 0
|
|
116
|
+
state="$(cd "$root" && rt_task_state "$number" 2>/dev/null)" || return 0
|
|
117
|
+
[ -z "$state" ] && return 0
|
|
118
|
+
|
|
119
|
+
printf '%s' "$state" | jq -e '.exists' >/dev/null 2>&1 \
|
|
120
|
+
|| deny "BLOCKED: ${where} ссылается на задачу #${number}, которой нет. Проверь номер или заведи задачу — ${task_new}."
|
|
121
|
+
printf '%s' "$state" | jq -e '.open' >/dev/null 2>&1 \
|
|
122
|
+
|| deny "BLOCKED: задача #${number} закрыта, а у задачи одна ветка. Работа за закрытой задачей заводится новой задачей — ${task_new}."
|
|
123
|
+
printf '%s' "$state" | jq -e '.onBoard' >/dev/null 2>&1 \
|
|
124
|
+
|| deny "BLOCKED: задачи #${number} нет в очереди работ — правка за ней не видна. Очередь к репозиторию не привязана и задачу сама не забирает; добавь её и сверь — ${board_check}."
|
|
125
|
+
printf '%s' "$state" | jq -e '.assigned' >/dev/null 2>&1 \
|
|
126
|
+
|| deny "BLOCKED: у задачи #${number} нет исполнителя — по очереди работ не видно, кто её взял. Поставь исполнителя${task_bot:+: }${task_bot}."
|
|
127
|
+
printf '%s' "$state" | jq -e '.numbered' >/dev/null 2>&1 \
|
|
128
|
+
|| deny "BLOCKED: заголовок задачи #${number} не начинается с её номера — одну работу придётся узнавать по тексту названия. Поправь заголовок и сверь очередь — ${board_check}."
|
|
129
|
+
|
|
130
|
+
return 0
|
|
131
|
+
}
|
|
132
|
+
|
|
56
133
|
# --- заведение ветки ---------------------------------------------------------------------
|
|
57
134
|
branch_arg=''
|
|
58
135
|
if printf '%s' "$cmd" | grep -qE '(^|[;&|[:space:]])git[[:space:]]+(checkout[[:space:]]+-b|switch[[:space:]]+-c)[[:space:]]'; then
|
|
@@ -64,47 +141,110 @@ fi
|
|
|
64
141
|
if [ -n "$branch_arg" ]; then
|
|
65
142
|
# Имя, притворяющееся веткой под задачу, но не совпадающее с формой, — это промах в имени,
|
|
66
143
|
# а не осознанная беззадачная ветка. Ловится до первого коммита.
|
|
67
|
-
if printf '%s' "$branch_arg" | grep -
|
|
68
|
-
|
|
144
|
+
if printf '%s' "$branch_arg" | grep -qiE '^[A-Za-z]+-[0-9]+'; then
|
|
145
|
+
rt_task_branch_ok "$branch_arg" \
|
|
146
|
+
|| deny "BLOCKED: имя ветки «${branch_arg}» не той формы, что принята здесь. Номер у ветки тот же, что у задачи и у заголовка заявки на слияние."
|
|
147
|
+
check_task "$(printf '%s' "$branch_arg" | sed -nE 's/^[A-Za-z]+-([0-9]+).*/\1/p')" "ветка «${branch_arg}»"
|
|
69
148
|
fi
|
|
149
|
+
# Ветка без номера законна и живёт локально: заявка с неё не откроется.
|
|
70
150
|
exit 0
|
|
71
151
|
fi
|
|
72
152
|
|
|
73
|
-
# ---
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
153
|
+
# --- слияние заявки ----------------------------------------------------------------------
|
|
154
|
+
#
|
|
155
|
+
# Папку задачи разбирают тем же PR, что и работу. После слияния этого уже никто не сделает:
|
|
156
|
+
# работа перешла к следующей задаче, а PR закрыт. Раньше слияния требовать нельзя — пока идёт
|
|
157
|
+
# ревью, plan.md нужен на диске, иначе гард хода работы не даст править код.
|
|
158
|
+
# Команду ищем от начала строки или после разделителя, а не где угодно в тексте. Иначе гард
|
|
159
|
+
# отбивает сообщение, где `gh pr merge` просто упомянут в кавычках, — так он и сработал на
|
|
160
|
+
# правке этого же текста. Полностью подстроку в кавычках так не отсечь, но случайное упоминание
|
|
161
|
+
# внутри слова или пути мимо уже не пройдёт.
|
|
162
|
+
if printf '%s' "$cmd" | grep -qE '(^|[;&|(]|&&|\|\|)[[:space:]]*(gh[[:space:]]+pr[[:space:]]+merge|glab[[:space:]]+mr[[:space:]]+merge|az[[:space:]]+repos[[:space:]]+pr[[:space:]]+update)([[:space:]]|$)'; then
|
|
163
|
+
[ -n "$tasks_dir" ] || exit 0 # ведения работы папкой в дереве нет
|
|
164
|
+
|
|
165
|
+
merge_branch="$(git branch --show-current 2>/dev/null)"
|
|
166
|
+
[ -z "$merge_branch" ] && exit 0
|
|
167
|
+
rt_task_branch_ok "$merge_branch" || exit 0 # за беззадачной веткой папки не стоит
|
|
168
|
+
|
|
169
|
+
folder="$tasks_dir/$merge_branch"
|
|
170
|
+
|
|
171
|
+
# Сначала ищем обход в самой команде — это работает и без сети. Если читать только
|
|
172
|
+
# тело PR, то без сети гард отбил бы слияние, причина которого в этом теле и написана.
|
|
173
|
+
printf '%s' "$cmd" | grep -qiE "$folder_skip_re" && exit 0
|
|
174
|
+
|
|
175
|
+
merge_number="$(printf '%s' "$cmd" | sed -nE 's/.*(pr|mr)[[:space:]]+(merge|update)[[:space:]]+([0-9]+).*/\3/p' | head -1)"
|
|
176
|
+
if [ -n "$merge_number" ] && command -v rt_report_body >/dev/null 2>&1; then
|
|
177
|
+
body="$(cd "$root" && rt_report_body "$merge_number" 2>/dev/null)"
|
|
178
|
+
[ -n "$body" ] && printf '%s' "$body" | grep -qiE "$folder_skip_re" && exit 0
|
|
179
|
+
fi
|
|
78
180
|
|
|
79
|
-
|
|
80
|
-
[ -
|
|
81
|
-
|
|
181
|
+
lying="$(folder_in_branch "$folder")"
|
|
182
|
+
[ -n "$lying" ] \
|
|
183
|
+
&& deny "BLOCKED: в ветке осталась папка задачи «${lying}» — она уедет в главную. Разобрать её потом будет некому: работа перейдёт к следующей задаче, а этот PR закроется. Перенеси в «${archive_dir:-архив}» то, что объясняет принятые решения, остальное удали и повтори. Если работа вливается частями, поставь в тело PR строку «Task-folder-skip: <причина>»."
|
|
184
|
+
|
|
185
|
+
# Запись в архиве спрашиваем только у ветки, которая папку удалила. Иначе проверка
|
|
186
|
+
# цеплялась бы к работе, у которой папки и не было. Без общего предка с главной веткой
|
|
187
|
+
# сравнивать не с чем — тогда молчим.
|
|
188
|
+
[ -n "$archive_dir" ] || exit 0
|
|
189
|
+
base="$(git merge-base "$main_branch" HEAD 2>/dev/null)"
|
|
190
|
+
[ -z "$base" ] && exit 0
|
|
191
|
+
|
|
192
|
+
had="$(git ls-tree -d --name-only "$base" -- "$folder" 2>/dev/null | head -1)"
|
|
193
|
+
[ -z "$had" ] && had="$(git log "$base..HEAD" --diff-filter=A --name-only --pretty=format: -- "$folder" 2>/dev/null | head -1)"
|
|
194
|
+
[ -z "$had" ] && exit 0
|
|
195
|
+
|
|
196
|
+
gained="$(git diff --name-only --diff-filter=A "$base" HEAD -- "$archive_dir" 2>/dev/null | head -1)"
|
|
197
|
+
[ -z "$gained" ] \
|
|
198
|
+
&& deny "BLOCKED: папку задачи удалили, но в «${archive_dir}» ветка ничего не добавила. Удалить проще, чем разобрать, — и вместе с папкой пропадает разбор просьбы, единственная запись слов владельца. Перенеси то, что объясняет принятые решения, одним файлом с понятным именем и повтори."
|
|
199
|
+
|
|
200
|
+
exit 0
|
|
201
|
+
fi
|
|
202
|
+
|
|
203
|
+
# --- открытие заявки на слияние ----------------------------------------------------------
|
|
204
|
+
# Команду ищем от начала строки или после разделителя — по той же причине, что и слияние:
|
|
205
|
+
# упоминание в кавычках командой не является.
|
|
206
|
+
printf '%s' "$cmd" \
|
|
207
|
+
| grep -qE '(^|[;&|(]|&&|\|\|)[[:space:]]*(gh[[:space:]]+pr[[:space:]]+create|glab[[:space:]]+mr[[:space:]]+create|az[[:space:]]+repos[[:space:]]+pr[[:space:]]+create)([[:space:]]|$)' \
|
|
208
|
+
|| exit 0
|
|
82
209
|
|
|
83
210
|
branch="$(git branch --show-current 2>/dev/null)"
|
|
84
211
|
[ -z "$branch" ] && exit 0 # открепившийся HEAD — не про этот случай
|
|
85
212
|
|
|
86
|
-
# Локальная ветка без номера законна, а
|
|
213
|
+
# Локальная ветка без номера законна, а заявка с неё — уже нет: правка, доезжающая до главной
|
|
87
214
|
# ветки, начинается с задачи. Это единственное место, где беззадачная ветка упирается.
|
|
88
215
|
rt_task_branch_ok "$branch" \
|
|
89
|
-
|| deny "
|
|
216
|
+
|| deny "BLOCKED: заявка с ветки «${branch}», за которой не стоит задачи. Правка начинается с задачи, видимой в очереди работ: заведи её — ${task_new} — и перенеси работу в ветку с её номером."
|
|
90
217
|
|
|
91
218
|
number="$(printf '%s' "$branch" | sed -nE 's/^[A-Za-z]+-([0-9]+).*/\1/p')"
|
|
92
219
|
|
|
93
220
|
title=''
|
|
94
221
|
if command -v perl >/dev/null 2>&1; then
|
|
95
222
|
title="$(printf '%s' "$cmd" | perl -0ne '
|
|
96
|
-
if (
|
|
223
|
+
if (/(?:--title|-t)(?:=|\s+)(?:"((?:[^"\\]|\\.)*)"|\x27([^\x27]*)\x27|(\S+))/s) {
|
|
97
224
|
print defined $1 ? $1 : (defined $2 ? $2 : $3);
|
|
98
225
|
}
|
|
99
226
|
' 2>/dev/null)"
|
|
100
227
|
fi
|
|
101
228
|
|
|
102
|
-
if [ -n "$title" ]
|
|
229
|
+
if [ -n "$title" ]; then
|
|
230
|
+
printf '%s' "$title" | grep -qE "$title_re" \
|
|
231
|
+
|| deny "BLOCKED: заголовок заявки не начинается с номера задачи. В списке заявок тела не видно, а строка связи живёт именно там — без номера в заголовке отчёт с задачей не сопоставить."
|
|
103
232
|
title_number="$(printf '%s' "$title" | sed -nE 's/^\[[A-Za-z]+-([0-9]+)\].*/\1/p')"
|
|
104
|
-
[ -
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
233
|
+
if [ -n "$number" ] && [ -n "$title_number" ]; then
|
|
234
|
+
[ "$title_number" = "$number" ] \
|
|
235
|
+
|| deny "BLOCKED: в заголовке заявки номер ${title_number}, у ветки — ${number}. Задача, ветка и отчёт несут один и тот же номер."
|
|
236
|
+
fi
|
|
237
|
+
fi
|
|
238
|
+
|
|
239
|
+
check_task "$number" "заявка с ветки «${branch}»"
|
|
240
|
+
|
|
241
|
+
# Сейчас папка ещё нужна: правки по замечаниям ревью идут в эту же ветку, а без plan.md их не
|
|
242
|
+
# пропустит гард хода работы. Поэтому здесь только напоминание. Требование стоит на слиянии —
|
|
243
|
+
# там папка уже не нужна, а вред от неё как раз и наступает.
|
|
244
|
+
if [ -n "$tasks_dir" ]; then
|
|
245
|
+
lying="$(folder_in_branch "$tasks_dir/$branch")"
|
|
246
|
+
[ -n "$lying" ] \
|
|
247
|
+
&& hint "В ветке лежит папка задачи «${lying}». Разбери её до слияния, этим же PR: потом за неё уже никто не возьмётся. На слиянии это будет отказ, а не напоминание."
|
|
108
248
|
fi
|
|
109
249
|
|
|
110
250
|
exit 0
|