@rt-tools/agent-kit 0.17.0 → 0.19.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.github.mjs +77 -73
- package/assets/checks/check-board.github.mjs +39 -3
- package/assets/checks/check-hook-scope.mjs +126 -0
- package/assets/checks/check-profile-drift.mjs +195 -0
- 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/defaults/project.sh +21 -117
- package/assets/defaults/shell.sh +139 -0
- 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-signature.sh +14 -5
- package/assets/hooks/git-guard-delivery.sh +15 -2
- package/assets/hooks/grill-gate.sh +63 -6
- package/assets/hooks/proposal-guard.sh +11 -4
- package/assets/hooks/rule-source-guard.sh +8 -13
- package/assets/hooks/task-context-load.sh +22 -0
- package/assets/hooks/task-flow-context.sh +9 -0
- package/assets/hooks/task-flow-draft-guard.sh +17 -0
- package/assets/hooks/turn-exit-guard.sh +45 -87
- package/assets/hooks/waiting-turn-guard.sh +65 -2
- package/assets/hooks/work-start-guard.sh +166 -0
- package/assets/hooks/write-targets.sh +24 -0
- package/assets/laws/verifiability.md +28 -0
- package/assets/patterns/browser-verification-measure.md +49 -1
- package/assets/patterns/browser-verification-stand.md +10 -2
- package/assets/patterns/doc-style-write.md +21 -1
- package/assets/patterns/git-workflow-commit.github.md +32 -3
- package/assets/patterns/git-workflow-docker.md +8 -0
- package/assets/patterns/git-workflow-freshness.md +87 -0
- package/assets/patterns/git-workflow-merge.md +23 -0
- package/assets/patterns/git-workflow-pr.github.md +6 -0
- package/assets/patterns/git-workflow-restart.md +18 -0
- package/assets/patterns/git-workflow-stack.md +30 -0
- package/assets/patterns/lib-layers-move.md +4 -0
- package/assets/patterns/spec-driven-domain.md +13 -1
- package/assets/patterns/spec-driven-rule.md +5 -4
- package/assets/patterns/spec-driven-sweep.md +1 -1
- package/assets/patterns/status-report-table.github.md +1 -1
- package/assets/patterns/task-flow-archive.md +12 -2
- package/assets/patterns/task-flow-close.md +31 -24
- package/assets/patterns/task-flow-handoff.md +14 -2
- package/assets/patterns/task-flow-resume.md +18 -3
- package/assets/patterns/task-flow-start.md +23 -23
- package/assets/patterns/testing-e2e.md +23 -0
- package/assets/patterns/turn-entry-map.md +1 -1
- package/assets/pitfalls/agent-kit.md +4 -6
- package/assets/pitfalls/doc-style.md +10 -0
- package/assets/pitfalls/git-workflow.github.md +20 -0
- package/assets/pitfalls/spec-driven.md +6 -0
- package/assets/pitfalls/task-flow.md +20 -0
- package/assets/pitfalls/testing.md +6 -0
- package/assets/pitfalls/turn-conduct.md +35 -0
- package/assets/rules/browser-verification.md +19 -0
- package/assets/rules/deploy-flow.github.md +9 -0
- package/assets/rules/doc-style.md +6 -0
- package/assets/rules/git-workflow.azure.md +17 -2
- package/assets/rules/git-workflow.github.md +89 -90
- package/assets/rules/git-workflow.gitlab.md +12 -4
- package/assets/rules/lists.md +5 -0
- package/assets/rules/observability.needs-app.md +4 -0
- package/assets/rules/reuse-first.md +6 -0
- package/assets/rules/spec-driven.md +29 -25
- package/assets/rules/task-flow.md +52 -46
- package/assets/rules/testing.md +27 -1
- package/assets/rules/turn-conduct.md +46 -43
- package/assets/skills/agent-kit.md +9 -0
- package/package.json +1 -1
- package/rt-tools-agent-kit-0.19.0.tgz +0 -0
- package/rt-tools-agent-kit-0.17.0.tgz +0 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Разбор команды оболочки: пишет ли она и какие пути называет.
|
|
3
|
+
#
|
|
4
|
+
# Умолчание пакета, часть профиля дерева — грузится из `project.sh`, рядом с которым лежит.
|
|
5
|
+
# Отдельным файлом потому, что вдвоём они перерастают предел длины, а читают их порознь: одному
|
|
6
|
+
# нужен признак записи, другому — цель.
|
|
7
|
+
|
|
8
|
+
# Пишет ли команда оболочки файл. Успех — да, и тогда пути из неё судятся тем же признаком,
|
|
9
|
+
# что и путь из вызова инструмента правки.
|
|
10
|
+
#
|
|
11
|
+
# Гарды подписаны на инструменты правки файла, и этого мало: ту же правку кладут командой —
|
|
12
|
+
# перенаправлением, `tee`, `sed -i`, интерпретатором с heredoc. Отбитая правка дважды за один
|
|
13
|
+
# заход легла именно так, и не увидел этого никто: в дереве она неотличима от положенной
|
|
14
|
+
# инструментом. Разбор — `2026-08-15-guard-denied-shell-wrote-anyway.md`.
|
|
15
|
+
#
|
|
16
|
+
# Список намеренно широк, и цена этого названа: команда чтения, в которой стоит имя
|
|
17
|
+
# интерпретатора, будет отбита наравне с командой правки. Узкий список стоил бы дороже —
|
|
18
|
+
# пропущенная форма записи возвращает обход целиком, а найти её можно только промахом.
|
|
19
|
+
#
|
|
20
|
+
# Перенаправление в пустое устройство и в поток ошибок снимается до разбора: файла оно не
|
|
21
|
+
# пишет, а выглядит как перенаправление в файл. Так глушат вывод команды чтения, и без этого
|
|
22
|
+
# `grep -rn x libs/ 2>/dev/null` судится наравне с записью — правило требуется на чтение, а
|
|
23
|
+
# отбитий, пришедшихся не на правку файла, набирается большинство. Настоящая запись рядом с
|
|
24
|
+
# заглушённым потоком остаётся видной: снимается перенаправление, а не команда целиком.
|
|
25
|
+
#
|
|
26
|
+
# Стрелка снимается там же и по той же причине. `->` и `=>` в оболочке не значат ничего, а знак
|
|
27
|
+
# в них тот же: команда, печатавшая таблицу «путь -> правило», объявлялась пишущей и отдавала
|
|
28
|
+
# все свои пути под суд. Туда же закрывающая скобка комментария разметки.
|
|
29
|
+
#
|
|
30
|
+
# Цель перенаправления сужена до знаков, из которых собирают пути. Строка цитаты разметки —
|
|
31
|
+
# знак и слово через пробел — от записи в файл одним знаком неотличима, и различает их только
|
|
32
|
+
# цель: за настоящим знаком стоит путь, а не слово словами. Цена названа прямо: путь, набранный
|
|
33
|
+
# не латиницей, записью больше не считается — в дереве таких нет ни одного, а появятся, признак
|
|
34
|
+
# придётся расширить.
|
|
35
|
+
rt_shell_writes_default() {
|
|
36
|
+
printf '%s' "$1" \
|
|
37
|
+
| sed -E 's#(&|[0-9]*)>>?[[:space:]]*/dev/(null|stderr)##g; s#[0-9]*>&[0-9-]##g; s#[-=]+>##g' \
|
|
38
|
+
| grep -Eq \
|
|
39
|
+
'>>?[[:space:]]*[A-Za-z0-9_./~$"'"'"'-]|\btee\b|\bsed\b[^|]*-i|\bperl\b[^|]*-i|\bpython3?\b|\bnode\b|\bruby\b|\bdd\b[^|]*of=|\bcp\b|\bmv\b|\brm\b|\btouch\b|\btruncate\b|\binstall\b|\bpatch\b|\bgit[[:space:]]+(checkout|restore|apply|stash)\b'
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# Пути, названные командой оболочки. Печатает по одному в строке; судит их зовущий.
|
|
43
|
+
#
|
|
44
|
+
# Разбирать оболочку по-настоящему нечем — здесь и не разбирают: из текста вынимается всё, что
|
|
45
|
+
# похоже на путь, и каждое отдаётся признаку. Лишнее он отсеет сам, а пропущенное вернуло бы
|
|
46
|
+
# обход. Кавычки снимаются заменой на пробел: путь внутри них тот же самый.
|
|
47
|
+
#
|
|
48
|
+
# Тело документа на месте путей не даёт: там лежит текст, который команда кладёт в файл, а
|
|
49
|
+
# чужой путь, названный в нём словами, требовал бы правила под запись, которой нет. Запись
|
|
50
|
+
# замысла так отбивалась трижды подряд, пока пути под каталогом кода не были названы иначе.
|
|
51
|
+
# Судится заголовок команды — именно в нём стоит тот путь, куда команда пишет.
|
|
52
|
+
#
|
|
53
|
+
# Исключение — интерпретатор: ему код приходит телом, и путь записи стоит именно там. Признак
|
|
54
|
+
# читается у той строки, которая тело открыла, а не у всей команды: тело принадлежит команде
|
|
55
|
+
# своего заголовка. Прежде он читался у всего текста разом, и слово из документа отключало
|
|
56
|
+
# вырезание целиком — строка «**Чем проверяется:** `bash projects/…`» в замысле делала запись
|
|
57
|
+
# `plan.md` правкой кода приложения. Гард отбивал тем самым запись того файла, отсутствием
|
|
58
|
+
# которого он же и отказывает.
|
|
59
|
+
rt_shell_paths_default() {
|
|
60
|
+
text="$(printf '%s' "$1" | tr "\"'\`" ' ')"
|
|
61
|
+
text="$(printf '%s' "$text" | awk '
|
|
62
|
+
function trim(s) { sub(/^[ \t]+/, "", s); sub(/[ \t]+$/, "", s); return s }
|
|
63
|
+
tag != "" {
|
|
64
|
+
if (keep) { print }
|
|
65
|
+
if (trim($0) == tag) { tag = ""; keep = 0 }
|
|
66
|
+
next
|
|
67
|
+
}
|
|
68
|
+
{
|
|
69
|
+
print
|
|
70
|
+
if (match($0, /<<-?[ \t]*[A-Za-z_][A-Za-z0-9_]*/)) {
|
|
71
|
+
t = substr($0, RSTART, RLENGTH)
|
|
72
|
+
sub(/^<<-?[ \t]*/, "", t)
|
|
73
|
+
tag = t
|
|
74
|
+
keep = ($0 ~ /(^|[|;&(]|[ \t])(python3?|node|ruby|perl|php|deno|bun|bash|sh|zsh)([ \t]|$)/)
|
|
75
|
+
}
|
|
76
|
+
}')"
|
|
77
|
+
|
|
78
|
+
# Пути берутся только у тех кусков команды, которые пишут. Прежде брались у всей строки
|
|
79
|
+
# целиком, и команда чтения, сцепленная с записью, отдавала свои пути как цели записи:
|
|
80
|
+
# `python3 <<PY … PY` рядом с `grep -n … projects/…` отбивался за правку кода, которой в нём
|
|
81
|
+
# не было. Отбитий, пришедшихся не на правку файла, набиралось большинство, и цену платил
|
|
82
|
+
# тот, кто просто читал соседний файл в той же строке.
|
|
83
|
+
#
|
|
84
|
+
# Кусок — строка верхнего уровня, а внутри неё `;`, `&&` и `||`. Тело heredoc от своей
|
|
85
|
+
# команды не отрывается: оно едет вместе с ней одним куском, потому что путь записи
|
|
86
|
+
# интерпретатора стоит именно там.
|
|
87
|
+
printf '%s' "$text" | awk '
|
|
88
|
+
function trim(s) { sub(/^[ \t]+/, "", s); sub(/[ \t]+$/, "", s); return s }
|
|
89
|
+
function flush( n, i, part) {
|
|
90
|
+
if (chunk == "") { return }
|
|
91
|
+
if (tag != "") { part = chunk; gsub(/\n/, " ", part); print part; chunk = ""; return }
|
|
92
|
+
n = split(chunk, parts, /;|&&|\|\|/)
|
|
93
|
+
for (i = 1; i <= n; i++) { print parts[i] }
|
|
94
|
+
chunk = ""
|
|
95
|
+
}
|
|
96
|
+
tag != "" {
|
|
97
|
+
chunk = chunk "\n" $0
|
|
98
|
+
if (trim($0) == tag) { flush(); tag = "" }
|
|
99
|
+
next
|
|
100
|
+
}
|
|
101
|
+
{
|
|
102
|
+
chunk = $0
|
|
103
|
+
if (match($0, /<<-?[ \t]*[A-Za-z_][A-Za-z0-9_]*/)) {
|
|
104
|
+
t = substr($0, RSTART, RLENGTH)
|
|
105
|
+
sub(/^<<-?[ \t]*/, "", t)
|
|
106
|
+
tag = t
|
|
107
|
+
next
|
|
108
|
+
}
|
|
109
|
+
flush()
|
|
110
|
+
}
|
|
111
|
+
END { if (chunk != "") { part = chunk; gsub(/\n/, " ", part); print part } }
|
|
112
|
+
' \
|
|
113
|
+
| while IFS= read -r piece; do
|
|
114
|
+
[ -z "$piece" ] && continue
|
|
115
|
+
rt_shell_writes "$piece" || continue
|
|
116
|
+
# Кусок, который пишет одним лишь перенаправлением, отдаёт цель записи, а не всякое
|
|
117
|
+
# похожее на путь слово из своего текста. Имя файла, названное в аргументе или в
|
|
118
|
+
# содержимом, правкой этого файла не является: команда чтения, сцепленная с записью
|
|
119
|
+
# в свой файл, требовала правила по чужому имени и кончала ход — цена такого отказа
|
|
120
|
+
# равна цене настоящего срабатывания, потому что обойти его нечем.
|
|
121
|
+
#
|
|
122
|
+
# Прочие виды записи разбираются по-прежнему: у правки по месту, копирования,
|
|
123
|
+
# переноса и интерпретатора путь стоит в самой команде и не в одном месте.
|
|
124
|
+
if ! printf '%s' "$piece" | grep -Eq '\bsed\b[^|]*-i|\bperl\b[^|]*-i|\bpython3?\b|\bnode\b|\bruby\b|\bdd\b[^|]*of=|\bcp\b|\bmv\b|\brm\b|\btouch\b|\btruncate\b|\binstall\b|\bpatch\b|\bgit[[:space:]]+(checkout|restore|apply|stash)\b'; then
|
|
125
|
+
printf '%s' "$piece" \
|
|
126
|
+
| grep -oE '(>>?[[:space:]]*|\btee\b([[:space:]]+-a)?[[:space:]]+)[A-Za-z0-9_@.~/-]+' \
|
|
127
|
+
| sed -E 's/^(>>?|tee([[:space:]]+-a)?)[[:space:]]*//' \
|
|
128
|
+
| grep -E '^[A-Za-z0-9_@.-]*/[A-Za-z0-9_@./-]+$' \
|
|
129
|
+
| sed 's|^\./||'
|
|
130
|
+
continue
|
|
131
|
+
fi
|
|
132
|
+
printf '%s' "$piece" \
|
|
133
|
+
| tr "(),;=" ' ' \
|
|
134
|
+
| tr '[:space:]' '\n' \
|
|
135
|
+
| grep -E '^[A-Za-z0-9_@.-]*/[A-Za-z0-9_@./-]+$' \
|
|
136
|
+
| sed 's|^\./||'
|
|
137
|
+
done \
|
|
138
|
+
| sort -u
|
|
139
|
+
}
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# rt-hook: PreToolUse mcp__playwright__.*|mcp__chrome-devtools__.*|Bash
|
|
2
|
+
# rt-hook: PreToolUse mcp__playwright__.*|mcp__chrome-devtools__.*|Bash|mcp__webstorm__execute_terminal_command|mcp__webstorm__execute_tool
|
|
3
3
|
# Требует: hooks/deny-tail.sh
|
|
4
4
|
# Гард обходных путей к браузеру. PreToolUse.
|
|
5
5
|
#
|
|
6
6
|
# Закрепление профиля чего-то стоит только тогда, когда дверь одна. Здесь перечислены двери,
|
|
7
7
|
# которые обходят её целиком и закреплённый профиль не спрашивают вовсе: второй драйвер,
|
|
8
8
|
# третий драйвер, открытие ссылки средствами системы, управление браузером через сценарий
|
|
9
|
-
# автоматизации, отдельная
|
|
9
|
+
# автоматизации, отдельная утилита, прямой запуск бинарника и браузер, поднятый библиотекой
|
|
10
|
+
# изнутри скрипта.
|
|
11
|
+
#
|
|
12
|
+
# Последняя дверь в строке команды не видна вовсе: там стоят имя интерпретатора и путь файла, а
|
|
13
|
+
# вождение лежит внутри самого файла. Поэтому судится и содержимое запускаемого файла, и код,
|
|
14
|
+
# переданный доводом вместо файла, — на известные точки входа браузерных библиотек. Так
|
|
15
|
+
# закреплённый профиль обошли дважды за один заход, и не остановило ничто.
|
|
10
16
|
#
|
|
11
17
|
# Написание сквозных спек при этом остаётся законным: прогон спеки не выдаёт агенту
|
|
12
18
|
# интерактивный браузер. Отбиваются только глаголы вождения.
|
|
@@ -86,4 +92,37 @@ printf '%s' "$cmd" | grep -qE "${RT_CMD_BOUND}(/[^[:space:]]*/)?(google-chrome|c
|
|
|
86
92
|
printf '%s' "$cmd" | grep -qF 'Google Chrome.app/Contents/MacOS' \
|
|
87
93
|
&& deny "Прямой запуск бинарника браузера обходит закреплённый профиль."
|
|
88
94
|
|
|
95
|
+
# Точки входа браузерных библиотек — то, чем браузер поднимают из кода. Перечислены и глагол
|
|
96
|
+
# запуска, и глагол присоединения к уже поднятому: второй обходит профиль ровно так же.
|
|
97
|
+
launch='(chromium|firefox|webkit|browserType|puppeteer|chromeLauncher)[[:space:]]*\.[[:space:]]*(launch|connect)|launchPersistentContext|webdriver\.(Chrome|Firefox)|chrome-launcher'
|
|
98
|
+
|
|
99
|
+
# Код, переданный доводом вместо файла. Судится только вместе с именем интерпретатора и его
|
|
100
|
+
# флагом кода: голый образец отбивал бы и поиск по дереву, в котором такое слово просто ищут.
|
|
101
|
+
if printf '%s' "$cmd" | grep -qE "${RT_CMD_BOUND}(node|bun|deno|python3?)([[:space:]]+-[^[:space:]]+)*[[:space:]]+(-e|--eval|-c|-p|--print)[[:space:]]" \
|
|
102
|
+
&& printf '%s' "$cmd" | grep -qE "$launch"; then
|
|
103
|
+
deny "Браузер, поднятый библиотекой из кода довода, закреплённый профиль не спрашивает."
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
# Файлы, которые команда запускает интерпретатором. Обёртки менеджеров пакетов снимаются: под
|
|
107
|
+
# ними стоит тот же интерпретатор, а видеть его надо в позиции команды.
|
|
108
|
+
launched="$(printf '%s' "$cmd" | sed -E '
|
|
109
|
+
s/(^|[[:space:]])(npx|bunx)[[:space:]]+/\1/g
|
|
110
|
+
s/(^|[[:space:]])(pnpm|npm|yarn)[[:space:]]+(exec|dlx)[[:space:]]+/\1/g
|
|
111
|
+
s/(^|[[:space:]])deno[[:space:]]+run[[:space:]]+/\1deno /g
|
|
112
|
+
' | tr '|&;' '\n\n\n' | sed -nE '
|
|
113
|
+
s/.*(^|[[:space:]])(node|bun|deno|tsx|ts-node|python3?)[[:space:]]+((-[^[:space:]]+|[A-Za-z_][A-Za-z0-9_]*=[^[:space:]]+)[[:space:]]+)*([^[:space:]]+).*/\5/p
|
|
114
|
+
' | tr -d "\"'\`")"
|
|
115
|
+
|
|
116
|
+
cwd="$(rt_hook_cwd)"
|
|
117
|
+
for file in $launched; do
|
|
118
|
+
for base in '' "${cwd:-.}/" "${CLAUDE_PROJECT_DIR:-.}/"; do
|
|
119
|
+
path="${base}${file}"
|
|
120
|
+
[ -f "$path" ] || continue
|
|
121
|
+
if head -c 200000 "$path" 2>/dev/null | grep -qE "$launch"; then
|
|
122
|
+
deny "Браузер, поднятый библиотекой изнутри ${file}, закреплённый профиль не спрашивает."
|
|
123
|
+
fi
|
|
124
|
+
break
|
|
125
|
+
done
|
|
126
|
+
done
|
|
127
|
+
|
|
89
128
|
exit 0
|
|
@@ -22,6 +22,12 @@
|
|
|
22
22
|
# сверять. Эти случаи держит правило ведения работы, и это известная граница гарда, а не
|
|
23
23
|
# обещание.
|
|
24
24
|
#
|
|
25
|
+
# Не судится и чужое слово: цитата в кавычках, вопрос экзаменатора, пересказ чужого текста,
|
|
26
|
+
# строка кода и предложение в условном наклонении утверждением о дереве не являются. Отбитые
|
|
27
|
+
# за них ходы обходятся дороже пропущенных: гард, отбивающий цитату, учит не проверять дерево,
|
|
28
|
+
# а не писать кавычек. Поэтому перед разбором из сказанного вынимаются кавычки, код, строки
|
|
29
|
+
# цитирования и предложения со словом условия.
|
|
30
|
+
#
|
|
25
31
|
# ОТКАЗ В ПОЛЬЗУ РАБОТЫ: при любой ошибке, нехватке `jq`, отсутствии записи хода или пустом
|
|
26
32
|
# тексте ход РАЗРЕШАЕТСЯ (exit 0). Сломанный гард не имеет права заклинить разговор.
|
|
27
33
|
|
|
@@ -95,6 +101,19 @@ said="$(printf '%s' "$turn" | jq -r '.said // ""' 2>/dev/null)"
|
|
|
95
101
|
ran="$(printf '%s' "$turn" | jq -r '.ran // ""' 2>/dev/null)"
|
|
96
102
|
[ -z "$said" ] && exit 0
|
|
97
103
|
|
|
104
|
+
# Чужое слово вынимается до разбора. Строка цитирования и блок кода уходят целиком, кавычки и
|
|
105
|
+
# код в строке гасятся пробелом, а предложения со словом условия отбрасываются: «прогнал бы» и
|
|
106
|
+
# «если бы линт был зелёным» о состоянии дерева не говорят ничего.
|
|
107
|
+
judged="$(printf '%s\n' "$said" \
|
|
108
|
+
| awk 'BEGIN { code = 0 }
|
|
109
|
+
/^[[:space:]]*```/ { code = !code; next }
|
|
110
|
+
!code && !/^[[:space:]]*>/' \
|
|
111
|
+
| sed -E 's/`[^`]*`/ /g; s/«[^»]*»/ /g; s/"[^"]*"/ /g' \
|
|
112
|
+
| sed -E 's/([.!?])[[:space:]]+/\1\'$'\n''/g' \
|
|
113
|
+
| grep -viE '(^|[[:space:]])(бы|если|разве|неужели|ли)([[:space:]]|[,.]|$)' 2>/dev/null)"
|
|
114
|
+
[ -z "$judged" ] && exit 0
|
|
115
|
+
said="$judged"
|
|
116
|
+
|
|
98
117
|
# Карта утверждений: что сказано о дереве | чем это показывают | что назвать в отказе.
|
|
99
118
|
#
|
|
100
119
|
# Слово берётся в той форме, в какой его говорят владельцу. Будущее время сюда не идёт:
|
|
@@ -128,7 +147,9 @@ for row in "${claims[@]}"; do
|
|
|
128
147
|
|
|
129
148
|
Утверждение о дереве стоит ровно столько, сколько команда, его показавшая: сказанное без команды владелец читает как проверенный факт и узнаёт о расхождении последним.
|
|
130
149
|
|
|
131
|
-
|
|
150
|
+
Первый выход — убрать утверждение из ответа и сказать о сделанном без него. Он же и верный там, где владелец команды не просил: запуск ради снятия отказа бывает опаснее того, что гард стережёт, — так ход, которому нечего было поставлять, дошёл до пуша.
|
|
151
|
+
|
|
152
|
+
Второй — запустить ${name} этим же ходом и назвать её вывод.
|
|
132
153
|
|
|
133
154
|
Гард судит один ход: следующий заход не отбивается."
|
|
134
155
|
|
|
@@ -1,5 +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
|
+
# rt-hook: PreToolUse Edit|Write|MultiEdit|NotebookEdit|Bash|mcp__webstorm__create_new_file|mcp__webstorm__execute_terminal_command|mcp__webstorm__execute_tool
|
|
3
3
|
# Требует: hooks/profile-check.sh, hooks/deny-tail.sh
|
|
4
4
|
# Гард пары «правка и её документ». PreToolUse.
|
|
5
5
|
#
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# rt-hook: PreToolUse Edit|Write|MultiEdit|mcp__webstorm__create_new_file|Bash
|
|
3
|
-
# Требует: agents/strict-teacher.md, hooks/roles.sh, hooks/deny-tail.sh
|
|
2
|
+
# rt-hook: PreToolUse Edit|Write|MultiEdit|mcp__webstorm__create_new_file|Bash|mcp__webstorm__execute_terminal_command
|
|
3
|
+
# Требует: agents/strict-teacher.md, hooks/roles.sh, hooks/deny-tail.sh, hooks/write-targets.sh
|
|
4
4
|
# Гард экзамена: правка не идёт, пока за сессию не сдан экзамен по загруженным правилам.
|
|
5
5
|
#
|
|
6
6
|
# Зачем именно так. Гейт правил требует загрузить правило перед правкой и на этом кончается:
|
|
@@ -15,6 +15,21 @@
|
|
|
15
15
|
# между вопросами и ответами не должно быть чтения тех же правил. Признак грубый и своей границы
|
|
16
16
|
# не скрывает: чтение соседнего правила он засчитает списыванием тоже.
|
|
17
17
|
#
|
|
18
|
+
# Вердикт ищется во всех формах записи хода, а не в одной. Форму доставки выбирает хост: роль,
|
|
19
|
+
# работающая фоном, отдаёт результат уведомлением о завершении, и записи вида «ответ инструмента»
|
|
20
|
+
# у неё нет вовсе. Дерево, где роль так и работает, гард запер целиком — пять кругов экзамена с
|
|
21
|
+
# полным вердиктом не отпустили ни одной правки; разбор — в описаниях происшествий.
|
|
22
|
+
#
|
|
23
|
+
# Отброшены при этом две формы, и обе намеренно. Свой текст помощника вердиктом не бывает:
|
|
24
|
+
# написать нужную строку в ответе стоит одного движения. Ответы инструментов чтения и записи —
|
|
25
|
+
# тоже: печать той же строки эхом или чтение файла с нею проходили бы гард, то есть единственным
|
|
26
|
+
# достижимым способом стала бы подделка. Засчитывается ответ инструмента, который читать и
|
|
27
|
+
# писать файлы не умеет, — им роль и запускают.
|
|
28
|
+
#
|
|
29
|
+
# Из-под гарда выведены настройка дерева, её надстройки и передача захода: настройка, которой
|
|
30
|
+
# гард выключается, этим гардом не запирается — иначе выхода из отказа нет вовсе. Отправка груза
|
|
31
|
+
# в приём заперта была той же дырой: адрес приёма живёт в той же настройке.
|
|
32
|
+
#
|
|
18
33
|
# FAIL-OPEN: нет jq, нет записи хода, чужой инструмент → пропуск. Сломанный гард не должен
|
|
19
34
|
# мешать работать.
|
|
20
35
|
|
|
@@ -34,16 +49,46 @@ rt_hooks_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
34
49
|
[ -f "$rt_hooks_dir/roles.sh" ] && . "$rt_hooks_dir/roles.sh" 2>/dev/null
|
|
35
50
|
command -v rt_role_off >/dev/null 2>&1 && rt_role_off strict-teacher && exit 0
|
|
36
51
|
|
|
52
|
+
# Цели записи разбирает общий помощник — тот же, которым их разбирает гард места правки.
|
|
53
|
+
# shellcheck disable=SC1090
|
|
54
|
+
[ -f "$rt_hooks_dir/write-targets.sh" ] && . "$rt_hooks_dir/write-targets.sh" 2>/dev/null
|
|
55
|
+
command -v rt_write_targets >/dev/null 2>&1 || rt_write_targets() { cat >/dev/null; }
|
|
56
|
+
|
|
57
|
+
# Пути, которые гард не судит: настройка дерева, её надстройки и каталог передачи захода.
|
|
58
|
+
# Печатает «да», если все названные цели выведены из-под гарда.
|
|
59
|
+
rt_exam_free_paths() {
|
|
60
|
+
free=1
|
|
61
|
+
while IFS= read -r target; do
|
|
62
|
+
[ -z "$target" ] && continue
|
|
63
|
+
case "$target" in
|
|
64
|
+
*.claude/rt-kit.json | *.claude/rt-kit/* | *.claude/handoff/*) ;;
|
|
65
|
+
*) free=0 ;;
|
|
66
|
+
esac
|
|
67
|
+
done
|
|
68
|
+
[ "$free" = "1" ] && printf 'да'
|
|
69
|
+
}
|
|
70
|
+
|
|
37
71
|
tool="$(rt_hook_tool)"
|
|
38
72
|
# Второй экзамен спрашивается на снятии черновика: работа кончилась, и правила поставки к этому
|
|
39
73
|
# моменту читались давно — между их чтением и этой минутой прошёл весь заход.
|
|
40
74
|
ready=0
|
|
41
75
|
case "$tool" in
|
|
42
|
-
Edit | Write | MultiEdit | mcp__webstorm__create_new_file)
|
|
43
|
-
|
|
76
|
+
Edit | Write | MultiEdit | mcp__webstorm__create_new_file)
|
|
77
|
+
target="$(printf '%s' "$input" | jq -r '.tool_input.file_path // .tool_input.pathInProject // empty' 2>/dev/null)"
|
|
78
|
+
[ -n "$target" ] && [ "$(printf '%s\n' "$target" | rt_exam_free_paths)" = "да" ] && exit 0
|
|
79
|
+
;;
|
|
80
|
+
Bash | mcp__webstorm__execute_terminal_command)
|
|
44
81
|
cmd="$(rt_hook_cmd)"
|
|
45
|
-
printf '%s' "$cmd" | grep -qE 'pr[[:space:]]+ready|mr[[:space:]]+update[^|;&]*--ready'
|
|
46
|
-
|
|
82
|
+
if printf '%s' "$cmd" | grep -qE 'pr[[:space:]]+ready|mr[[:space:]]+update[^|;&]*--ready'; then
|
|
83
|
+
ready=1
|
|
84
|
+
else
|
|
85
|
+
# Запись файла вызовом оболочки судится наравне с правкой: закрытый честный путь при
|
|
86
|
+
# открытом обходном означает, что гард держит того, кто правилам следует, и пропускает
|
|
87
|
+
# того, кто их обходит.
|
|
88
|
+
targets="$(printf '%s' "$cmd" | rt_write_targets)"
|
|
89
|
+
[ -z "$targets" ] && exit 0
|
|
90
|
+
[ "$(printf '%s\n' "$targets" | rt_exam_free_paths)" = "да" ] && exit 0
|
|
91
|
+
fi
|
|
47
92
|
;;
|
|
48
93
|
*) exit 0 ;;
|
|
49
94
|
esac
|
|
@@ -70,11 +115,39 @@ deny() {
|
|
|
70
115
|
|
|
71
116
|
# Судится вся сессия, а не последний ход: экзамен сдаётся один раз на старте и держится до конца.
|
|
72
117
|
verdict="$(jq -s -r '
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
118
|
+
def textof:
|
|
119
|
+
if type == "string" then .
|
|
120
|
+
elif type == "array" then (map(if type == "object" then (.text // "") else tostring end) | join("\n"))
|
|
121
|
+
else tostring end;
|
|
122
|
+
|
|
123
|
+
# Инструменты, читающие и пишущие файлы: их ответ вердиктом не считается — иначе печать той
|
|
124
|
+
# же строки эхом и чтение файла с нею проходят гард, а настоящий вердикт не проходит.
|
|
125
|
+
["Bash", "Read", "Grep", "Glob", "Edit", "Write", "MultiEdit", "NotebookEdit"] as $mute
|
|
126
|
+
| [.[] | select(.type == "assistant") | (.message.content // [])[]
|
|
127
|
+
| select(.type == "tool_use") | select(.name as $n | $mute | index($n) != null) | (.id // "")] as $muted
|
|
128
|
+
|
|
129
|
+
| [ .[]
|
|
130
|
+
# Свой текст помощника вердиктом не бывает: написать нужную строку в ответе стоит одного
|
|
131
|
+
# движения.
|
|
132
|
+
| if .type == "assistant" then ""
|
|
133
|
+
elif .type == "user" then
|
|
134
|
+
([ ((.message.content // []) | if type == "array" then .[] else empty end
|
|
135
|
+
| select(.type == "tool_result")
|
|
136
|
+
| select(((.tool_use_id // "") as $i | $muted | index($i)) == null)
|
|
137
|
+
| .content | textof),
|
|
138
|
+
((.message.content // "") | if type == "string" then . else "" end),
|
|
139
|
+
# Поле результата вызова: та же запись, другая форма. Отбрасывается, только
|
|
140
|
+
# если этот результат принадлежит инструменту чтения или записи.
|
|
141
|
+
(. as $rec
|
|
142
|
+
| if ($rec.toolUseResult // null) == null then ""
|
|
143
|
+
elif ([($rec.message.content // []) | if type == "array" then .[] else empty end
|
|
144
|
+
| select(.type == "tool_result") | (.tool_use_id // "")]
|
|
145
|
+
| map($muted | index(.)) | any(. != null)) then ""
|
|
146
|
+
else ($rec.toolUseResult | textof) end)
|
|
147
|
+
] | join("\n"))
|
|
148
|
+
# Записи хоста — уведомление о завершении роли и вложение: форму их выбирает хост, и
|
|
149
|
+
# засчитываются они целиком.
|
|
150
|
+
else tostring end ] | join("\n")
|
|
78
151
|
| [scan("ЭКЗАМЕН:[[:space:]]*сдано[[:space:]]*([0-9]+)[[:space:]]*из[[:space:]]*([0-9]+)")]
|
|
79
152
|
| if length == 0 then "нет"
|
|
80
153
|
else (.[-1] | if .[0] == .[1] then "сдан" else "провален" end)
|
|
@@ -120,6 +193,6 @@ case "$verdict" in
|
|
|
120
193
|
deny "BLOCKED by exam-guard: экзамен по загруженным правилам провален. Перечитай правило целиком — не тот кусок, о котором спрашивали, — и позови роль strict-teacher снова. Показанный ответ даёт знание одной строки, а не правила."
|
|
121
194
|
;;
|
|
122
195
|
*)
|
|
123
|
-
deny "BLOCKED by exam-guard: за эту сессию экзамена по загруженным правилам не было. Позови роль strict-teacher, передай ей список загруженных правил, ответь на её вопросы по памяти и верни ей ответы — вердикт она отдаёт строкой «ЭКЗАМЕН: сдано N из 5». Загруженное правило и прочитанное правило — разные вещи, и цену этой разницы платит владелец."
|
|
196
|
+
deny "BLOCKED by exam-guard: за эту сессию экзамена по загруженным правилам не было. Позови роль strict-teacher, передай ей список загруженных правил, ответь на её вопросы по памяти и верни ей ответы — вердикт она отдаёт строкой «ЭКЗАМЕН: сдано N из 5». Засчитывается он из ответа роли в любой форме, какой его доставил хост, но не из вывода оболочки и не из твоего же текста: печать этой строки эхом гард не отпускает. Роль уже звали и вердикт получен — значит, он пришёл формой, которой гард не видит: это дефект гарда, и правка `.claude/rt-kit.json` из-под него выведена. Загруженное правило и прочитанное правило — разные вещи, и цену этой разницы платит владелец."
|
|
124
197
|
;;
|
|
125
198
|
esac
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Конфликтующая своя заявка для гарда поставки: пока хоть одна отданная работа помечена
|
|
3
|
+
# конфликтующей, новая не берётся.
|
|
4
|
+
#
|
|
5
|
+
# Строки `# rt-hook:` здесь нет намеренно: событие и образец вызова объявляет сам гард, а
|
|
6
|
+
# помощник рядом хуком не регистрируется и в одиночку ничего не решает. Он зовётся из гарда,
|
|
7
|
+
# пользуется его переменными и его отказом — тем же приёмом, что помощники папки и подписи.
|
|
8
|
+
#
|
|
9
|
+
# Зачем именно так. Гард поставки стоит в трёх точках и в каждой требует того, что в эту минуту
|
|
10
|
+
# исправимо, а состояние уже отданного не спрашивает нигде. Конфликт приезжает в отданную заявку
|
|
11
|
+
# чужим слиянием, без единого действия её автора: влить её человек больше не может, и заявка
|
|
12
|
+
# стоит, пока автор не заметит. Заметить её нечем — требование перечитывать свои открытые заявки
|
|
13
|
+
# записано текстом и держится памятью: две заявки простояли конфликтующими весь заход, и заметил
|
|
14
|
+
# это владелец.
|
|
15
|
+
#
|
|
16
|
+
# Исправимо это ровно в минуту, когда берётся следующая работа: после неё конфликтов становится
|
|
17
|
+
# больше, а не меньше — каждая новая заявка растёт из той же главной ветки и приезжает в ту же
|
|
18
|
+
# очередь, которую придётся догонять.
|
|
19
|
+
#
|
|
20
|
+
# Работой здесь считаются четыре команды: заведение задачи, заведение ветки под задачу, перевод
|
|
21
|
+
# колонки в работу и открытие заявки. Всё, чем конфликт чинится, — подтягивание, вливание,
|
|
22
|
+
# коммит, отправка, переход между ветками — идёт как прежде: гард, отбивающий починку, запирал
|
|
23
|
+
# бы дерево наглухо.
|
|
24
|
+
#
|
|
25
|
+
# Судится только прямое «конфликтует». Пока хостинг считает сливаемость, он отвечает
|
|
26
|
+
# неопределённостью — и считает он её заново после каждой правки главной ветки, — а читать
|
|
27
|
+
# неопределённость как конфликт значило бы отбивать работу на каждой свежей вершине.
|
|
28
|
+
#
|
|
29
|
+
# ОТКАЗ В ПОЛЬЗУ РАБОТЫ: нет функции профиля, нет сети, нет машинной записи — ярус пропускается.
|
|
30
|
+
|
|
31
|
+
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
|
|
32
|
+
# Начало вызова помощник берёт оттуда же, откуда все гарды. Источается он и здесь, хотя зовущий
|
|
33
|
+
# гард это уже сделал: пустой образец превратил бы признак в поиск слова по всей строке.
|
|
34
|
+
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
|
|
35
|
+
|
|
36
|
+
rt_delivery_conflict() {
|
|
37
|
+
# Признак взятия работы читается из текста команды и стоит первым: опрос ходит в сеть, и
|
|
38
|
+
# платить за него на каждой команде оболочки нельзя.
|
|
39
|
+
taking=''
|
|
40
|
+
case "$cmd" in
|
|
41
|
+
*"${task_new}"*) taking='заведение задачи' ;;
|
|
42
|
+
esac
|
|
43
|
+
# Перевод колонки судится вместе с именем колонки работы: перевод в разбор и в закрытые —
|
|
44
|
+
# это конец работы, а не её начало.
|
|
45
|
+
if [ -z "$taking" ] && [ -n "$task_move" ]; then
|
|
46
|
+
case "$cmd" in
|
|
47
|
+
*"${task_move}"*)
|
|
48
|
+
case "$cmd" in
|
|
49
|
+
*"${RT_BOARD_INPROGRESS_KEY:-in-progress}"*) taking='перевод колонки в работу' ;;
|
|
50
|
+
esac
|
|
51
|
+
;;
|
|
52
|
+
esac
|
|
53
|
+
fi
|
|
54
|
+
if [ -z "$taking" ] \
|
|
55
|
+
&& printf '%s' "$cmd" | grep -qE '(^|[;&|[:space:]])git[[:space:]]+(checkout[[:space:]]+-b|switch[[:space:]]+-c)[[:space:]]+[A-Za-z]+-[0-9]+'; then
|
|
56
|
+
taking='заведение ветки под задачу'
|
|
57
|
+
fi
|
|
58
|
+
if [ -z "$taking" ] \
|
|
59
|
+
&& printf '%s' "$cmd" | grep -qE "${RT_CMD_BOUND}(gh[[:space:]]+pr[[:space:]]+create|glab[[:space:]]+mr[[:space:]]+create)([[:space:]]|\$)"; then
|
|
60
|
+
taking='открытие заявки'
|
|
61
|
+
fi
|
|
62
|
+
[ -z "$taking" ] && return 0
|
|
63
|
+
|
|
64
|
+
rt_needs rt_conflicting_pulls git-guard-delivery || return 0
|
|
65
|
+
stuck="$(cd "$root" && rt_conflicting_pulls 2>/dev/null)" || return 0
|
|
66
|
+
[ -z "$stuck" ] && return 0
|
|
67
|
+
|
|
68
|
+
listed=''
|
|
69
|
+
while IFS= read -r line; do
|
|
70
|
+
[ -z "$line" ] && continue
|
|
71
|
+
listed="${listed}${listed:+
|
|
72
|
+
}— ${line}"
|
|
73
|
+
done <<EOF
|
|
74
|
+
${stuck}
|
|
75
|
+
EOF
|
|
76
|
+
[ -z "$listed" ] && return 0
|
|
77
|
+
|
|
78
|
+
deny "BLOCKED: ${taking} при своей конфликтующей заявке. Отданное конфликтует с главной веткой, и влить его человек не может:
|
|
79
|
+
|
|
80
|
+
${listed}
|
|
81
|
+
|
|
82
|
+
Конфликт приезжает чужим слиянием, без единого действия автора заявки: она стоит, пока её не догонят. Новая работа его не чинит — она прибавляет к очереди ещё одну ветку, которая растёт из той же главной и отстанет так же.
|
|
83
|
+
|
|
84
|
+
Ход отсюда один: подтянуть главную, влить её в названную ветку, разобрать конфликт и отправить — и только потом брать новую работу. Что чем чинится — паттерн git-workflow-freshness."
|
|
85
|
+
}
|
|
@@ -24,9 +24,15 @@ rt_delivery_signature() {
|
|
|
24
24
|
# изнутри это выглядит верным — имя учётной записи в истории то самое. Так одиннадцать коммитов
|
|
25
25
|
# и уехали в главную ветку за чужой подписью; нашёл это владелец, читая историю глазами.
|
|
26
26
|
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
27
|
+
# Точек две — коммит и пуш, и судят они одно: подпись коммитов, уже лежащих во вкладе ветки.
|
|
28
|
+
# Текст самой команды при этом не разбирается ни там, ни там: почта задаётся её переменными, и
|
|
29
|
+
# разбор ловил бы ту же строку, которая и так перед глазами у набравшего её.
|
|
30
|
+
#
|
|
31
|
+
# Коммит добавлен потому, что промах делается на нём и до пуша успевает лечь в несколько коммитов
|
|
32
|
+
# подряд: каждый следующий берёт адрес у предыдущего. Отбитый на коммите промах чинится
|
|
33
|
+
# переписыванием последнего коммита тем же ходом; отбитый на пуше — переписыванием истории, а
|
|
34
|
+
# права на него у исполнителя может не быть вовсе. Первый коммит ветки при этом проходит: судить
|
|
35
|
+
# нечего, вклад пуст.
|
|
30
36
|
#
|
|
31
37
|
# Судится коммит, НАЗВАВШИЙСЯ машинной записью: её логин стоит именем автора либо левой частью
|
|
32
38
|
# почты. Опознавать по самой почте нельзя — она в таком коммите как раз и неверна, а требовать
|
|
@@ -45,9 +51,12 @@ rt_delivery_signature() {
|
|
|
45
51
|
# Пробный пуш не отправляет ничего, и подпись у него не спрашивают. Отложенная правка — тоже:
|
|
46
52
|
# `git stash push` кладёт правку в тайник этой же машины, поэтому тайник вырезается из строки, а
|
|
47
53
|
# признак считается по остатку.
|
|
54
|
+
# Коммит узнаётся тем же приёмом: слово `commit` отдельным словом при вызове git. Пробный
|
|
55
|
+
# коммит здесь не отличают — своего `--dry-run` он не знает.
|
|
48
56
|
if printf '%s' "$cmd" | grep -qE "${RT_CMD_BOUND}git([[:space:]]|\$)" \
|
|
49
|
-
&& printf '%s' "$cmd" | sed -E 's/git[[:space:]]+stash[[:space:]]+push/git stash/g' | grep -qE '(^|[[:space:]])push([[:space:]]|$)' \
|
|
50
|
-
|
|
57
|
+
&& { { printf '%s' "$cmd" | sed -E 's/git[[:space:]]+stash[[:space:]]+push/git stash/g' | grep -qE '(^|[[:space:]])push([[:space:]]|$)' \
|
|
58
|
+
&& ! printf '%s' "$cmd" | grep -q -- '--dry-run'; } \
|
|
59
|
+
|| printf '%s' "$cmd" | grep -qE '(^|[[:space:]])commit([[:space:]]|$)'; }; then
|
|
51
60
|
# Два условия молчания. Дерево, не назвавшее почты машинной записи, требования не получает:
|
|
52
61
|
# своей машинной записи у пакета нет, а выдуманная отбивала бы работу в чужом дереве.
|
|
53
62
|
# Вершины главной ветки нет — вклад считать не от чего.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# rt-hook: PreToolUse Bash|mcp__webstorm__execute_terminal_command|mcp__webstorm__execute_tool
|
|
3
|
-
# Требует: hooks/git-guard-delivery-folder.sh, hooks/profile-check.sh, hooks/deny-tail.sh
|
|
3
|
+
# Требует: hooks/git-guard-delivery-folder.sh, hooks/git-guard-delivery-conflict.sh, hooks/profile-check.sh, hooks/deny-tail.sh
|
|
4
4
|
# Гард поставки. PreToolUse на заведении ветки, пуше и открытии заявки на слияние.
|
|
5
5
|
#
|
|
6
6
|
# Закон о поставке требует трёх вещей, которых обычно не проверяет ничто: правка начинается с
|
|
@@ -163,6 +163,14 @@ fault() {
|
|
|
163
163
|
# shellcheck disable=SC1090
|
|
164
164
|
[ -f "$rt_hooks_dir/git-guard-delivery-folder.sh" ] && . "$rt_hooks_dir/git-guard-delivery-folder.sh" 2>/dev/null
|
|
165
165
|
|
|
166
|
+
# Конфликтующая своя заявка: тот же приём, что у папки и подписи, — свой предмет живёт
|
|
167
|
+
# помощником рядом. Зовётся он до всех ярусов ниже и судит не готовность этой работы, а право
|
|
168
|
+
# брать следующую: пока отданное конфликтует, его чинят первым действием хода. Нет помощника —
|
|
169
|
+
# ярус не судится, а работа идёт дальше.
|
|
170
|
+
# shellcheck disable=SC1090
|
|
171
|
+
[ -f "$rt_hooks_dir/git-guard-delivery-conflict.sh" ] && . "$rt_hooks_dir/git-guard-delivery-conflict.sh" 2>/dev/null
|
|
172
|
+
command -v rt_delivery_conflict >/dev/null 2>&1 && rt_delivery_conflict
|
|
173
|
+
|
|
166
174
|
# Отказ по накопленному. Пусто — вызывающая сторона идёт дальше.
|
|
167
175
|
deny_faults() {
|
|
168
176
|
[ -z "$faults" ] && return 0
|
|
@@ -380,7 +388,12 @@ fi
|
|
|
380
388
|
if [ -n "$title" ]; then
|
|
381
389
|
printf '%s' "$title" | grep -qE "$title_re" \
|
|
382
390
|
|| fault "заголовок заявки не начинается с номера задачи. В списке заявок тела не видно, а строка связи живёт именно там — без номера в заголовке PR с задачей не сопоставить."
|
|
383
|
-
|
|
391
|
+
# Номер вынимается из той части заголовка, которую признала сама форма, а не второй
|
|
392
|
+
# регуляркой рядом. Своя регулярка знает только пакетную форму: дерево, замостившее форму
|
|
393
|
+
# своей, получало пустой номер — и сверка номера заголовка с номером ветки молча не
|
|
394
|
+
# выполнялась вовсе, выглядя при этом сошедшейся.
|
|
395
|
+
title_matched="$(printf '%s' "$title" | grep -oE "$title_re" | head -1)"
|
|
396
|
+
title_number="$(printf '%s' "$title_matched" | grep -oE '[A-Za-z]+-[0-9]+' | head -1 | sed -E 's/^[A-Za-z]+-//')"
|
|
384
397
|
if [ -n "$number" ] && [ -n "$title_number" ]; then
|
|
385
398
|
[ "$title_number" = "$number" ] \
|
|
386
399
|
|| fault "в заголовке заявки номер ${title_number}, у ветки — ${number}. Задача, ветка и PR несут один и тот же номер."
|