@rt-tools/agent-kit 0.22.0 → 0.24.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 +8 -2
- package/assets/checks/board-long-work.github.mjs +101 -0
- package/assets/checks/board-runs.github.mjs +34 -0
- package/assets/checks/board.github.mjs +1 -1
- package/assets/checks/check-board.github.mjs +30 -2
- package/assets/checks/check-doc-paths.mjs +24 -5
- package/assets/checks/check-file-size.mjs +8 -2
- package/assets/checks/check-prose-style.mjs +10 -1
- package/assets/checks/check-reuse.mjs +4 -1
- package/assets/checks/check-schema-drift.mjs +65 -6
- package/assets/checks/rt-kit-checks.config.mjs +13 -0
- package/assets/checks/signals.mjs +41 -1
- package/assets/commands/next-session.md +16 -5
- package/assets/defaults/gate-map.sh +13 -0
- package/assets/defaults/project.sh +26 -0
- package/assets/defaults/shell.sh +18 -3
- package/assets/hooks/browser-guard-device-id.sh +42 -12
- package/assets/hooks/browser-guard-no-asking.sh +5 -1
- package/assets/hooks/dispatch.sh +40 -9
- package/assets/hooks/docs-guard.sh +10 -0
- package/assets/hooks/exam-guard.sh +66 -16
- package/assets/hooks/git-guard-delivery-draft.sh +78 -0
- package/assets/hooks/git-guard-delivery.sh +64 -122
- package/assets/hooks/git-guard-main.sh +39 -4
- package/assets/hooks/git-guard-push-tests.sh +71 -3
- package/assets/hooks/glossary-load.sh +23 -2
- package/assets/hooks/grill-gate.sh +62 -0
- package/assets/hooks/hook-input.sh +17 -6
- package/assets/hooks/rule-source-guard.sh +11 -0
- package/assets/hooks/stand-login-guard.sh +101 -0
- package/assets/hooks/write-targets.sh +37 -4
- package/assets/laws/autonomous-work.md +30 -0
- package/assets/laws/project-documentation.md +8 -0
- package/assets/laws/verifiability.md +12 -2
- package/assets/laws/work-conduct.md +59 -65
- package/assets/patterns/autonomous-work-run.md +105 -0
- package/assets/patterns/browser-verification-measure.md +41 -1
- package/assets/patterns/browser-verification-stand.md +57 -16
- package/assets/patterns/doc-style-human.md +75 -0
- package/assets/patterns/doc-style-write.md +16 -0
- package/assets/patterns/git-workflow-commit.azure.md +12 -0
- package/assets/patterns/git-workflow-commit.github.md +16 -3
- package/assets/patterns/git-workflow-commit.gitlab.md +12 -0
- package/assets/patterns/git-workflow-merge.md +8 -0
- package/assets/patterns/git-workflow-pr-ready.md +93 -0
- package/assets/patterns/git-workflow-pr.azure.md +1 -1
- package/assets/patterns/git-workflow-pr.github.md +1 -1
- package/assets/patterns/git-workflow-pr.gitlab.md +1 -1
- package/assets/patterns/task-flow-start.md +48 -48
- package/assets/patterns/testing-e2e.md +18 -8
- package/assets/patterns/ts-procedure.md +3 -2
- package/assets/pitfalls/task-flow.md +40 -40
- package/assets/rules/autonomous-work.md +92 -0
- package/assets/rules/browser-verification.md +35 -3
- package/assets/rules/deploy-flow.azure.md +7 -0
- package/assets/rules/deploy-flow.github.md +7 -0
- package/assets/rules/deploy-flow.gitlab.md +7 -0
- package/assets/rules/doc-style.md +54 -0
- package/assets/rules/git-workflow.azure.md +8 -0
- package/assets/rules/git-workflow.github.md +59 -63
- package/assets/rules/git-workflow.gitlab.md +8 -0
- package/assets/rules/reuse-first.md +25 -5
- package/assets/rules/styling-bem.md +8 -1
- package/assets/rules/task-flow.md +108 -109
- package/assets/rules/testing.md +21 -0
- package/assets/skills/agent-kit.md +72 -82
- package/lib/commands.d.ts.map +1 -1
- package/lib/commands.js +69 -2
- package/lib/commands.js.map +1 -1
- package/lib/enroll.d.ts.map +1 -1
- package/lib/enroll.js +1 -1
- package/lib/enroll.js.map +1 -1
- package/lib/observations.d.ts +10 -1
- package/lib/observations.d.ts.map +1 -1
- package/lib/observations.js +1 -0
- package/lib/observations.js.map +1 -1
- package/lib/override-marks.d.ts +24 -0
- package/lib/override-marks.d.ts.map +1 -0
- package/lib/override-marks.js +98 -0
- package/lib/override-marks.js.map +1 -0
- package/lib/push-gate.d.ts +14 -0
- package/lib/push-gate.d.ts.map +1 -0
- package/lib/push-gate.js +93 -0
- package/lib/push-gate.js.map +1 -0
- package/lib/shipment.d.ts.map +1 -1
- package/lib/shipment.js +1 -1
- package/lib/shipment.js.map +1 -1
- package/package.json +1 -1
- package/rt-tools-agent-kit-0.24.0.tgz +0 -0
- package/assets/laws/application/access.md +0 -34
- package/assets/laws/application/locales.md +0 -33
- package/assets/laws/application/search-visibility.md +0 -24
- package/assets/patterns/permissions-procedure.md +0 -71
- package/assets/patterns/seo-page.md +0 -104
- package/assets/patterns/seo-verify.md +0 -83
- package/assets/patterns/translations-content.md +0 -107
- package/assets/patterns/translations-key.md +0 -64
- package/assets/rules/permissions.md +0 -116
- package/assets/rules/seo.md +0 -139
- package/assets/rules/translations.md +0 -96
- package/rt-tools-agent-kit-0.22.0.tgz +0 -0
|
@@ -241,6 +241,21 @@ rt_task_branch_ok_default() {
|
|
|
241
241
|
| grep -qE '^([A-Z]+-)?[0-9]+-[a-z0-9][a-z0-9-]*$|^(feat|fix|refactor|docs|chore|style|perf|test|build|ci)/[0-9]+-[a-z0-9][a-z0-9-]*$'
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
+
# Номер задачи из имени ветки. Второй вопрос к тому же имени: форма отвечает «годится ли»,
|
|
245
|
+
# а это — «какой он», и свести их в одно нельзя: одна возвращает код, другой строку.
|
|
246
|
+
#
|
|
247
|
+
# Формы те же, что знает проверка формы выше: приставкой стоит либо ключ задач через дефис,
|
|
248
|
+
# либо род правки через косую черту, либо её нет вовсе. Разойдясь с ней, разбор оставит форму
|
|
249
|
+
# законной, а номер пустым — и сверка номера ветки с номером заголовка заявки пропустится молча,
|
|
250
|
+
# выглядя при этом сошедшейся.
|
|
251
|
+
#
|
|
252
|
+
# Хвост после номера здесь не судится намеренно: `RT-9_guest` — это промах в разделителе, а не
|
|
253
|
+
# осознанно беззадачная ветка, и номер из неё вынуть надо, чтобы форму имени было с чем сверить.
|
|
254
|
+
# Требование дефиса оставляло такое имя без номера, и гард пропускал его как ветку под пробу.
|
|
255
|
+
rt_task_branch_number_default() {
|
|
256
|
+
printf '%s' "$1" | sed -nE 's#^([A-Za-z]+[-/])?([0-9]+).*#\2#p'
|
|
257
|
+
}
|
|
258
|
+
|
|
244
259
|
# Адрес боевого хранилища: образец, по которому его узнают в командной строке — порт туннеля,
|
|
245
260
|
# хост, домен. Любая запись по нему отбивается совсем, и опт-аут не действует. Умолчание
|
|
246
261
|
# молчит: чужой адрес здесь опаснее неназванного — он отбивал бы работу с локальной базой.
|
|
@@ -273,6 +288,16 @@ RT_TASK_BOT="${RT_TASK_BOT:-}"
|
|
|
273
288
|
RT_PULL_TOKEN_VAR="${RT_PULL_TOKEN_VAR:-}"
|
|
274
289
|
RT_PULL_TOKEN_HINT="${RT_PULL_TOKEN_HINT:-}"
|
|
275
290
|
|
|
291
|
+
# Кто придёт к хостингу по этому токену. Подстановка в команде говорит только о намерении: она
|
|
292
|
+
# читает файл, а файла на машине может не быть — тогда значение пустое, клиент отвечает от
|
|
293
|
+
# залогиненной записи, и заявка выходит от владельца при верной с виду команде. Спросить это
|
|
294
|
+
# стоит одного вызова, но как спрашивать, знает только дерево: хостинг, клиент и путь к токену
|
|
295
|
+
# у каждого свои. Умолчание молчит: дерево, не объявившее функции, второго яруса не получает.
|
|
296
|
+
#
|
|
297
|
+
# Контракт: печатает логин, под которым уйдёт пишущий вызов. Пустой вывод означает «спросить не
|
|
298
|
+
# удалось» — гард пропускает вызов и сообщает об этом.
|
|
299
|
+
rt_pull_token_login() { :; }
|
|
300
|
+
|
|
276
301
|
# Раздел, который тело заявки обязано нести с минуты открытия: решение о слиянии принимается на
|
|
277
302
|
# её странице, где переписки нет вовсе, и сказанного вслух там не остаётся. Умолчание молчит —
|
|
278
303
|
# заголовок пишется языком заявки, а чужих слов пакет не знает: не названный деревом, раздел не
|
|
@@ -384,6 +409,7 @@ rt_push_docs_only() { rt_push_docs_only_default "$@"; }
|
|
|
384
409
|
rt_docs_pair_for() { rt_docs_pair_for_default "$@"; }
|
|
385
410
|
rt_lint_for() { rt_lint_for_default "$@"; }
|
|
386
411
|
rt_task_branch_ok() { rt_task_branch_ok_default "$@"; }
|
|
412
|
+
rt_task_branch_number() { rt_task_branch_number_default "$@"; }
|
|
387
413
|
rt_reinvented_in() { rt_reinvented_in_default "$@"; }
|
|
388
414
|
rt_is_app_code() { rt_is_app_code_default "$@"; }
|
|
389
415
|
# Каталог источников пакета правил в этом дереве, от корня. Пусто — дерево пакета не везёт, и
|
package/assets/defaults/shell.sh
CHANGED
|
@@ -41,10 +41,25 @@
|
|
|
41
41
|
# не латиницей, записью больше не считается — в дереве таких нет ни одного, а появятся, признак
|
|
42
42
|
# придётся расширить.
|
|
43
43
|
rt_shell_writes_default() {
|
|
44
|
-
printf '%s' "$1" \
|
|
45
|
-
| sed -E 's#(&|[0-9]*)>>?[[:space:]]*/dev/(null|stderr)##g; s#[0-9]*>&[0-9-]##g; s#[-=]+>##g'
|
|
44
|
+
cleaned="$(printf '%s' "$1" \
|
|
45
|
+
| sed -E 's#(&|[0-9]*)>>?[[:space:]]*/dev/(null|stderr)##g; s#[0-9]*>&[0-9-]##g; s#[-=]+>##g')"
|
|
46
|
+
|
|
47
|
+
# Интерпретатор пишет телом, а не именем файла, который запускает. Путь, стоящий у него
|
|
48
|
+
# первым доводом, — это то, что он читает: запуск проверки дерева по её пути записью не
|
|
49
|
+
# считается. Раньше проверялось само имя, и заход, запускавший проверку ради диагностики,
|
|
50
|
+
# получал требование правила общего кода, ничего в нём не правя; за две задачи таких отказов
|
|
51
|
+
# набралось около пятнадцати, и часть пришлась на команды, не писавшие ничего.
|
|
52
|
+
#
|
|
53
|
+
# Тело у интерпретатора двух видов, и оба остаются записью: документ на входе и код доводом.
|
|
54
|
+
interp='(^|[|;&(]|[[:space:]])(python3?|node|ruby|deno|bun|php|perl)'
|
|
55
|
+
if printf '%s' "$cleaned" | grep -Eq \
|
|
56
|
+
"${interp}([[:space:]][^|]*)?<<|${interp}([[:space:]]+-[^[:space:]]*)*[[:space:]]+(-e|--eval|-c|-p|--print)([[:space:]]|\$)"; then
|
|
57
|
+
return 0
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
printf '%s' "$cleaned" \
|
|
46
61
|
| grep -Eq \
|
|
47
|
-
'>>?[[:space:]]*[A-Za-z0-9_./~$"'"'"'-]|\btee\b|\bsed\b[^|]*-i|\bperl\b[^|]*-i|\
|
|
62
|
+
'>>?[[:space:]]*[A-Za-z0-9_./~$"'"'"'-]|\btee\b|\bsed\b[^|]*-i|\bperl\b[^|]*-i|\bdd\b[^|]*of=|\bcp\b|\bmv\b|\brm\b|\btouch\b|\btruncate\b|\binstall\b|\bpatch\b|\bgit[[:space:]]+(checkout|restore|apply|stash)\b'
|
|
48
63
|
}
|
|
49
64
|
|
|
50
65
|
# Пути, названные командой оболочки. Печатает по одному в строке; судит их зовущий.
|
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# rt-hook: PreToolUse mcp__claude-in-chrome__select_browser
|
|
3
|
+
# rt-hook: PostToolUse mcp__claude-in-chrome__select_browser
|
|
3
4
|
# Требует: hooks/deny-tail.sh
|
|
4
|
-
# Гард выбора браузера.
|
|
5
|
+
# Гард выбора браузера. Работает на двух событиях: до вызова и после него.
|
|
5
6
|
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
7
|
+
# До вызова запрещает любой профиль, кроме закреплённого. Чужой профиль ведёт в браузер, где нет
|
|
8
|
+
# сессий этого проекта.
|
|
8
9
|
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
10
|
+
# После вызова ставит метку сессии, если ответ подтверждает подключение. Гард свежести читает
|
|
11
|
+
# возраст этой метки и по нему разрешает дальнейшую работу с браузером.
|
|
11
12
|
#
|
|
12
|
-
#
|
|
13
|
+
# Почему метка ставится после вызова. Раньше она ставилась до вызова, при совпадении признака,
|
|
14
|
+
# то есть на попытке выбора. Если закреплённый профиль отключён, вызов возвращает отказ, а метка
|
|
15
|
+
# уже есть — и гард свежести пропускает состав вкладок, переход и снимок экрана в тот браузер,
|
|
16
|
+
# который расширение считает активным. Ошибку заметил владелец, а не гард. Метка до вызова
|
|
17
|
+
# подтверждает только запрос профиля, а не подключение к нему.
|
|
18
|
+
#
|
|
19
|
+
# При ошибке гард пропускает: помощник не назвал профиль — вызов разрешён. Ответ без признаков
|
|
20
|
+
# подключения метку не ставит; работа не останавливается — следующий вызов запретит гард
|
|
21
|
+
# свежести и потребует выбрать профиль заново.
|
|
13
22
|
|
|
14
|
-
#
|
|
23
|
+
# Имя гарда для наблюдений: его пишет общий хвост отказа.
|
|
15
24
|
RT_GUARD_NAME=browser-guard-device-id
|
|
16
25
|
|
|
17
26
|
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
|
|
@@ -20,23 +29,44 @@ RT_GUARD_NAME=browser-guard-device-id
|
|
|
20
29
|
rt_hook_read
|
|
21
30
|
input="$RT_HOOK_INPUT"
|
|
22
31
|
|
|
23
|
-
#
|
|
24
|
-
# приходит
|
|
32
|
+
# Идентификатор сессии передаётся помощнику: без него сообщение о ненастроенном дереве
|
|
33
|
+
# помечается датой и приходит раз в сутки, а не раз за заход.
|
|
25
34
|
sid="$(printf '%s' "$input" | jq -r '.session_id // "nosession"' 2>/dev/null)"
|
|
26
35
|
|
|
27
36
|
device_id="$("${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/browser-device-id.sh" "$sid")"
|
|
28
37
|
[ -z "$device_id" ] && exit 0
|
|
29
38
|
|
|
30
39
|
requested="$(printf '%s' "$input" | jq -r '.tool_input.deviceId // empty' 2>/dev/null)"
|
|
40
|
+
event="$(printf '%s' "$input" | jq -r '.hook_event_name // empty' 2>/dev/null)"
|
|
41
|
+
|
|
42
|
+
if [ "$event" = "PostToolUse" ]; then
|
|
43
|
+
# Ответ вызова приводится к строке: он приходит объектом, строкой или списком блоков, и
|
|
44
|
+
# разбирать каждую форму отдельно дорого. Проверяется текст — в нём есть и признак профиля,
|
|
45
|
+
# и слово об отказе.
|
|
46
|
+
answer="$(printf '%s' "$input" | jq -r '.tool_response // empty | if type == "string" then . else tojson end' 2>/dev/null)"
|
|
47
|
+
|
|
48
|
+
# Отказ выбора: подключения не было, метка не ставится. Слова отказа перечислены на двух
|
|
49
|
+
# языках: помощник отвечает на своём, дерево выводит текст на русском.
|
|
50
|
+
case "$answer" in
|
|
51
|
+
*'"error"'* | *'"isError":true'* | *"failed"* | *"not found"* | *"not connected"* | *"не найден"* | *"отключ"*)
|
|
52
|
+
exit 0
|
|
53
|
+
;;
|
|
54
|
+
esac
|
|
55
|
+
|
|
56
|
+
# Пустой ответ подключением не считается: по нему ничего не проверить, а метка утверждала
|
|
57
|
+
# бы, что профиль подключён.
|
|
58
|
+
[ -z "$answer" ] && exit 0
|
|
31
59
|
|
|
32
|
-
if [ "$requested" = "$device_id" ]; then
|
|
33
60
|
marker_dir="${TMPDIR:-/tmp}/claude-browser-guard"
|
|
34
61
|
mkdir -p "$marker_dir" 2>/dev/null && : >"$marker_dir/${sid}" 2>/dev/null
|
|
35
62
|
exit 0
|
|
36
63
|
fi
|
|
37
64
|
|
|
38
|
-
#
|
|
39
|
-
|
|
65
|
+
# До вызова проверяется только признак профиля; метка здесь не ставится.
|
|
66
|
+
[ "$requested" = "$device_id" ] && exit 0
|
|
67
|
+
|
|
68
|
+
# Общий хвост отказа: два допустимых шага и допустимая форма обхода, если она есть. Файл может
|
|
69
|
+
# быть не разложен — тогда хвоста нет, причина отказа остаётся.
|
|
40
70
|
# shellcheck disable=SC1090
|
|
41
71
|
[ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" ] \
|
|
42
72
|
&& . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" 2>/dev/null
|
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
# being about which Chrome profile to automate. Anchor on selection phrasing instead
|
|
13
13
|
# (deviceId, "which/what browser", "какой браузер").
|
|
14
14
|
#
|
|
15
|
+
# The Russian pronoun is spelled out case by case on purpose: an optional ending would leave a
|
|
16
|
+
# bare "как" in the pattern, and "какие браузеры поддерживаем" — a support question, not a
|
|
17
|
+
# selection one — got denied by exactly that.
|
|
18
|
+
#
|
|
15
19
|
# FAIL-OPEN when browser-device-id.sh prints nothing.
|
|
16
20
|
|
|
17
21
|
# Своё имя в наблюдениях: отбой пишет общий хвост отказа, а не сам гард.
|
|
@@ -29,7 +33,7 @@ device_id="$("${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/browser-device-id.sh" 2>/de
|
|
|
29
33
|
questions="$(printf '%s' "$input" | jq -r '[.tool_input.questions[]? | .question, .header, (.options[]?.label)] | join(" ")' 2>/dev/null)"
|
|
30
34
|
[ -z "$questions" ] && exit 0
|
|
31
35
|
|
|
32
|
-
printf '%s' "$questions" | grep -qiE 'deviceid|device id|(which|what|pick|choose|select)[^.]{0,25}\bbrowser\b|\bbrowser\b[^.]{0,25}(profile|to use)|как(
|
|
36
|
+
printf '%s' "$questions" | grep -qiE 'deviceid|device id|(which|what|pick|choose|select)[^.]{0,25}\bbrowser\b|\bbrowser\b[^.]{0,25}(profile|to use)|как(ой|ую|им|ого)[^.]{0,15}браузер|брауз[а-я]*[^.]{0,20}(использовать|выбрать|выбор|нужен|запустить)' || exit 0
|
|
33
37
|
|
|
34
38
|
# Общий хвост отказа: два законных хода и законная форма обхода, если она у отказа есть. Файл
|
|
35
39
|
# может быть не разложен — тогда хвоста нет, а причина отказа остаётся прежней.
|
package/assets/hooks/dispatch.sh
CHANGED
|
@@ -33,10 +33,10 @@ input="$(cat 2>/dev/null)"
|
|
|
33
33
|
# Разбор один на все ветки: четыре поля одним вызовом разборщика вместо шести на каждый гард.
|
|
34
34
|
# Значения приходят уже закавыченными для оболочки — `@sh` в разборщике для того и сделан:
|
|
35
35
|
# командная строка держит и кавычки, и переводы строк, и подставить её иначе нельзя.
|
|
36
|
-
assignments="$(printf '%s' "$input" | jq -r '@sh "RT_HOOK_TOOL=\(.tool_name // "") RT_HOOK_CMD=\(.tool_input.command // "") RT_HOOK_FILE=\(.tool_input.file_path // "") RT_HOOK_CWD=\(.cwd // "")"' 2>/dev/null)"
|
|
36
|
+
assignments="$(printf '%s' "$input" | jq -r '@sh "RT_HOOK_TOOL=\(.tool_name // "") RT_HOOK_CMD=\(.tool_input.command // "") RT_HOOK_FILE=\(.tool_input.file_path // "") RT_HOOK_CWD=\(.cwd // "") RT_HOOK_SOURCE=\(.source // "")"' 2>/dev/null)"
|
|
37
37
|
if [ -n "$assignments" ]; then
|
|
38
38
|
eval "$assignments" 2>/dev/null || true
|
|
39
|
-
export RT_HOOK_TOOL RT_HOOK_CMD RT_HOOK_FILE RT_HOOK_CWD
|
|
39
|
+
export RT_HOOK_TOOL RT_HOOK_CMD RT_HOOK_FILE RT_HOOK_CWD RT_HOOK_SOURCE
|
|
40
40
|
# Признак разбора: по нему ветки отличают готовое поле от пустой переменной, случайно
|
|
41
41
|
# оказавшейся в окружении прогона. Без него пустое значение читается как «поля нет».
|
|
42
42
|
export RT_HOOK_PARSED=1
|
|
@@ -58,13 +58,21 @@ for branch in $branches; do
|
|
|
58
58
|
branch_event="${declaration%% *}"
|
|
59
59
|
[ "$branch_event" = "$event" ] || continue
|
|
60
60
|
|
|
61
|
-
# Образец вызова: его нет вовсе — гард зовётся на любом; есть — сверяется
|
|
62
|
-
#
|
|
63
|
-
#
|
|
61
|
+
# Образец вызова: его нет вовсе — гард зовётся на любом; есть — сверяется целиком, а
|
|
62
|
+
# не куском. Звёздочка и точка со звёздочкой значат одно: любой вызов.
|
|
63
|
+
#
|
|
64
|
+
# Предмет сверки зависит от события. У вызова инструмента это имя инструмента; у входа в
|
|
65
|
+
# сессию имени инструмента нет, и образец там называет род запуска — `startup`,
|
|
66
|
+
# `resume`, `compact`, `clear`. Сверка с пустым именем не совпадала ни разу, и через
|
|
67
|
+
# диспетчер не вызывался ни один хук входа: заход начинался без свода законов, без
|
|
68
|
+
# словаря, без состояния работы и без передачи прошлого захода — с нулевым кодом и
|
|
69
|
+
# пустым выводом.
|
|
64
70
|
matcher="${declaration#"$branch_event"}"
|
|
65
71
|
matcher="${matcher#"${matcher%%[![:space:]]*}"}"
|
|
72
|
+
subject="${RT_HOOK_TOOL:-}"
|
|
73
|
+
[ -z "$subject" ] && subject="${RT_HOOK_SOURCE:-}"
|
|
66
74
|
if [ -n "$matcher" ] && [ "$matcher" != '*' ] && [ "$matcher" != '.*' ]; then
|
|
67
|
-
[[ "$
|
|
75
|
+
[[ "$subject" =~ ^(${matcher})$ ]] || continue
|
|
68
76
|
fi
|
|
69
77
|
|
|
70
78
|
matched=1
|
|
@@ -77,10 +85,33 @@ EOF
|
|
|
77
85
|
# одном файле звали бы гард дважды на один ввод, и второй вызов судил бы то же самое.
|
|
78
86
|
[ "$matched" = 1 ] || continue
|
|
79
87
|
|
|
80
|
-
|
|
81
|
-
|
|
88
|
+
# Поток ошибок ветки собирается отдельно, а не отбрасывается: на удачном ходу он шум и
|
|
89
|
+
# наружу не идёт, а на отказе он и есть причина. Гард, печатающий свой отказ туда, приходил к
|
|
90
|
+
# исполнителю строкой о сломанном файле — при целом файле и понятном тексте, которого никто
|
|
91
|
+
# не видел. За один заход так пропало два отказа подряд.
|
|
92
|
+
branch_err="$(mktemp 2>/dev/null)"
|
|
93
|
+
if [ -n "$branch_err" ]; then
|
|
94
|
+
branch_out="$(printf '%s' "$input" | bash "$branch" 2>"$branch_err")"
|
|
95
|
+
code=$?
|
|
96
|
+
said_err="$(cat "$branch_err" 2>/dev/null)"
|
|
97
|
+
rm -f "$branch_err" 2>/dev/null
|
|
98
|
+
else
|
|
99
|
+
branch_out="$(printf '%s' "$input" | bash "$branch" 2>/dev/null)"
|
|
100
|
+
code=$?
|
|
101
|
+
said_err=""
|
|
102
|
+
fi
|
|
82
103
|
if [ "$code" -ne 0 ]; then
|
|
83
|
-
[ -n "$branch_out" ]
|
|
104
|
+
if [ -n "$branch_out" ]; then
|
|
105
|
+
printf '%s\n' "$branch_out"
|
|
106
|
+
elif [ -n "$said_err" ]; then
|
|
107
|
+
printf '%s\n' "$said_err"
|
|
108
|
+
else
|
|
109
|
+
# Ветка вышла ненулём и не сказала ничего ни одним потоком. Снаружи это неотличимо от
|
|
110
|
+
# отказа по делу, а починить нечего: какой файл сломан, не знает никто. Имя диспетчер
|
|
111
|
+
# поэтому называет сам, иначе о сломанной ветке не говорит ничто.
|
|
112
|
+
printf 'Гард %s вышел с кодом %s и ничего не напечатал: похоже, файл сломан.\n' \
|
|
113
|
+
"$(basename "$branch")" "$code"
|
|
114
|
+
fi
|
|
84
115
|
exit "$code"
|
|
85
116
|
fi
|
|
86
117
|
|
|
@@ -215,6 +215,16 @@ done
|
|
|
215
215
|
if rt_needs rt_docs_pair_for docs-guard; then
|
|
216
216
|
while IFS= read -r file; do
|
|
217
217
|
[ -z "$file" ] && continue
|
|
218
|
+
|
|
219
|
+
# Файл, положенный раскладкой, пары не требует: автор у него в дереве-потребителе один —
|
|
220
|
+
# пакет, и документ о нём лежит там же. Иначе первая же раскладка требует обход на весь
|
|
221
|
+
# свой объём, а обход, объявленный на сотню файлов, снимает требование и с будущих правок
|
|
222
|
+
# этих файлов вручную. Признак — шапка раскладки: она стоит в каждом разложенном файле и
|
|
223
|
+
# отличает его надёжнее любого перечня путей.
|
|
224
|
+
if [ -f "$file" ] && head -12 "$file" 2>/dev/null | grep -qE 'rt-kit v[^ ]+ · [^ ]+ · [0-9a-f]+'; then
|
|
225
|
+
continue
|
|
226
|
+
fi
|
|
227
|
+
|
|
218
228
|
want="$(rt_docs_pair_for "$file" 2>/dev/null)"
|
|
219
229
|
[ -z "$want" ] && continue
|
|
220
230
|
# Пара считается приехавшей, если хоть один файл коммита подходит под образец.
|
|
@@ -82,7 +82,10 @@ case "$tool" in
|
|
|
82
82
|
;;
|
|
83
83
|
Bash | mcp__webstorm__execute_terminal_command)
|
|
84
84
|
cmd="$(rt_hook_cmd)"
|
|
85
|
-
|
|
85
|
+
# Снятием черновика считается вызов клиента, а не вхождение слов: команда, которая только
|
|
86
|
+
# пишет о снятии — строка в файле предложений, тело коммита, разбор происшествия, —
|
|
87
|
+
# проверялась наравне с самим снятием, и отказ приходил на попытку описать этот дефект.
|
|
88
|
+
if printf '%s' "$cmd" | grep -qE "${RT_CMD_BOUND}(gh[[:space:]]+pr[[:space:]]+ready|glab[[:space:]]+mr[[:space:]]+update[^|;&]*--ready)([[:space:]]|\$)"; then
|
|
86
89
|
ready=1
|
|
87
90
|
else
|
|
88
91
|
# Запись файла вызовом оболочки судится наравне с правкой: закрытый честный путь при
|
|
@@ -107,7 +110,27 @@ transcript="$(printf '%s' "$input" | jq -r '.transcript_path // empty' 2>/dev/nu
|
|
|
107
110
|
&& . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" 2>/dev/null
|
|
108
111
|
command -v rt_deny_tail >/dev/null 2>&1 || rt_deny_tail() { :; }
|
|
109
112
|
|
|
113
|
+
# Второй выход у отказа — не требующий снимать защиту.
|
|
114
|
+
#
|
|
115
|
+
# Единственным выходом гард называл список выключенных ролей в настройке дерева. Среда, где
|
|
116
|
+
# работает исполнитель, правку такого списка запрещает своим механизмом, о котором гард не знает:
|
|
117
|
+
# одно правило говорит «выйди отсюда», второе — «этим путём нельзя», и работа стоит при зелёном
|
|
118
|
+
# наборе и сказанном слове владельца.
|
|
119
|
+
#
|
|
120
|
+
# Обход объявляется строкой `Exam-skip: <причина>` в теле последнего коммита ветки: она остаётся
|
|
121
|
+
# в истории и видна владельцу на странице заявки. Причина обязательна — подстановка вместо неё
|
|
122
|
+
# обходом не считается, как и у гарда документов.
|
|
123
|
+
rt_exam_declared_skip() {
|
|
124
|
+
git -C "${CLAUDE_PROJECT_DIR:-.}" log -1 --format=%B 2>/dev/null \
|
|
125
|
+
| grep -qE '^Exam-skip:[[:space:]]*[^[:space:]<]'
|
|
126
|
+
}
|
|
127
|
+
|
|
110
128
|
deny() {
|
|
129
|
+
if rt_exam_declared_skip; then
|
|
130
|
+
printf 'гард экзамена: обход объявлен в теле последнего коммита строкой Exam-skip. Вызов пропущен, запись осталась в истории.\n' >&2
|
|
131
|
+
exit 0
|
|
132
|
+
fi
|
|
133
|
+
|
|
111
134
|
reason="$1"
|
|
112
135
|
tail_text="$(rt_deny_tail "$2")"
|
|
113
136
|
[ -n "$tail_text" ] && reason="$1 ${tail_text}"
|
|
@@ -136,7 +159,7 @@ verdict="$(jq -s -r '
|
|
|
136
159
|
elif .type == "user" then
|
|
137
160
|
([ ((.message.content // []) | if type == "array" then .[] else empty end
|
|
138
161
|
| select(.type == "tool_result")
|
|
139
|
-
| select((
|
|
162
|
+
| select((.tool_use_id // "") | if . == "" then true else ($muted | index(.)) == null end)
|
|
140
163
|
| .content | textof),
|
|
141
164
|
((.message.content // "") | if type == "string" then . else "" end),
|
|
142
165
|
# Поле результата вызова: та же запись, другая форма. Отбрасывается, только
|
|
@@ -163,19 +186,46 @@ if [ "$ready" = "1" ]; then
|
|
|
163
186
|
# Записи сводятся в один поток в порядке их появления: команда и ответ инструмента лежат в
|
|
164
187
|
# разных полях, и индекс из одного массива в другом не значит ничего.
|
|
165
188
|
after="$(jq -s -r '
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
189
|
+
def textof:
|
|
190
|
+
if type == "string" then .
|
|
191
|
+
elif type == "array" then (map(if type == "object" then (.text // "") else tostring end) | join("\n"))
|
|
192
|
+
else tostring end;
|
|
193
|
+
|
|
194
|
+
# Тот же набор форм, что у широкой выборки: вердикт приходит в той форме, какую выбрал
|
|
195
|
+
# хост, и роль, работающая фоном, отдаёт его уведомлением о завершении — записи вида
|
|
196
|
+
# «ответ инструмента» у неё нет. Раньше эта выборка читала только команды помощника и
|
|
197
|
+
# ответы инструментов, и второй экзамен в таком дереве не засчитывался: пять кругов с
|
|
198
|
+
# полным вердиктом не пропустили ни одной правки.
|
|
199
|
+
["Bash", "Read", "Grep", "Glob", "Edit", "Write", "MultiEdit", "NotebookEdit"] as $mute
|
|
200
|
+
| [.[] | select(.type == "assistant") | (.message.content // [])[]
|
|
201
|
+
| select(.type == "tool_use") | select(.name as $n | $mute | index($n) != null) | (.id // "")] as $muted
|
|
202
|
+
|
|
203
|
+
# Запись даёт две строки: команду — по ней ищется момент открытия заявки — и вердикт,
|
|
204
|
+
# который проверяется теми же правилами, что и в широкой выборке. Порядок один и тот же,
|
|
205
|
+
# поэтому отсчёт от найденной команды остаётся верным.
|
|
206
|
+
| [ .[] | {
|
|
207
|
+
cmd: (if .type == "assistant"
|
|
208
|
+
then ([(.message.content // [])[] | select(.type == "tool_use") | (.input.command // "")] | join("\n"))
|
|
209
|
+
else "" end),
|
|
210
|
+
say: (if .type == "assistant" then ""
|
|
211
|
+
elif .type == "user" then
|
|
212
|
+
([ ((.message.content // []) | if type == "array" then .[] else empty end
|
|
213
|
+
| select(.type == "tool_result")
|
|
214
|
+
| select((.tool_use_id // "") | if . == "" then true else ($muted | index(.)) == null end)
|
|
215
|
+
| .content | textof),
|
|
216
|
+
((.message.content // "") | if type == "string" then . else "" end),
|
|
217
|
+
(. as $rec
|
|
218
|
+
| if ($rec.toolUseResult // null) == null then ""
|
|
219
|
+
elif ([($rec.message.content // []) | if type == "array" then .[] else empty end
|
|
220
|
+
| select(.type == "tool_result") | (.tool_use_id // "")]
|
|
221
|
+
| map($muted | index(.)) | any(. != null)) then ""
|
|
222
|
+
else ($rec.toolUseResult | textof) end)
|
|
223
|
+
] | join("\n"))
|
|
224
|
+
else tostring end)
|
|
225
|
+
} ] as $flow
|
|
226
|
+
| ($flow | map(.cmd | test("pr[[:space:]]+create|mr[[:space:]]+create")) | index(true)) as $opened
|
|
177
227
|
| if $opened == null then "нет-pr"
|
|
178
|
-
else ($flow[($opened + 1):] | join("\n")
|
|
228
|
+
else ($flow[($opened + 1):] | map(.say) | join("\n")
|
|
179
229
|
| [scan("ЭКЗАМЕН:[[:space:]]*сдано[[:space:]]*([0-9]+)[[:space:]]*из[[:space:]]*([0-9]+)")]
|
|
180
230
|
| if length == 0 then "нет"
|
|
181
231
|
elif (.[-1] | .[0] == .[1]) then "сдан"
|
|
@@ -185,7 +235,7 @@ if [ "$ready" = "1" ]; then
|
|
|
185
235
|
case "$after" in
|
|
186
236
|
сдан | нет-pr) exit 0 ;;
|
|
187
237
|
*)
|
|
188
|
-
deny "BLOCKED by exam-guard: черновик снимается после второго экзамена, а его за эту сессию не было. Позови роль strict-teacher с правилами поставки и с тем, чего требовала задача: между чтением этих правил и снятием черновика прошёл весь заход."
|
|
238
|
+
deny "BLOCKED by exam-guard: черновик снимается после второго экзамена, а его за эту сессию не было. Позови роль strict-teacher с правилами поставки и с тем, чего требовала задача: между чтением этих правил и снятием черновика прошёл весь заход. Выход через список выключенных ролей требует снять защиту, и среда исполнения такую правку может запрещать; второй выход её не требует — объяви обход строкой «Exam-skip: причина» в теле последнего коммита ветки: она остаётся в истории и видна владельцу на странице заявки."
|
|
189
239
|
;;
|
|
190
240
|
esac
|
|
191
241
|
fi
|
|
@@ -193,7 +243,7 @@ fi
|
|
|
193
243
|
case "$verdict" in
|
|
194
244
|
сдан) exit 0 ;;
|
|
195
245
|
провален)
|
|
196
|
-
deny "BLOCKED by exam-guard: экзамен по загруженным правилам провален. Перечитай правило целиком — не тот кусок, о котором спрашивали, — и позови роль strict-teacher снова. Показанный ответ даёт знание одной строки, а не правила."
|
|
246
|
+
deny "BLOCKED by exam-guard: экзамен по загруженным правилам провален. Перечитай правило целиком — не тот кусок, о котором спрашивали, — и позови роль strict-teacher снова. Показанный ответ даёт знание одной строки, а не правила. Выход через список выключенных ролей требует снять защиту, и среда исполнения такую правку может запрещать; второй выход её не требует — обход объявляется строкой «Exam-skip: причина» в теле последнего коммита ветки."
|
|
197
247
|
;;
|
|
198
248
|
*)
|
|
199
249
|
deny "BLOCKED by exam-guard: за эту сессию экзамена по загруженным правилам не было. Позови роль strict-teacher, передай ей список загруженных правил, ответь на её вопросы по памяти и верни ей ответы — вердикт она отдаёт строкой «ЭКЗАМЕН: сдано N из 5». Засчитывается он из ответа роли в любой форме, какой его доставил хост, но не из вывода оболочки и не из твоего же текста: печать этой строки эхом гард не отпускает. Роль уже звали и вердикт получен — значит, он пришёл формой, которой гард не видит: это дефект гарда, и правка `.claude/rt-kit.json` из-под него выведена. Загруженное правило и прочитанное правило — разные вещи, и цену этой разницы платит владелец."
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Снятие черновика для гарда поставки: есть ли у заявки разбор, не конфликтует ли она и той ли
|
|
3
|
+
# записью открыта.
|
|
4
|
+
#
|
|
5
|
+
# Строки `# rt-hook:` здесь нет намеренно: событие и образец вызова объявляет сам гард, а
|
|
6
|
+
# помощник рядом хуком не регистрируется и в одиночку ничего не решает. Он зовётся из гарда и
|
|
7
|
+
# пользуется его же переменными — командой, корнем дерева, машинной записью — и его отказом.
|
|
8
|
+
#
|
|
9
|
+
# Вынесен он отсюда потому, что гард дорос до предела длины: снятие черновика среди его
|
|
10
|
+
# предметов самое отдельное — своя точка проверки, свой сетевой ярус, свои три отказа.
|
|
11
|
+
|
|
12
|
+
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
|
|
13
|
+
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
|
|
14
|
+
|
|
15
|
+
rt_delivery_draft_ready() {
|
|
16
|
+
# --- снятие черновика --------------------------------------------------------------------
|
|
17
|
+
#
|
|
18
|
+
# Ревьювера до этой правки не спрашивал никто. Он стоял прозой в правиле, а хостинг запрос
|
|
19
|
+
# разбора на самого себя принимает молча и не создаёт: разбор при этом выглядит запрошенным.
|
|
20
|
+
# Раньше снятия черновика спросить негде — до открытия заявки ревьювера нет вовсе, — а само
|
|
21
|
+
# снятие и есть тот ход, которым работа объявляется готовой.
|
|
22
|
+
#
|
|
23
|
+
# Ярус сетевой, и молчит он так же, как ярус состояния задачи: нет ответа — нет требования.
|
|
24
|
+
#
|
|
25
|
+
# Возврат заявки в черновик под требование не подпадает: он делает ровно то, чего гард и
|
|
26
|
+
# добивается, — снимает с работы вид готовой.
|
|
27
|
+
if printf '%s' "$cmd" | grep -qE "${RT_CMD_BOUND}(gh[[:space:]]+pr[[:space:]]+ready|glab[[:space:]]+mr[[:space:]]+update[^|;&]*--ready)([[:space:]]|\$)" \
|
|
28
|
+
&& ! printf '%s' "$cmd" | grep -q -- '--undo'; then
|
|
29
|
+
# Ссылка на заявку необязательна: без неё клиент берёт заявку текущей ветки, и это самая
|
|
30
|
+
# короткая форма вызова. Требовать номер значило бы снимать всё требование одним пробелом.
|
|
31
|
+
# Поэтому берётся первый довод, каким бы он ни был — номер, адрес или имя ветки, — а его
|
|
32
|
+
# отсутствие означает «спроси про текущую ветку».
|
|
33
|
+
pull_ref="$(printf '%s' "$cmd" | sed -nE 's/.*(gh[[:space:]]+pr[[:space:]]+ready|glab[[:space:]]+mr[[:space:]]+update)[[:space:]]+([^[:space:];&|-][^[:space:];&|]*).*/\2/p' | head -1)"
|
|
34
|
+
if rt_needs rt_pull_state git-guard-delivery; then
|
|
35
|
+
pull="$(cd "$root" && rt_pull_state "$pull_ref" 2>/dev/null)" || pull=''
|
|
36
|
+
if [ -n "$pull" ] && printf '%s' "$pull" | jq -e '.exists' >/dev/null 2>&1; then
|
|
37
|
+
# Номер берётся из ответа, а если его там нет — из самой команды: заявка,
|
|
38
|
+
# названная адресом или именем ветки, в отказе должна остаться узнаваемой.
|
|
39
|
+
pull_name="$(printf '%s' "$pull" | jq -r '.number // empty' 2>/dev/null)"
|
|
40
|
+
# Номер пишется с решёткой, а имя ветки или адрес — в кавычках: решётка перед
|
|
41
|
+
# адресом читается как опечатка, а не как ссылка на заявку.
|
|
42
|
+
[ -z "$pull_name" ] && pull_name="$pull_ref"
|
|
43
|
+
case "$pull_name" in
|
|
44
|
+
'') ;;
|
|
45
|
+
*[!0-9]*) pull_name=" «${pull_name}»" ;;
|
|
46
|
+
*) pull_name=" #${pull_name}" ;;
|
|
47
|
+
esac
|
|
48
|
+
printf '%s' "$pull" | jq -e '.reviewed' >/dev/null 2>&1 \
|
|
49
|
+
|| fault "у заявки${pull_name} нет разбора: ревьювер не запрошен и отзыва никто не оставлял. Снятый черновик читается как «можно вливать», а вливать некому — назначь ревьювера и повтори."
|
|
50
|
+
|
|
51
|
+
# Конфликт приезжает в отданную заявку чужим слиянием, без единого действия её
|
|
52
|
+
# автора: основание, проверенное на открытии, к моменту снятия черновика уже
|
|
53
|
+
# вчерашнее. Снятый черновик читается как «можно вливать», а вливать нечего —
|
|
54
|
+
# владелец открывает заявку и находит там конфликт. Молчание при неизвестной
|
|
55
|
+
# сливаемости остаётся: хостинг считает её заново после каждой правки главной ветки,
|
|
56
|
+
# и «ещё не посчитано» — не «конфликтует».
|
|
57
|
+
printf '%s' "$pull" | jq -e '.conflicting' >/dev/null 2>&1 \
|
|
58
|
+
&& fault "заявка${pull_name} конфликтует с главной веткой. Влей её в свою ветку, разбери конфликт и повтори: снятый черновик читается как «можно вливать», а слить эту заявку нельзя."
|
|
59
|
+
|
|
60
|
+
# Автор заявки. На открытии судить было нечем, кроме текста команды: личность вызова
|
|
61
|
+
# приходит окружением. Здесь она уже названа хостингом, и это последний ход, где
|
|
62
|
+
# промах ещё исправим — после снятия черновика заявку вливают, а переоткрыть влитую
|
|
63
|
+
# нельзя. Дерево, не назвавшее машинной записи, автора не судит.
|
|
64
|
+
if [ -n "$task_bot" ]; then
|
|
65
|
+
pull_author="$(printf '%s' "$pull" | jq -r '.author // empty' 2>/dev/null)"
|
|
66
|
+
[ -n "$pull_author" ] && [ "$pull_author" != "$task_bot" ] \
|
|
67
|
+
&& fault "заявку${pull_name} открыла запись «${pull_author}», а не машинная «${task_bot}». Автор заявки её ревьювером не бывает, и разбор ей назначить нечем. Автора не сменить — закрой заявку и открой заново${pull_token_hint:+, подставив токен: ${pull_token_hint} …}."
|
|
68
|
+
fi
|
|
69
|
+
fi
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
# Папка задачи: тот же предмет, что на открытии и на слиянии, третьим рубежом. Условие
|
|
73
|
+
# местное — оно читает ветку, а не хостинг, — и потому стоит вне сетевого яруса выше.
|
|
74
|
+
command -v rt_delivery_ready_folder >/dev/null 2>&1 && rt_delivery_ready_folder
|
|
75
|
+
|
|
76
|
+
deny_faults
|
|
77
|
+
fi
|
|
78
|
+
}
|