@rt-tools/agent-kit 0.16.1 → 0.18.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/assets/checks/board-epics.github.mjs +123 -0
- package/assets/checks/board-gh.github.mjs +121 -0
- package/assets/checks/board-paths.github.mjs +88 -0
- package/assets/checks/board-runs.github.mjs +8 -0
- package/assets/checks/board-titles.github.mjs +66 -0
- package/assets/checks/board.github.mjs +77 -73
- package/assets/checks/check-board.github.mjs +56 -7
- package/assets/checks/check-file-size.mjs +10 -2
- package/assets/checks/check-glossary.mjs +170 -0
- package/assets/checks/check-hook-scope.mjs +126 -0
- package/assets/checks/check-profile-drift.mjs +195 -0
- package/assets/checks/check-push-gate.mjs +59 -1
- package/assets/checks/check-schema-drift.mjs +12 -5
- package/assets/checks/check-specs.mjs +1 -1
- package/assets/checks/rt-kit-checks.config.mjs +13 -0
- package/assets/checks/spec-anchors.mjs +2 -2
- package/assets/checks/spec-contract.mjs +9 -0
- package/assets/defaults/gate-map.sh +13 -4
- package/assets/defaults/project.sh +31 -108
- package/assets/defaults/shell.sh +139 -0
- package/assets/docs/GLOSSARY.md +1 -1
- package/assets/hooks/browser-device-id.sh +20 -4
- package/assets/hooks/browser-guard-device-id.sh +5 -2
- package/assets/hooks/browser-guard-no-other-drivers.sh +41 -2
- package/assets/hooks/claim-guard.sh +22 -1
- package/assets/hooks/docs-guard.sh +1 -1
- package/assets/hooks/exam-guard.sh +85 -12
- package/assets/hooks/git-guard-delivery-conflict.sh +85 -0
- package/assets/hooks/git-guard-delivery-folder.sh +19 -0
- package/assets/hooks/git-guard-delivery-signature.sh +14 -5
- package/assets/hooks/git-guard-delivery.sh +54 -2
- package/assets/hooks/git-guard-push-tests.sh +21 -1
- package/assets/hooks/grill-gate-ask.sh +25 -0
- package/assets/hooks/grill-gate.sh +75 -11
- package/assets/hooks/lint-after-edit.sh +44 -16
- package/assets/hooks/proposal-guard.sh +11 -4
- package/assets/hooks/rule-source-guard.sh +8 -13
- package/assets/hooks/skill-gate-layers.sh +7 -0
- package/assets/hooks/skill-gate.sh +29 -0
- package/assets/hooks/task-context-load.sh +32 -0
- package/assets/hooks/task-flow-context.sh +194 -0
- package/assets/hooks/task-flow-draft-guard.sh +109 -0
- package/assets/hooks/task-flow-guard.sh +27 -159
- package/assets/hooks/turn-exit-guard.sh +53 -88
- package/assets/hooks/waiting-turn-guard.sh +65 -2
- package/assets/hooks/window-fill-guard.sh +5 -1
- package/assets/hooks/work-start-guard.sh +166 -0
- package/assets/hooks/write-targets.sh +24 -0
- package/assets/laws/delivery.md +19 -0
- package/assets/laws/frontend-application.md +4 -0
- package/assets/laws/verifiability.md +44 -0
- package/assets/laws/work-conduct.md +15 -0
- package/assets/patterns/browser-verification-measure.md +3 -1
- package/assets/patterns/browser-verification-stand.md +17 -4
- package/assets/patterns/doc-style-write.md +62 -2
- package/assets/patterns/git-workflow-commit.github.md +33 -5
- package/assets/patterns/git-workflow-docker.md +22 -0
- package/assets/patterns/git-workflow-freshness.md +87 -0
- package/assets/patterns/git-workflow-merge.md +41 -0
- package/assets/patterns/git-workflow-migration.md +11 -0
- package/assets/patterns/git-workflow-pr.github.md +12 -1
- package/assets/patterns/git-workflow-restart.md +18 -0
- package/assets/patterns/git-workflow-secrets.md +14 -0
- package/assets/patterns/git-workflow-stack.md +93 -1
- package/assets/patterns/lib-layers-move.md +4 -0
- package/assets/patterns/spec-driven-domain.md +47 -2
- package/assets/patterns/spec-driven-rule.md +16 -4
- package/assets/patterns/spec-driven-sweep.md +57 -0
- package/assets/patterns/status-report-table.github.md +1 -1
- package/assets/patterns/task-flow-archive.md +58 -16
- package/assets/patterns/task-flow-close.md +106 -23
- package/assets/patterns/task-flow-handoff.md +14 -2
- package/assets/patterns/task-flow-resume.md +35 -8
- package/assets/patterns/task-flow-start.md +60 -22
- package/assets/patterns/testing-e2e.md +23 -0
- package/assets/patterns/turn-entry-map.md +1 -1
- package/assets/pitfalls/agent-kit.md +71 -3
- package/assets/pitfalls/doc-style.md +15 -0
- package/assets/pitfalls/git-workflow.github.md +88 -0
- package/assets/pitfalls/spec-driven.md +22 -0
- package/assets/pitfalls/task-flow.md +113 -0
- package/assets/pitfalls/testing.md +6 -0
- package/assets/pitfalls/turn-conduct.md +49 -0
- package/assets/rules/browser-verification.md +58 -0
- package/assets/rules/deploy-flow.azure.md +8 -0
- package/assets/rules/deploy-flow.github.md +27 -0
- package/assets/rules/deploy-flow.gitlab.md +8 -0
- package/assets/rules/doc-style.md +20 -0
- package/assets/rules/git-workflow.azure.md +22 -2
- package/assets/rules/git-workflow.github.md +127 -118
- package/assets/rules/git-workflow.gitlab.md +17 -4
- package/assets/rules/lists.md +5 -0
- package/assets/rules/observability.needs-app.md +4 -0
- package/assets/rules/reuse-first.md +14 -0
- package/assets/rules/shared-code.md +5 -0
- package/assets/rules/spec-driven.md +33 -15
- package/assets/rules/task-flow.md +132 -126
- package/assets/rules/testing.md +41 -2
- package/assets/rules/turn-conduct.md +73 -57
- package/assets/rules/turn-entry.md +6 -0
- package/assets/samples/tasks/_template/grill.md +5 -0
- package/assets/samples/tasks/_template/plan.md +3 -0
- package/assets/skills/agent-kit.md +108 -76
- package/assets/templates/postmortem.md +5 -1
- package/bin/agent-kit.d.ts.map +1 -1
- package/bin/agent-kit.js +30 -6
- package/bin/agent-kit.js.map +1 -1
- package/lib/catalog.d.ts.map +1 -1
- package/lib/catalog.js +2 -1
- package/lib/catalog.js.map +1 -1
- package/lib/commands.d.ts.map +1 -1
- package/lib/commands.js +96 -7
- package/lib/commands.js.map +1 -1
- package/lib/hooks-map.d.ts +26 -0
- package/lib/hooks-map.d.ts.map +1 -1
- package/lib/hooks-map.js +58 -2
- package/lib/hooks-map.js.map +1 -1
- package/lib/sections.d.ts +6 -0
- package/lib/sections.d.ts.map +1 -1
- package/lib/sections.js +19 -0
- package/lib/sections.js.map +1 -1
- package/lib/shipment.d.ts +2 -0
- package/lib/shipment.d.ts.map +1 -1
- package/lib/shipment.fixture.d.ts +5 -0
- package/lib/shipment.fixture.d.ts.map +1 -1
- package/lib/shipment.fixture.js +7 -0
- package/lib/shipment.fixture.js.map +1 -1
- package/lib/shipment.js +13 -1
- package/lib/shipment.js.map +1 -1
- package/lib/sync.d.ts +35 -3
- package/lib/sync.d.ts.map +1 -1
- package/lib/sync.js +59 -8
- package/lib/sync.js.map +1 -1
- package/package.json +1 -1
- package/rt-tools-agent-kit-0.18.0.tgz +0 -0
- package/rt-tools-agent-kit-0.16.1.tgz +0 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Общий разбор для гардов хода работы. НЕ гард: объявления `rt-hook:` у него нет, к событиям
|
|
3
|
+
# агента он не подключается. Его источают сами гарды — тем же приёмом, каким они источают общий
|
|
4
|
+
# хвост отказа.
|
|
5
|
+
#
|
|
6
|
+
# Зачем он есть. Требования к ходу работы стоят двумя гардами — папка задачи с замыслом и
|
|
7
|
+
# состоянием отдельно, договорённость о продукте отдельно, — а разбор у обоих один и тот же:
|
|
8
|
+
# какой путь пишет вызов, код ли это приложения, в какой ветке идёт правка и где лежит папка
|
|
9
|
+
# задачи. Разложенный вторым разом, этот разбор расходится молча: правка одного гарда чинит
|
|
10
|
+
# половину случаев, и видно это только там, где второй промолчал.
|
|
11
|
+
#
|
|
12
|
+
# ЧТО ОН ДЕЛАЕТ. Читает ввод, поднимает профиль дерева, вынимает из вызова пути, отбирает среди
|
|
13
|
+
# них первый путь кода приложения, переходит в рабочий каталог правки и называет ветку, корень,
|
|
14
|
+
# каталог папок задач, саму папку и замысел в ней.
|
|
15
|
+
#
|
|
16
|
+
# ЧЕГО ОН НЕ ДЕЛАЕТ. Он ничего не судит и ничего не отбивает: имя ветки, наличие папки, состояние
|
|
17
|
+
# работы и договорённость — дело самих гардов, и отказ печатает тот, чьё это требование.
|
|
18
|
+
#
|
|
19
|
+
# FAIL-OPEN: нет jq, не git-репозиторий, битый ввод, чужой инструмент, нет функции профиля →
|
|
20
|
+
# ответ «судить нечего». Сломанный разбор не должен мешать работать.
|
|
21
|
+
|
|
22
|
+
# Разбор вызова. Возвращает 0 и ставит переменные, если правка касается кода приложения в ветке
|
|
23
|
+
# с историей; иначе — ненулевой код, и гард выходит молча.
|
|
24
|
+
#
|
|
25
|
+
# RT_TF_PATH — путь кода приложения, из-за которого гард вообще судит
|
|
26
|
+
# RT_TF_BRANCH — текущая ветка рабочего каталога правки
|
|
27
|
+
# RT_TF_ROOT — корень рабочего дерева
|
|
28
|
+
# RT_TF_TASKS_DIR — каталог папок задач, как он назван в дереве
|
|
29
|
+
# RT_TF_MAIN_BRANCH — главная ветка дерева
|
|
30
|
+
# RT_TF_DIR — папка этой задачи
|
|
31
|
+
# RT_TF_PLAN — замысел в ней
|
|
32
|
+
# Разложенный слой правил судится наравне с кодом приложения. Путями кода он не покрыт нигде —
|
|
33
|
+
# лежит в каталоге законов, в каталоге агента и среди проверок, — и полторы сотни его файлов
|
|
34
|
+
# легли без единого отклика гарда; отбил он двумя ходами позже, на записи в папку задачи. Признак
|
|
35
|
+
# тот же, по которому слой находит гейт правил: шапка раскладки в начале файла. Ресурс пакета, из
|
|
36
|
+
# которого раскладка идёт, шапки не несёт и судится по-прежнему своим путём.
|
|
37
|
+
rt_tf_laid_out() {
|
|
38
|
+
[ -f "$1" ] || return 1
|
|
39
|
+
head -n 3 "$1" 2>/dev/null | grep -q 'rt-kit v[^[:space:]]* ·'
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
rt_task_flow_context() {
|
|
43
|
+
rt_tf_hooks_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
44
|
+
|
|
45
|
+
# shellcheck disable=SC1090
|
|
46
|
+
. "$rt_tf_hooks_dir/utf8.sh" 2>/dev/null || true
|
|
47
|
+
# shellcheck disable=SC1090
|
|
48
|
+
. "$rt_tf_hooks_dir/hook-input.sh" 2>/dev/null || true
|
|
49
|
+
|
|
50
|
+
command -v rt_hook_read >/dev/null 2>&1 || return 1
|
|
51
|
+
rt_hook_read
|
|
52
|
+
[ -z "$RT_HOOK_INPUT" ] && return 1
|
|
53
|
+
command -v jq >/dev/null 2>&1 || return 1
|
|
54
|
+
|
|
55
|
+
# Профиль дерева: сперва умолчание пакета, поверх него — надстройка проекта, если она есть.
|
|
56
|
+
# Читается до разбора пути: пути из команды оболочки вынимает как раз профиль.
|
|
57
|
+
for rt_tf_profile in \
|
|
58
|
+
"$rt_tf_hooks_dir/../rt-kit/defaults/project.sh" \
|
|
59
|
+
"$rt_tf_hooks_dir/../defaults/project.sh" \
|
|
60
|
+
"${CLAUDE_PROJECT_DIR:-.}/.claude/rt-kit/defaults/project.sh" \
|
|
61
|
+
"${CLAUDE_PROJECT_DIR:-.}/.claude/rt-kit/project.sh"; do
|
|
62
|
+
# shellcheck disable=SC1090
|
|
63
|
+
[ -f "$rt_tf_profile" ] && . "$rt_tf_profile" 2>/dev/null
|
|
64
|
+
done
|
|
65
|
+
|
|
66
|
+
# Слово о нехватке функции профиля: хук, вышедший молча, неотличим от работающего. Файл может
|
|
67
|
+
# быть не разложен — тогда остаётся прежнее поведение, молчаливое.
|
|
68
|
+
# shellcheck disable=SC1090
|
|
69
|
+
[ -f "$rt_tf_hooks_dir/profile-check.sh" ] && . "$rt_tf_hooks_dir/profile-check.sh"
|
|
70
|
+
command -v rt_needs >/dev/null 2>&1 || rt_needs() { command -v "$1" >/dev/null 2>&1; }
|
|
71
|
+
|
|
72
|
+
rt_tf_tool="$(rt_hook_tool)"
|
|
73
|
+
rt_tf_candidates=""
|
|
74
|
+
case "$rt_tf_tool" in
|
|
75
|
+
# Инструмент редактора заводит файл теми же двумя данными, только называет их иначе —
|
|
76
|
+
# без этой ветки правка шла бы мимо гарда сменой инструмента.
|
|
77
|
+
Edit | Write | MultiEdit | mcp__webstorm__create_new_file)
|
|
78
|
+
rt_tf_candidates="$(printf '%s' "$RT_HOOK_INPUT" | jq -r '.tool_input.file_path // .tool_input.pathInProject // empty' 2>/dev/null)"
|
|
79
|
+
;;
|
|
80
|
+
# Второй ярус: та же правка, положенная командой оболочки. Без него отказ гарда обходится
|
|
81
|
+
# сменой не инструмента, а способа записи — перенаправлением, `sed -i`, интерпретатором с
|
|
82
|
+
# heredoc.
|
|
83
|
+
#
|
|
84
|
+
# Терминал среды исполняет ту же командную строку и кладёт её в то же поле: без этих двух
|
|
85
|
+
# имён гард стоял бы объявленным на них и молча пропускал — состояние хуже необъявленного,
|
|
86
|
+
# потому что снаружи выглядит закрытым.
|
|
87
|
+
Bash | mcp__webstorm__execute_terminal_command | mcp__webstorm__execute_tool)
|
|
88
|
+
rt_tf_cmd="$(rt_hook_cmd)"
|
|
89
|
+
[ -z "$rt_tf_cmd" ] && return 1
|
|
90
|
+
# Универсальный исполнитель прячет настоящую команду во вложенной строке: без её
|
|
91
|
+
# разбора путь стоит за кавычкой, и до него не дотягивается ни один образец.
|
|
92
|
+
if [ "$rt_tf_tool" = "mcp__webstorm__execute_tool" ] && command -v perl >/dev/null 2>&1; then
|
|
93
|
+
rt_tf_inner="$(printf '%s' "$rt_tf_cmd" | perl -0ne '
|
|
94
|
+
if (/--command(?:=|\s+)(?:"((?:[^"\\]|\\.)*)"|\x27([^\x27]*)\x27|(.+))/s) {
|
|
95
|
+
print defined $1 ? $1 : (defined $2 ? $2 : $3);
|
|
96
|
+
}
|
|
97
|
+
' 2>/dev/null)"
|
|
98
|
+
[ -n "$rt_tf_inner" ] && rt_tf_cmd="$rt_tf_inner"
|
|
99
|
+
fi
|
|
100
|
+
# Команда заведения задачи не судится вовсе. Она пишет папку задачи и карточку в
|
|
101
|
+
# очереди работ, а текст её несёт тело задачи целиком: цитата со знаком «больше»
|
|
102
|
+
# подходит под признак записи, путь к коду в прозе тела — под признак пути. Отбитая,
|
|
103
|
+
# она отбивается тем самым гардом, который печатает её в тексте своего отказа, и
|
|
104
|
+
# завести задачу становится нечем — ни с ветки закрытой задачи, ни с главной. Имя
|
|
105
|
+
# команды берётся у профиля дерева: своя копия разошлась бы с ним молча.
|
|
106
|
+
case "$rt_tf_cmd" in
|
|
107
|
+
*"${RT_TASK_NEW_CMD:-npm run task:new}"*) return 1 ;;
|
|
108
|
+
esac
|
|
109
|
+
rt_needs rt_shell_writes task-flow-guard || return 1
|
|
110
|
+
rt_needs rt_shell_paths task-flow-guard || return 1
|
|
111
|
+
rt_shell_writes "$rt_tf_cmd" || return 1
|
|
112
|
+
rt_tf_candidates="$(rt_shell_paths "$rt_tf_cmd")"
|
|
113
|
+
;;
|
|
114
|
+
*) return 1 ;;
|
|
115
|
+
esac
|
|
116
|
+
[ -z "$rt_tf_candidates" ] && return 1
|
|
117
|
+
|
|
118
|
+
# Признак «правка меняет поведение» — путь, а не оценка на глаз: оценку назначает тот, кому
|
|
119
|
+
# она мешает, и порог плывёт. Где живёт код приложения, знает профиль: правила, тексты,
|
|
120
|
+
# обвязка и зависимости под требование не попадают — иначе разбор задачи нельзя было бы
|
|
121
|
+
# вести до заведения ветки.
|
|
122
|
+
rt_needs rt_is_app_code task-flow-guard || return 1
|
|
123
|
+
|
|
124
|
+
# Удаление отличается от записи одним: снимаемого может не быть в истории вовсе. Свой
|
|
125
|
+
# временный каталог под корнем приложений правкой продукта не бывает — снимать его,
|
|
126
|
+
# восстанавливая ради этого замысел на диске, значит исполнять требование, написанное про
|
|
127
|
+
# другое действие. Отслеживаемый путь судится по-прежнему: снятый файл кода меняет поведение
|
|
128
|
+
# так же, как переписанный.
|
|
129
|
+
rt_tf_removes=0
|
|
130
|
+
case "${rt_tf_cmd:-}" in
|
|
131
|
+
*"rm "*) rt_tf_removes=1 ;;
|
|
132
|
+
esac
|
|
133
|
+
# Каталог правки: ветка и история смотрятся ниже, а спросить историю нужно уже здесь.
|
|
134
|
+
rt_tf_askdir="$(rt_hook_cwd)"
|
|
135
|
+
[ -z "$rt_tf_askdir" ] && rt_tf_askdir="${CLAUDE_PROJECT_DIR:-.}"
|
|
136
|
+
|
|
137
|
+
# Судится каждый названный путь: команда пишет столько файлов, сколько в ней стоит, и одного
|
|
138
|
+
# под требованием довольно, чтобы отбить её целиком.
|
|
139
|
+
RT_TF_PATH=""
|
|
140
|
+
while IFS= read -r rt_tf_candidate; do
|
|
141
|
+
[ -z "$rt_tf_candidate" ] && continue
|
|
142
|
+
# Снятие того, чего в истории нет, — не правка продукта, а уборка за собой. Спрашивается
|
|
143
|
+
# путь, как он назван в команде: приклеенный корень уводит вопрос в чужое дерево.
|
|
144
|
+
if [ "$rt_tf_removes" = 1 ] && git -C "$rt_tf_askdir" rev-parse --is-inside-work-tree >/dev/null 2>&1 &&
|
|
145
|
+
! git -C "$rt_tf_askdir" ls-files --error-unmatch -- "$rt_tf_candidate" >/dev/null 2>&1; then
|
|
146
|
+
continue
|
|
147
|
+
fi
|
|
148
|
+
case "$rt_tf_candidate" in
|
|
149
|
+
/*) ;;
|
|
150
|
+
*) rt_tf_candidate="${CLAUDE_PROJECT_DIR:-.}/$rt_tf_candidate" ;;
|
|
151
|
+
esac
|
|
152
|
+
if rt_is_app_code "$rt_tf_candidate" || rt_tf_laid_out "$rt_tf_candidate"; then
|
|
153
|
+
RT_TF_PATH="$rt_tf_candidate"
|
|
154
|
+
break
|
|
155
|
+
fi
|
|
156
|
+
done <<EOF
|
|
157
|
+
$rt_tf_candidates
|
|
158
|
+
EOF
|
|
159
|
+
[ -z "$RT_TF_PATH" ] && return 1
|
|
160
|
+
|
|
161
|
+
# Ветку смотрим там, где пойдёт правка: у worktree она своя.
|
|
162
|
+
rt_tf_workdir="$(rt_hook_cwd)"
|
|
163
|
+
[ -z "$rt_tf_workdir" ] && rt_tf_workdir="${CLAUDE_PROJECT_DIR:-.}"
|
|
164
|
+
cd "$rt_tf_workdir" 2>/dev/null || return 1
|
|
165
|
+
git rev-parse --is-inside-work-tree >/dev/null 2>&1 || return 1
|
|
166
|
+
|
|
167
|
+
RT_TF_BRANCH="$(git branch --show-current 2>/dev/null)"
|
|
168
|
+
[ -z "$RT_TF_BRANCH" ] && return 1 # detached HEAD — не про наш случай
|
|
169
|
+
|
|
170
|
+
RT_TF_ROOT="$(git rev-parse --show-toplevel 2>/dev/null)"
|
|
171
|
+
[ -z "$RT_TF_ROOT" ] && return 1
|
|
172
|
+
|
|
173
|
+
# Каталог папок задач: у дерева он свой, но имя обычно общее.
|
|
174
|
+
RT_TF_TASKS_DIR="${RT_TASKS_DIR:-docs/tasks}"
|
|
175
|
+
RT_TF_MAIN_BRANCH="${RT_MAIN_BRANCH:-main}"
|
|
176
|
+
RT_TF_DIR="$RT_TF_ROOT/$RT_TF_TASKS_DIR/$RT_TF_BRANCH"
|
|
177
|
+
RT_TF_PLAN="$RT_TF_DIR/plan.md"
|
|
178
|
+
|
|
179
|
+
return 0
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
# Отказ гарда хода работы: причина первым параметром, законная форма обхода — вторым. Хвост
|
|
183
|
+
# дописывается здесь, а не в каждом тексте: пропущенный в одном месте, он читается как «у этого
|
|
184
|
+
# отказа ходов нет». Хвост может быть не разложен — тогда его нет, а причина остаётся прежней.
|
|
185
|
+
rt_task_flow_deny() {
|
|
186
|
+
rt_tf_reason="$1"
|
|
187
|
+
if command -v rt_deny_tail >/dev/null 2>&1; then
|
|
188
|
+
rt_tf_tail="$(rt_deny_tail "$2")"
|
|
189
|
+
[ -n "$rt_tf_tail" ] && rt_tf_reason="$1 ${rt_tf_tail}"
|
|
190
|
+
fi
|
|
191
|
+
jq -n --arg r "$rt_tf_reason" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$r}}' 2>/dev/null \
|
|
192
|
+
|| printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"%s"}}\n' "$rt_tf_reason"
|
|
193
|
+
exit 0
|
|
194
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
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
|
+
# Требует: hooks/task-flow-context.sh, hooks/task-flow-guard.sh, hooks/deny-tail.sh
|
|
4
|
+
# PreToolUse guard for Edit|Write|MultiEdit: код не пишется раньше договорённости о продукте.
|
|
5
|
+
#
|
|
6
|
+
# Замысел говорит, как работа будет сделана, и молчит о том, о чём договорились с владельцем.
|
|
7
|
+
# Договорённость о продукте — единственный текст, который называет обещанное поведение до кода;
|
|
8
|
+
# написанная после, она пересказывает состоявшееся решение, а не проверяет его.
|
|
9
|
+
#
|
|
10
|
+
# Гард требует одно и ровно одно: замысел называет договорённость строкой `**Драфт:** \`путь\``,
|
|
11
|
+
# и названное существует. Папку задачи, сам замысел и объявленное состояние требует
|
|
12
|
+
# `task-flow-guard`, и до его отказов этот гард не судит ничего: замысла нет на диске — молчит.
|
|
13
|
+
#
|
|
14
|
+
# Требования разведены затем, чтобы дерево могло отказаться от одного, сохранив второе. Пока
|
|
15
|
+
# оба ехали одним файлом, отказ от требования договорённости снимал заодно требование папки
|
|
16
|
+
# задачи и замысла — то есть всё, ради чего гард заводился.
|
|
17
|
+
#
|
|
18
|
+
# Осознанный выход есть: строка `**Поведение:** не меняется — <причина>` в замысле снимает
|
|
19
|
+
# требование договорённости. Пустая причина не принимается, как и у `Docs-skip:`.
|
|
20
|
+
#
|
|
21
|
+
# Правило целиком — скил `task-flow`.
|
|
22
|
+
#
|
|
23
|
+
# FAIL-OPEN: нет jq, не git-репозиторий, битый ввод, чужой инструмент → пропуск. Сломанный
|
|
24
|
+
# гард не должен мешать работать.
|
|
25
|
+
|
|
26
|
+
rt_hooks_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
27
|
+
|
|
28
|
+
# Разбор вызова — общий у обоих гардов хода работы. Файл может быть не разложен: тогда судить
|
|
29
|
+
# нечем, и гард молчит.
|
|
30
|
+
# shellcheck disable=SC1090
|
|
31
|
+
[ -f "$rt_hooks_dir/task-flow-context.sh" ] && . "$rt_hooks_dir/task-flow-context.sh"
|
|
32
|
+
command -v rt_task_flow_context >/dev/null 2>&1 || exit 0
|
|
33
|
+
|
|
34
|
+
# Общий хвост отказа: два законных хода и законная форма обхода, если она у отказа есть. Файл
|
|
35
|
+
# может быть не разложен — тогда хвоста нет, а причина отказа остаётся прежней.
|
|
36
|
+
# shellcheck disable=SC1090
|
|
37
|
+
[ -f "$rt_hooks_dir/deny-tail.sh" ] && . "$rt_hooks_dir/deny-tail.sh"
|
|
38
|
+
|
|
39
|
+
rt_task_flow_context || exit 0
|
|
40
|
+
|
|
41
|
+
branch="$RT_TF_BRANCH"
|
|
42
|
+
root="$RT_TF_ROOT"
|
|
43
|
+
tasks_dir="$RT_TF_TASKS_DIR"
|
|
44
|
+
plan="$RT_TF_PLAN"
|
|
45
|
+
|
|
46
|
+
deny() { rt_task_flow_deny "$@"; }
|
|
47
|
+
|
|
48
|
+
# Замысла нет — это чужое требование, и отказ о нём печатает гард хода работы. Отсюда же уходит
|
|
49
|
+
# и работа с разобранной папкой: замысел с диска к этой минуте снят намеренно.
|
|
50
|
+
[ -f "$plan" ] || exit 0
|
|
51
|
+
|
|
52
|
+
# Строка обхода: поведение не меняется, договорённость о продукте не нужна. Причина обязана
|
|
53
|
+
# стоять — без неё обход становится умолчанием.
|
|
54
|
+
if grep -qE '^\*\*Поведение:\*\*[[:space:]]*не меняется[[:space:]]*—[[:space:]]*\S' "$plan" 2>/dev/null; then
|
|
55
|
+
exit 0
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
# Договорённость называется одним из двух видов. Первый — отдельный документ в каталоге
|
|
59
|
+
# «предложено»: он переезжает в спек домена, когда работа закрывается. Второй — сам спек домена:
|
|
60
|
+
# дерево, у которого отдельного каталога нет, пишет договорённость прямо в него, и переезжать там
|
|
61
|
+
# нечему — черновик совпадает со спеком с первого дня. Требовать один вид значит навязывать способ
|
|
62
|
+
# записи договорённостей вместе с проверкой того, что работа идёт по замыслу: дерево со вторым
|
|
63
|
+
# способом отказывается от гарда целиком и остаётся без единственной машинной проверки под правило.
|
|
64
|
+
draft="$(sed -n 's/^\*\*Драфт:\*\*[[:space:]]*`\([^`]*\)`.*/\1/p' "$plan" 2>/dev/null | head -1)"
|
|
65
|
+
[ -z "$draft" ] && draft="$(sed -n 's/^\*\*Спек:\*\*[[:space:]]*`\([^`]*\)`.*/\1/p' "$plan" 2>/dev/null | head -1)"
|
|
66
|
+
|
|
67
|
+
if [ -z "$draft" ]; then
|
|
68
|
+
deny "BLOCKED by task-flow: в '${tasks_dir}/${branch}/plan.md' не названа договорённость о продукте. Назови её одной из двух строк: '**Драфт:** \`путь\`' — отдельный документ в docs/specs/<домен>/proposed/<фича>/, либо '**Спек:** \`путь\`' — спек домена, в который договорённость пишется прямо. Правило — скил task-flow." \
|
|
69
|
+
"строка '**Поведение:** не меняется — <причина владельца>' в замысле; пустая причина не принимается"
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
case "$draft" in
|
|
73
|
+
/*) draft_path="$draft" ;;
|
|
74
|
+
*) draft_path="$root/$draft" ;;
|
|
75
|
+
esac
|
|
76
|
+
|
|
77
|
+
if [ -e "$draft_path" ]; then
|
|
78
|
+
exit 0
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
# Договорённость, влитая в спек домена, с диска уходит — так и задумано: в главной ветке
|
|
82
|
+
# директории «предложено» быть не должно. Но замысел на неё ссылается до конца работы, и без
|
|
83
|
+
# этой развилки последний коммит PR запирал бы ветку: ни правки по замечаниям разбора, ни
|
|
84
|
+
# записи в журнал изменений после вливания уже не сделать.
|
|
85
|
+
#
|
|
86
|
+
# Влитое от незаведённого отличает история ветки: путь, которого в ней никогда не было,
|
|
87
|
+
# договорённостью не был. Спросить об этом нечем, кроме git, поэтому нет git — отказ остаётся.
|
|
88
|
+
if git -C "$root" log --oneline -1 -- "$draft" 2>/dev/null | grep -q .; then
|
|
89
|
+
exit 0
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
# Вливание бывает и первым коммитом, который вообще завёл названный путь: черновик писали, не
|
|
93
|
+
# коммитя, а в историю уехал уже спек домена. Тогда истории у пути нет, а работа сделана ровно
|
|
94
|
+
# так, как велит паттерн закрытия, — и отказ запирал бы ветку на последнем шаге. Признак:
|
|
95
|
+
# договорённость названа путём вида `<домен>/proposed/<фича>`, каталог домена в дереве есть, и
|
|
96
|
+
# имя фичи в нём встречается. Пока такого спека нет, отказ остаётся прежним.
|
|
97
|
+
case "$draft" in
|
|
98
|
+
*/proposed/*)
|
|
99
|
+
domain_dir="${draft%%/proposed/*}"
|
|
100
|
+
feature="${draft##*/proposed/}"
|
|
101
|
+
feature="${feature%/}"
|
|
102
|
+
if [ -n "$feature" ] && [ -d "$root/$domain_dir" ] \
|
|
103
|
+
&& grep -rq -- "$feature" "$root/$domain_dir" 2>/dev/null; then
|
|
104
|
+
exit 0
|
|
105
|
+
fi
|
|
106
|
+
;;
|
|
107
|
+
esac
|
|
108
|
+
|
|
109
|
+
deny "BLOCKED by task-flow: замысел называет договорённость '${draft}', а её на диске нет и в истории ветки не было. Заведи её с образца (docs/specs/_template) или поправь путь в '${tasks_dir}/${branch}/plan.md'. Правило — скил task-flow."
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# rt-hook: PreToolUse Edit|Write|MultiEdit|Bash|mcp__webstorm__create_new_file|mcp__webstorm__execute_terminal_command|mcp__webstorm__execute_tool
|
|
3
|
-
# Требует: hooks/profile-check.sh, hooks/deny-tail.sh
|
|
3
|
+
# Требует: hooks/task-flow-context.sh, hooks/profile-check.sh, hooks/deny-tail.sh
|
|
4
4
|
# PreToolUse guard for Edit|Write|MultiEdit: код не пишется раньше замысла.
|
|
5
5
|
#
|
|
6
6
|
# Работа идёт много заходов, и между ними исполнитель не помнит ничего. Замысел, лежащий на
|
|
@@ -8,145 +8,48 @@
|
|
|
8
8
|
# закоммичены, PR не открыт, а очередь работ показывает задачу начатой и молчит о том, что
|
|
9
9
|
# внутри неё сделано.
|
|
10
10
|
#
|
|
11
|
-
# Гард требует
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
# законе `docs/constitution/work-conduct.md`).
|
|
11
|
+
# Гард требует три вещи и ровно их: папку задачи по имени ветки, замысел в ней и объявленное в
|
|
12
|
+
# ходе работы состояние — из тех, в которых код правится. Полноту написанного он не судит — это
|
|
13
|
+
# за владельцем (решения в законе `docs/constitution/work-conduct.md`).
|
|
15
14
|
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
15
|
+
# Договорённость о продукте требует свой гард — `task-flow-draft-guard`, объявленный на те же
|
|
16
|
+
# события. Разведены они затем, чтобы дерево могло отказаться от одного требования, сохранив
|
|
17
|
+
# второе: пока оба ехали одним файлом, отказ от требования договорённости снимал заодно и
|
|
18
|
+
# требование папки задачи, а держать их вместе дерево не просило никогда.
|
|
19
19
|
#
|
|
20
20
|
# Правило целиком — скил `task-flow`.
|
|
21
21
|
#
|
|
22
|
-
# Осознанный выход есть: строка `**Поведение:** не меняется — <причина>` в замысле снимает
|
|
23
|
-
# требование договорённости. Пустая причина не принимается, как и у `Docs-skip:`.
|
|
24
|
-
#
|
|
25
22
|
# FAIL-OPEN: нет jq, не git-репозиторий, битый ввод, чужой инструмент → пропуск. Сломанный
|
|
26
23
|
# гард не должен мешать работать.
|
|
27
24
|
|
|
28
|
-
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
|
|
29
|
-
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
|
|
30
|
-
|
|
31
|
-
rt_hook_read
|
|
32
|
-
input="$RT_HOOK_INPUT"
|
|
33
|
-
[ -z "$input" ] && exit 0
|
|
34
|
-
command -v jq >/dev/null 2>&1 || exit 0
|
|
35
|
-
|
|
36
|
-
# Профиль дерева: сперва умолчание пакета, поверх него — надстройка проекта, если она есть.
|
|
37
|
-
# Читается до разбора пути: пути из команды оболочки вынимает как раз профиль.
|
|
38
25
|
rt_hooks_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
39
|
-
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
|
|
40
|
-
# shellcheck disable=SC1090
|
|
41
|
-
[ -f "$profile" ] && . "$profile" 2>/dev/null
|
|
42
|
-
done
|
|
43
26
|
|
|
44
|
-
#
|
|
45
|
-
#
|
|
27
|
+
# Разбор вызова — общий у обоих гардов хода работы. Файл может быть не разложен: тогда судить
|
|
28
|
+
# нечем, и гард молчит.
|
|
46
29
|
# shellcheck disable=SC1090
|
|
47
|
-
[ -f "$rt_hooks_dir/
|
|
48
|
-
command -v
|
|
49
|
-
|
|
50
|
-
tool="$(rt_hook_tool)"
|
|
51
|
-
candidates=""
|
|
52
|
-
case "$tool" in
|
|
53
|
-
# Инструмент редактора заводит файл теми же двумя данными, только называет их иначе —
|
|
54
|
-
# без этой ветки правка шла бы мимо гарда сменой инструмента.
|
|
55
|
-
Edit | Write | MultiEdit | mcp__webstorm__create_new_file)
|
|
56
|
-
candidates="$(printf '%s' "$input" | jq -r '.tool_input.file_path // .tool_input.pathInProject // empty' 2>/dev/null)"
|
|
57
|
-
;;
|
|
58
|
-
# Второй ярус: та же правка, положенная командой оболочки. Без него отказ гарда обходится
|
|
59
|
-
# сменой не инструмента, а способа записи — перенаправлением, `sed -i`, интерпретатором с
|
|
60
|
-
# heredoc. Разбор — `2026-08-15-guard-denied-shell-wrote-anyway.md`.
|
|
61
|
-
#
|
|
62
|
-
# Терминал среды исполняет ту же командную строку и кладёт её в то же поле: без этих двух
|
|
63
|
-
# имён гард стоял бы объявленным на них и молча пропускал — состояние хуже необъявленного,
|
|
64
|
-
# потому что снаружи выглядит закрытым.
|
|
65
|
-
Bash | mcp__webstorm__execute_terminal_command | mcp__webstorm__execute_tool)
|
|
66
|
-
cmd="$(rt_hook_cmd)"
|
|
67
|
-
[ -z "$cmd" ] && exit 0
|
|
68
|
-
# Универсальный исполнитель прячет настоящую команду во вложенной строке: без её разбора
|
|
69
|
-
# путь стоит за кавычкой, и до него не дотягивается ни один образец.
|
|
70
|
-
if [ "$tool" = "mcp__webstorm__execute_tool" ] && command -v perl >/dev/null 2>&1; then
|
|
71
|
-
inner="$(printf '%s' "$cmd" | perl -0ne '
|
|
72
|
-
if (/--command(?:=|\s+)(?:"((?:[^"\\]|\\.)*)"|\x27([^\x27]*)\x27|(.+))/s) {
|
|
73
|
-
print defined $1 ? $1 : (defined $2 ? $2 : $3);
|
|
74
|
-
}
|
|
75
|
-
' 2>/dev/null)"
|
|
76
|
-
[ -n "$inner" ] && cmd="$inner"
|
|
77
|
-
fi
|
|
78
|
-
rt_needs rt_shell_writes task-flow-guard || exit 0
|
|
79
|
-
rt_needs rt_shell_paths task-flow-guard || exit 0
|
|
80
|
-
rt_shell_writes "$cmd" || exit 0
|
|
81
|
-
candidates="$(rt_shell_paths "$cmd")"
|
|
82
|
-
;;
|
|
83
|
-
*) exit 0 ;;
|
|
84
|
-
esac
|
|
85
|
-
[ -z "$candidates" ] && exit 0
|
|
86
|
-
|
|
87
|
-
# Признак «правка меняет поведение» — путь, а не оценка на глаз: оценку назначает тот, кому
|
|
88
|
-
# она мешает, и порог плывёт. Где живёт код приложения, знает профиль: правила, тексты, обвязка
|
|
89
|
-
# и зависимости под требование не попадают — иначе разбор задачи нельзя было бы вести до
|
|
90
|
-
# заведения ветки.
|
|
91
|
-
rt_needs rt_is_app_code task-flow-guard || exit 0
|
|
92
|
-
|
|
93
|
-
# Судится каждый названный путь: команда пишет столько файлов, сколько в ней стоит, и одного
|
|
94
|
-
# под требованием довольно, чтобы отбить её целиком.
|
|
95
|
-
path=""
|
|
96
|
-
while IFS= read -r candidate; do
|
|
97
|
-
[ -z "$candidate" ] && continue
|
|
98
|
-
case "$candidate" in
|
|
99
|
-
/*) ;;
|
|
100
|
-
*) candidate="${CLAUDE_PROJECT_DIR:-.}/$candidate" ;;
|
|
101
|
-
esac
|
|
102
|
-
if rt_is_app_code "$candidate"; then
|
|
103
|
-
path="$candidate"
|
|
104
|
-
break
|
|
105
|
-
fi
|
|
106
|
-
done <<EOF
|
|
107
|
-
$candidates
|
|
108
|
-
EOF
|
|
109
|
-
[ -z "$path" ] && exit 0
|
|
110
|
-
|
|
111
|
-
# Каталог папок задач: у дерева он свой, но имя обычно общее.
|
|
112
|
-
tasks_dir="${RT_TASKS_DIR:-docs/tasks}"
|
|
113
|
-
main_branch="${RT_MAIN_BRANCH:-main}"
|
|
30
|
+
[ -f "$rt_hooks_dir/task-flow-context.sh" ] && . "$rt_hooks_dir/task-flow-context.sh"
|
|
31
|
+
command -v rt_task_flow_context >/dev/null 2>&1 || exit 0
|
|
114
32
|
|
|
115
33
|
# Общий хвост отказа: два законных хода и законная форма обхода, если она у отказа есть. Файл
|
|
116
34
|
# может быть не разложен — тогда хвоста нет, а причина отказа остаётся прежней.
|
|
117
35
|
# shellcheck disable=SC1090
|
|
118
36
|
[ -f "$rt_hooks_dir/deny-tail.sh" ] && . "$rt_hooks_dir/deny-tail.sh"
|
|
119
|
-
command -v rt_deny_tail >/dev/null 2>&1 || rt_deny_tail() { :; }
|
|
120
37
|
|
|
121
|
-
|
|
122
|
-
# не в каждом тексте: пропущенный в одном месте, он читается как «у этого отказа ходов нет».
|
|
123
|
-
deny() {
|
|
124
|
-
reason="$1"
|
|
125
|
-
tail_text="$(rt_deny_tail "$2")"
|
|
126
|
-
[ -n "$tail_text" ] && reason="$1 ${tail_text}"
|
|
127
|
-
jq -n --arg r "$reason" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$r}}' 2>/dev/null \
|
|
128
|
-
|| printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"%s"}}\n' "$reason"
|
|
129
|
-
exit 0
|
|
130
|
-
}
|
|
38
|
+
rt_task_flow_context || exit 0
|
|
131
39
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
40
|
+
branch="$RT_TF_BRANCH"
|
|
41
|
+
root="$RT_TF_ROOT"
|
|
42
|
+
tasks_dir="$RT_TF_TASKS_DIR"
|
|
43
|
+
main_branch="$RT_TF_MAIN_BRANCH"
|
|
44
|
+
dir="$RT_TF_DIR"
|
|
45
|
+
plan="$RT_TF_PLAN"
|
|
137
46
|
|
|
138
|
-
|
|
139
|
-
[ -z "$branch" ] && exit 0 # detached HEAD — не про наш случай
|
|
47
|
+
deny() { rt_task_flow_deny "$@"; }
|
|
140
48
|
|
|
141
49
|
if rt_needs rt_task_branch_ok task-flow-guard && ! rt_task_branch_ok "$branch"; then
|
|
142
50
|
deny "BLOCKED by task-flow: правка кода идёт в ветке под задачу, а текущая ветка — '${branch}'. Заведи задачу (npm run task:new -- --title '…' --slug <slug>) и ветку под её номером, затем повтори. Правило — скил task-flow."
|
|
143
51
|
fi
|
|
144
52
|
|
|
145
|
-
root="$(git rev-parse --show-toplevel 2>/dev/null)"
|
|
146
|
-
[ -z "$root" ] && exit 0
|
|
147
|
-
dir="$root/$tasks_dir/$branch"
|
|
148
|
-
plan="$dir/plan.md"
|
|
149
|
-
|
|
150
53
|
# Папка, разобранная коммитом этой ветки, — признак того, что работа отдана. Замысел с диска к
|
|
151
54
|
# этой минуте снят намеренно: уборка стоит до открытия заявки, потому что кнопку слияния
|
|
152
55
|
# нажимает человек на хостинге и закрывающему коммиту места после одобрения не остаётся.
|
|
@@ -154,11 +57,11 @@ plan="$dir/plan.md"
|
|
|
154
57
|
# бы запирать ветку собственным порядком. Признак берётся из истории ветки, а не с диска:
|
|
155
58
|
# снесённая, но не закоммиченная папка отданной работы не означает.
|
|
156
59
|
folder_archived() {
|
|
157
|
-
[ -n "$(git ls-tree -d --name-only HEAD -- "$tasks_dir/$branch" 2>/dev/null | head -1)" ] && return 1
|
|
158
|
-
base="$(git merge-base "$main_branch" HEAD 2>/dev/null)"
|
|
60
|
+
[ -n "$(git -C "$root" ls-tree -d --name-only HEAD -- "$tasks_dir/$branch" 2>/dev/null | head -1)" ] && return 1
|
|
61
|
+
base="$(git -C "$root" merge-base "$main_branch" HEAD 2>/dev/null)"
|
|
159
62
|
[ -z "$base" ] && return 1
|
|
160
|
-
had="$(git ls-tree -d --name-only "$base" -- "$tasks_dir/$branch" 2>/dev/null | head -1)"
|
|
161
|
-
[ -z "$had" ] && had="$(git log "$base..HEAD" --diff-filter=A --name-only --pretty=format: -- "$tasks_dir/$branch" 2>/dev/null | head -1)"
|
|
63
|
+
had="$(git -C "$root" ls-tree -d --name-only "$base" -- "$tasks_dir/$branch" 2>/dev/null | head -1)"
|
|
64
|
+
[ -z "$had" ] && had="$(git -C "$root" log "$base..HEAD" --diff-filter=A --name-only --pretty=format: -- "$tasks_dir/$branch" 2>/dev/null | head -1)"
|
|
162
65
|
[ -n "$had" ]
|
|
163
66
|
}
|
|
164
67
|
|
|
@@ -215,7 +118,7 @@ case "$state" in
|
|
|
215
118
|
;;
|
|
216
119
|
esac
|
|
217
120
|
|
|
218
|
-
# Папка задачи едет в ветку коммитом, а не живёт в одном рабочем дереве.
|
|
121
|
+
# Папка задачи едет в ветку коммитом, а не живёт в одном рабочем дереве. Три требования выше
|
|
219
122
|
# смотрят диск, и папка, ни разу не закоммиченная, проходит их все без единого отказа — а
|
|
220
123
|
# признак отданной работы гард берёт из истории, и там её нет. Отказ приходит в последней точке,
|
|
221
124
|
# на открытии заявки, когда папка уже разобрана своими руками: чинить нечего, замысел снят, и
|
|
@@ -228,44 +131,9 @@ esac
|
|
|
228
131
|
#
|
|
229
132
|
# Спрашивается та же история, что и у признака отданной работы: папка стоит в `HEAD` либо её
|
|
230
133
|
# добавлял коммит ветки. Нет git — требования нет: спросить историю нечем.
|
|
231
|
-
if [ -n "$(git rev-parse --verify HEAD 2>/dev/null)" ]; then
|
|
232
|
-
in_tree="$(git ls-tree -d --name-only HEAD -- "$tasks_dir/$branch" 2>/dev/null | head -1)"
|
|
134
|
+
if [ -n "$(git -C "$root" rev-parse --verify HEAD 2>/dev/null)" ]; then
|
|
135
|
+
in_tree="$(git -C "$root" ls-tree -d --name-only HEAD -- "$tasks_dir/$branch" 2>/dev/null | head -1)"
|
|
233
136
|
if [ -z "$in_tree" ]; then
|
|
234
137
|
deny "BLOCKED by task-flow: папка задачи '${tasks_dir}/${branch}' лежит в рабочем дереве, а в историю ветки не заведена. Заведи её коммитом (git add ${tasks_dir}/${branch} && git commit), затем повтори: признак отданной работы гард берёт из истории, и с некоммиченной папкой отказ придёт на открытии заявки — когда папка уже разобрана и чинить нечего. Правило — скил task-flow."
|
|
235
138
|
fi
|
|
236
139
|
fi
|
|
237
|
-
|
|
238
|
-
# Строка обхода: поведение не меняется, договорённость о продукте не нужна. Причина обязана
|
|
239
|
-
# стоять — без неё обход становится умолчанием.
|
|
240
|
-
if grep -qE '^\*\*Поведение:\*\*[[:space:]]*не меняется[[:space:]]*—[[:space:]]*\S' "$plan" 2>/dev/null; then
|
|
241
|
-
exit 0
|
|
242
|
-
fi
|
|
243
|
-
|
|
244
|
-
draft="$(sed -n 's/^\*\*Драфт:\*\*[[:space:]]*`\([^`]*\)`.*/\1/p' "$plan" 2>/dev/null | head -1)"
|
|
245
|
-
|
|
246
|
-
if [ -z "$draft" ]; then
|
|
247
|
-
deny "BLOCKED by task-flow: в '${tasks_dir}/${branch}/plan.md' не названа договорённость о продукте. Заведи её в docs/specs/<домен>/proposed/<фича>/ и укажи строкой '**Драфт:** \`путь\`'. Правило — скил task-flow." \
|
|
248
|
-
"строка '**Поведение:** не меняется — <причина владельца>' в замысле; пустая причина не принимается"
|
|
249
|
-
fi
|
|
250
|
-
|
|
251
|
-
case "$draft" in
|
|
252
|
-
/*) draft_path="$draft" ;;
|
|
253
|
-
*) draft_path="$root/$draft" ;;
|
|
254
|
-
esac
|
|
255
|
-
|
|
256
|
-
if [ -e "$draft_path" ]; then
|
|
257
|
-
exit 0
|
|
258
|
-
fi
|
|
259
|
-
|
|
260
|
-
# Договорённость, влитая в спек домена, с диска уходит — так и задумано: в главной ветке
|
|
261
|
-
# директории «предложено» быть не должно. Но замысел на неё ссылается до конца работы, и без
|
|
262
|
-
# этой развилки последний коммит PR запирал бы ветку: ни правки по замечаниям разбора, ни
|
|
263
|
-
# записи в журнал изменений после вливания уже не сделать.
|
|
264
|
-
#
|
|
265
|
-
# Влитое от незаведённого отличает история ветки: путь, которого в ней никогда не было,
|
|
266
|
-
# договорённостью не был. Спросить об этом нечем, кроме git, поэтому нет git — отказ остаётся.
|
|
267
|
-
if git -C "$root" log --oneline -1 -- "$draft" 2>/dev/null | grep -q .; then
|
|
268
|
-
exit 0
|
|
269
|
-
fi
|
|
270
|
-
|
|
271
|
-
deny "BLOCKED by task-flow: замысел называет договорённость '${draft}', а её на диске нет и в истории ветки не было. Заведи её с образца (docs/specs/_template) или поправь путь в '${tasks_dir}/${branch}/plan.md'. Правило — скил task-flow."
|