@rt-tools/agent-kit 0.8.1 → 0.8.2
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 +24 -19
- package/assets/agents/qa-engineer.md +1 -1
- package/assets/checks/board.github.mjs +56 -17
- package/assets/checks/check-board.github.mjs +49 -5
- package/assets/checks/task-new.github.mjs +33 -5
- package/assets/commands/agent-kit-digest.md +10 -5
- package/assets/commands/next-session.md +4 -4
- package/assets/commands/skill-curator.md +11 -9
- package/assets/defaults/gate-map.sh +11 -4
- package/assets/defaults/project.sh +46 -0
- package/assets/docs/GLOSSARY.md +28 -26
- package/assets/hooks/docs-guard.sh +19 -3
- package/assets/hooks/git-guard-delivery.sh +106 -13
- package/assets/hooks/proposal-guard.sh +93 -0
- package/assets/hooks/reuse-first-guard.sh +68 -14
- package/assets/hooks/skill-gate-layers.sh +1 -1
- package/assets/hooks/skill-gate.sh +26 -0
- package/assets/hooks/task-flow-guard.sh +44 -18
- package/assets/hooks/window-fill-guard.sh +1 -1
- package/assets/laws/delivery.md +13 -7
- package/assets/laws/project-documentation.md +21 -0
- package/assets/laws/verifiability.md +6 -1
- package/assets/laws/work-conduct.md +67 -3
- package/assets/patterns/git-workflow-commit.azure.md +10 -2
- package/assets/patterns/git-workflow-commit.github.md +15 -2
- package/assets/patterns/git-workflow-commit.gitlab.md +10 -2
- package/assets/patterns/git-workflow-merge.md +1 -1
- package/assets/patterns/spec-driven-domain.md +7 -1
- package/assets/patterns/spec-driven-rule.md +6 -0
- package/assets/patterns/task-flow-close.md +78 -9
- package/assets/patterns/task-flow-handoff.md +27 -4
- package/assets/patterns/task-flow-resume.md +23 -5
- package/assets/patterns/task-flow-start.md +5 -1
- package/assets/rules/browser-verification.md +10 -1
- package/assets/rules/doc-style.md +13 -5
- package/assets/rules/git-workflow.azure.md +12 -7
- package/assets/rules/git-workflow.github.md +31 -13
- package/assets/rules/git-workflow.gitlab.md +12 -7
- package/assets/rules/spec-driven.md +4 -0
- package/assets/rules/task-flow.md +47 -22
- package/assets/rules/testing.md +19 -0
- package/assets/rules/typescript-conventions.md +12 -0
- package/assets/skills/agent-kit-extend.md +24 -0
- package/assets/skills/agent-kit.md +54 -6
- package/assets/traits.json +14 -0
- package/bin/agent-kit.d.ts.map +1 -1
- package/bin/agent-kit.js +31 -16
- package/bin/agent-kit.js.map +1 -1
- package/index.d.ts +1 -0
- package/index.d.ts.map +1 -1
- package/index.js +1 -0
- package/index.js.map +1 -1
- package/lib/argv.d.ts +17 -0
- package/lib/argv.d.ts.map +1 -0
- package/lib/argv.js +44 -0
- package/lib/argv.js.map +1 -0
- package/lib/cargo.d.ts +88 -0
- package/lib/cargo.d.ts.map +1 -0
- package/lib/cargo.js +16 -0
- package/lib/cargo.js.map +1 -0
- package/lib/catalog.d.ts +18 -1
- package/lib/catalog.d.ts.map +1 -1
- package/lib/catalog.js +12 -2
- package/lib/catalog.js.map +1 -1
- package/lib/commands.d.ts +0 -26
- package/lib/commands.d.ts.map +1 -1
- package/lib/commands.js +78 -122
- package/lib/commands.js.map +1 -1
- package/lib/companion.d.ts +37 -0
- package/lib/companion.d.ts.map +1 -1
- package/lib/companion.js +42 -1
- package/lib/companion.js.map +1 -1
- package/lib/config.d.ts +28 -0
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +20 -0
- package/lib/config.js.map +1 -1
- package/lib/ship.d.ts +39 -0
- package/lib/ship.d.ts.map +1 -0
- package/lib/ship.js +87 -0
- package/lib/ship.js.map +1 -0
- package/lib/shipment.d.ts +60 -0
- package/lib/shipment.d.ts.map +1 -0
- package/lib/shipment.js +247 -0
- package/lib/shipment.js.map +1 -0
- package/lib/snapshot.d.ts +30 -0
- package/lib/snapshot.d.ts.map +1 -0
- package/lib/snapshot.js +73 -0
- package/lib/snapshot.js.map +1 -0
- package/lib/traits.d.ts +32 -0
- package/lib/traits.d.ts.map +1 -0
- package/lib/traits.js +82 -0
- package/lib/traits.js.map +1 -0
- package/package.json +6 -2
- package/rt-tools-agent-kit-0.8.2.tgz +0 -0
- package/lib/submit.d.ts +0 -24
- package/lib/submit.d.ts.map +0 -1
- package/lib/submit.js +0 -26
- package/lib/submit.js.map +0 -1
- package/rt-tools-agent-kit-0.8.1.tgz +0 -0
- /package/assets/rules/{entity-conventions.md → entity-conventions.needs-admin.md} +0 -0
- /package/assets/rules/{observability.md → observability.needs-app.md} +0 -0
package/assets/docs/GLOSSARY.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Словарь проекта
|
|
2
2
|
|
|
3
3
|
Слова, которые в этом дереве значат что-то определённое. Читается перед тем, как написать спек,
|
|
4
|
-
правило, комментарий, тело коммита или описание
|
|
4
|
+
правило, комментарий, тело коммита или описание PR: слово отсюда употребляется в том
|
|
5
5
|
значении, что здесь, а слово не отсюда либо заводится здесь же, либо заменяется простым.
|
|
6
6
|
|
|
7
7
|
Термины одного домена живут в разделе «Терминология» его спека — здесь только те, что проходят
|
|
@@ -36,21 +36,21 @@
|
|
|
36
36
|
|
|
37
37
|
## Работа
|
|
38
38
|
|
|
39
|
-
| Термин | Что это
|
|
40
|
-
| --------------- |
|
|
41
|
-
| Задача | Единица работы в очереди работ. Заводится до ветки, и номер её стоит в имени ветки и в заголовке
|
|
42
|
-
|
|
|
43
|
-
| Очередь работ | Доска, на которой видно состояние каждой задачи. Ветки она не видит
|
|
44
|
-
| Папка задачи | Одна работа от разбора до слияния: разбор просьбы, замысел, ход работы. Умирает со слиянием — разбирается, и объясняющее решение уезжает в архив
|
|
45
|
-
| Разбор | Расспрос владельца до первой правки. Записывается его словами и задним числом не переписывается
|
|
46
|
-
| Замысел | Файл папки задачи: след задачи и этапы с признаками готовности. После написания не правится — с ним сверяют результат при приёмке
|
|
47
|
-
| Ход работы | Файл папки задачи: «Где стоим», решения по ходу с причинами, записи заходов. Единственное место, где отмечается сделанное. Журналом не называется
|
|
48
|
-
| След задачи | Раздел замысла: какие спеки, законы, правила и части кода работа задевает
|
|
49
|
-
| Заход | Одна сессия работы над задачей. Работа живёт дольше одного захода, и между ними её состояние держит только ход работы
|
|
50
|
-
| Заполнение окна | Доля места захода, которую он уже занял: вход, запись в кэш, прочитанное из кэша и вывод последнего ответа, делённые на размер окна. Не «расход» и не «бюджет»: речь о месте, а не о деньгах
|
|
51
|
-
| Передача | Текст, которым заход закрывается: рабочее дерево, ветка, задача, где лежит ход работы, что сделано, следующий шаг, особенности захода. Кладётся вне дерева и не коммитится
|
|
52
|
-
|
|
|
53
|
-
| Архив | Записи о состоявшемся: что объясняет закрытое решение. После выкатки не правится
|
|
39
|
+
| Термин | Что это |
|
|
40
|
+
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
41
|
+
| Задача | Единица работы в очереди работ. Заводится до ветки, и номер её стоит в имени ветки и в заголовке PR |
|
|
42
|
+
| PR | Заявка на слияние: то же название, что у задачи, переведённое в сделанное. Отчётом, пул-реквестом и мёрдж-реквестом не называется — ни в файлах, ни в разговоре |
|
|
43
|
+
| Очередь работ | Доска, на которой видно состояние каждой задачи. Ветки она не видит |
|
|
44
|
+
| Папка задачи | Одна работа от разбора до слияния: разбор просьбы, замысел, ход работы. Умирает со слиянием — разбирается, и объясняющее решение уезжает в архив |
|
|
45
|
+
| Разбор | Расспрос владельца до первой правки. Записывается его словами и задним числом не переписывается |
|
|
46
|
+
| Замысел | Файл папки задачи: след задачи и этапы с признаками готовности. После написания не правится — с ним сверяют результат при приёмке |
|
|
47
|
+
| Ход работы | Файл папки задачи: «Где стоим», решения по ходу с причинами, записи заходов. Единственное место, где отмечается сделанное. Журналом не называется |
|
|
48
|
+
| След задачи | Раздел замысла: какие спеки, законы, правила и части кода работа задевает |
|
|
49
|
+
| Заход | Одна сессия работы над задачей. Работа живёт дольше одного захода, и между ними её состояние держит только ход работы |
|
|
50
|
+
| Заполнение окна | Доля места захода, которую он уже занял: вход, запись в кэш, прочитанное из кэша и вывод последнего ответа, делённые на размер окна. Не «расход» и не «бюджет»: речь о месте, а не о деньгах |
|
|
51
|
+
| Передача | Текст, которым заход закрывается: рабочее дерево, ветка, задача, где лежит ход работы, что сделано, следующий шаг, особенности захода. Кладётся вне дерева и не коммитится |
|
|
52
|
+
| Эпик | Серия задач одной темы, выполняемых в назначенном порядке. Живёт в двух местах сразу: карточка в очереди работ с меткой эпика и замысел рядом с ней — что за возможность разрабатывается, какие задачи входят и в каком порядке. Шире одной ветки. Линией работ не называется |
|
|
53
|
+
| Архив | Записи о состоявшемся: что объясняет закрытое решение. После выкатки не правится |
|
|
54
54
|
|
|
55
55
|
## Проверки
|
|
56
56
|
|
|
@@ -65,13 +65,15 @@
|
|
|
65
65
|
|
|
66
66
|
## Так не пишем
|
|
67
67
|
|
|
68
|
-
| Так не пишем
|
|
69
|
-
|
|
|
70
|
-
| спека (о тесте)
|
|
71
|
-
| таска, тикет
|
|
72
|
-
| пул-реквест, мёрдж-реквест
|
|
73
|
-
|
|
|
74
|
-
|
|
|
75
|
-
|
|
|
76
|
-
|
|
|
77
|
-
|
|
|
68
|
+
| Так не пишем | Пишем так |
|
|
69
|
+
| --------------------------- | ---------------------------------------------------------------------------------------------- |
|
|
70
|
+
| спека (о тесте) | тест — файл рядом с исходником; спек — документ. Одна буква разницы, а значения противоположны |
|
|
71
|
+
| таска, тикет | задача |
|
|
72
|
+
| пул-реквест, мёрдж-реквест | PR, а действие — слияние |
|
|
73
|
+
| отчёт (о заявке на слияние) | PR; отчёт — сводка данных, и слово занято ею |
|
|
74
|
+
| джоба, пайплайн | конвейер и его шаг |
|
|
75
|
+
| хендофф | передача |
|
|
76
|
+
| бэклог | очередь работ |
|
|
77
|
+
| линия работ | эпик |
|
|
78
|
+
| контекст-виндоу | окно захода, а его доля — заполнение окна |
|
|
79
|
+
| скилл, скилы | правило, паттерн или скил без закона — по тому, что это на самом деле |
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
#
|
|
18
18
|
# Отдельно — законы. Совпал ли код с законом, машина не знает: правка файла, на который закон
|
|
19
19
|
# ссылается якорем, поэтому не отклоняется, а выносится вопросом владельцу. Тем же вопросом
|
|
20
|
-
# встречается и правка самого
|
|
21
|
-
# молча гард не даёт.
|
|
20
|
+
# встречается и правка самого закона — как на месте, так и надстройкой над ним: закон описывает
|
|
21
|
+
# договорённость о продукте, и менять её молча гард не даёт.
|
|
22
22
|
#
|
|
23
23
|
# Обход — строка `Docs-skip: <причина>` в теле коммита. Причина остаётся в истории и видна при
|
|
24
24
|
# разборе ветки; пустая не принимается.
|
|
@@ -66,11 +66,18 @@ command -v rt_needs >/dev/null 2>&1 || rt_needs() { command -v "$1" >/dev/null 2
|
|
|
66
66
|
|
|
67
67
|
laws_dir="${RT_LAWS_DIR:-docs/constitution}"
|
|
68
68
|
lib_marker="${RT_LIB_MARKER:-project.json}"
|
|
69
|
+
overrides_dir="${RT_OVERRIDES_DIR:-.claude/rt-kit/overrides}"
|
|
69
70
|
|
|
70
71
|
# ── Правка закона спрашивает владельца ────────────────────────────────────────
|
|
71
72
|
#
|
|
72
73
|
# Спутник рядом с законом — привязка статей к коду, она устаревает при каждом переименовании и
|
|
73
74
|
# правится свободно. Спрашивается только сам текст закона.
|
|
75
|
+
#
|
|
76
|
+
# Путей к тексту закона два, и ходят чаще вторым. Разложенный закон на месте не правится вовсе:
|
|
77
|
+
# правка теряется на следующей раскладке, а сама раскладка на неё отказывает, — поэтому правят
|
|
78
|
+
# надстройку, и файл закона переписывает раскладка. Гард, знающий один каталог законов, сторожит
|
|
79
|
+
# ровно тот путь, которым к закону и не ходят: чем сложнее надстройка дерева, тем реже закон
|
|
80
|
+
# правят на месте. Спрашивается и надстройка — по содержанию это правка закона, а не обвязки.
|
|
74
81
|
case "$tool" in
|
|
75
82
|
Edit | Write | MultiEdit | NotebookEdit | mcp__webstorm__create_new_file)
|
|
76
83
|
target="$(printf '%s' "$input" | jq -r '.tool_input.file_path // .tool_input.filePath // .tool_input.path // .tool_input.pathInProject // empty' 2>/dev/null)"
|
|
@@ -79,6 +86,10 @@ case "$tool" in
|
|
|
79
86
|
*/"$laws_dir"/*.md | "$laws_dir"/*.md)
|
|
80
87
|
decide ask "Правка закона: \`${target##*/}\`. Закон описывает договорённость о продукте, а не устройство кода, — назови владельцу, что и почему меняешь, и дождись ответа. Если правка уже согласована, подтверди вызов."
|
|
81
88
|
;;
|
|
89
|
+
*/"$overrides_dir"/laws/*.implementation.md | "$overrides_dir"/laws/*.implementation.md) exit 0 ;;
|
|
90
|
+
*/"$overrides_dir"/laws/*.md | "$overrides_dir"/laws/*.md)
|
|
91
|
+
decide ask "Правка закона надстройкой: \`${target##*/}\`. Разложенный файл переписывает раскладка, поэтому правка надстройки — это правка самого закона: назови владельцу, что и почему меняешь, и дождись ответа. Если правка уже согласована, подтверди вызов."
|
|
92
|
+
;;
|
|
82
93
|
esac
|
|
83
94
|
exit 0
|
|
84
95
|
;;
|
|
@@ -112,7 +123,12 @@ esac
|
|
|
112
123
|
|
|
113
124
|
# Причина обхода остаётся в истории, поэтому обход законен. Пустая строка обходом не считается:
|
|
114
125
|
# «Docs-skip:» без причины — это тот же молчаливый пропуск, только с двоеточием.
|
|
115
|
-
|
|
126
|
+
#
|
|
127
|
+
# Строка начинает строку — свою в теле коммита или комментарий в конце команды — и подстановки
|
|
128
|
+
# не принимает. То же условие, что у обхода при слиянии: иначе текст, который ОБЪЯСНЯЕТ обход,
|
|
129
|
+
# снимает требование сам собой. Тело коммита о правке гарда как раз называет эту строку, и без
|
|
130
|
+
# привязки к началу гард пропускал бы такой коммит молча.
|
|
131
|
+
if printf '%s' "$cmd" | grep -qiE '(^|#)[[:space:]]*Docs-skip:[[:space:]]*[^[:space:]<"'"'"'][^[:space:]"'"'"']{2,}'; then
|
|
116
132
|
exit 0
|
|
117
133
|
fi
|
|
118
134
|
|
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# rt-hook: PreToolUse Bash|mcp__webstorm__execute_terminal_command|mcp__webstorm__execute_tool
|
|
3
3
|
# Требует: hooks/profile-check.sh
|
|
4
|
-
# Гард поставки. PreToolUse на заведении
|
|
4
|
+
# Гард поставки. PreToolUse на заведении ветки, пуше и открытии заявки на слияние.
|
|
5
5
|
#
|
|
6
6
|
# Закон о поставке требует трёх вещей, которых обычно не проверяет ничто: правка начинается с
|
|
7
|
-
# задачи, видимой в очереди работ; задача, ветка и
|
|
7
|
+
# задачи, видимой в очереди работ; задача, ветка и PR несут один номер; у задачи есть
|
|
8
8
|
# исполнитель. Держатся они памятью — и не удерживаются: задачи стоят вне очереди, исполнитель
|
|
9
9
|
# не проставлен, а большинство влитых заявок приходит с веток, за которыми задачи не стояло
|
|
10
10
|
# вовсе.
|
|
11
11
|
#
|
|
12
|
-
# Гард стоит в
|
|
12
|
+
# Гард стоит в трёх точках, и в каждой требует того, что в этот момент исправимо:
|
|
13
13
|
#
|
|
14
14
|
# заведение ветки — имя с номером разбирается на месте; имя без номера пропускается:
|
|
15
15
|
# локальная ветка под пробу законна, в главную она не поедет, потому что заявка с неё
|
|
16
16
|
# не откроется;
|
|
17
|
+
# пуш — подпись машинного коммита ещё переписывается на месте; после пуша её чинит только
|
|
18
|
+
# силовая отправка;
|
|
17
19
|
# открытие заявки — ветка обязана нести номер, заголовок обязан начинаться с того же номера,
|
|
18
20
|
# а задача — быть открытой, стоять в очереди работ и иметь исполнителя.
|
|
19
21
|
#
|
|
@@ -28,7 +30,9 @@
|
|
|
28
30
|
# numbered); молчание значит «спросить некого»;
|
|
29
31
|
# RT_TASK_NEW_CMD — чем заводится задача;
|
|
30
32
|
# RT_BOARD_CHECK_CMD — чем сверяется очередь работ;
|
|
31
|
-
# RT_TASK_BOT — учётная запись, которую ставят
|
|
33
|
+
# RT_TASK_BOT — учётная запись, которую ставят исполнителем;
|
|
34
|
+
# RT_COMMIT_EMAIL — почта, которой подписан машинный коммит; её же левой частью он и
|
|
35
|
+
# опознаётся.
|
|
32
36
|
# Отказ называет и то, что не так, и чем это чинится: отказ без действия обходят, а не исполняют.
|
|
33
37
|
#
|
|
34
38
|
# ОТКАЗ В ПОЛЬЗУ РАБОТЫ: не репозиторий, нет разборщика, битый ввод, нет профиля — пропуск.
|
|
@@ -87,6 +91,7 @@ title_re="${RT_TASK_TITLE_RE:-^\[[A-Za-z]+-[0-9]+\][[:space:]]+[^[:space:]]}"
|
|
|
87
91
|
task_new="${RT_TASK_NEW_CMD:-npm run task:new}"
|
|
88
92
|
board_check="${RT_BOARD_CHECK_CMD:-npm run check:board}"
|
|
89
93
|
task_bot="${RT_TASK_BOT:-}"
|
|
94
|
+
commit_email="${RT_COMMIT_EMAIL:-}"
|
|
90
95
|
tasks_dir="${RT_TASKS_DIR:-}"
|
|
91
96
|
archive_dir="${RT_ARCHIVE_DIR:-}"
|
|
92
97
|
main_branch="${RT_MAIN_BRANCH:-main}"
|
|
@@ -94,7 +99,13 @@ main_branch="${RT_MAIN_BRANCH:-main}"
|
|
|
94
99
|
# Обход требования: строка с причиной. Причина видна тому, кто вливает, поэтому обход законен.
|
|
95
100
|
# Без причины это просто молчаливый пропуск, поэтому она обязательна. Порог в три знака — тот
|
|
96
101
|
# же, что у гарда документа: если сделать по-разному, две формы одного обхода разойдутся.
|
|
97
|
-
|
|
102
|
+
#
|
|
103
|
+
# Строка обхода начинает строку — свою в теле PR или комментарий в конце команды — и
|
|
104
|
+
# подстановки не принимает. Без этих двух условий текст, который ОБЪЯСНЯЕТ, что обход называется
|
|
105
|
+
# так-то, от самого обхода неотличим: тело PR со строкой-примером снимало требование само
|
|
106
|
+
# собой, и тот же пример гард печатает в своём отказе ниже. Нашлось это приёмкой такого же
|
|
107
|
+
# признака — прогон обязан был покраснеть на настоящем нарушении и остался зелёным.
|
|
108
|
+
folder_skip_re='(^|#)[[:space:]]*Task-folder-skip:[[:space:]]*[^[:space:]<"'"'"'][^[:space:]"'"'"']{2,}'
|
|
98
109
|
|
|
99
110
|
deny() {
|
|
100
111
|
# Отказ гарда — наблюдение: гард, отбивающий чаще прочих, говорит, какое место поставки
|
|
@@ -109,7 +120,7 @@ deny() {
|
|
|
109
120
|
exit 0
|
|
110
121
|
}
|
|
111
122
|
|
|
112
|
-
# Подсказка вместо отказа: на открытии
|
|
123
|
+
# Подсказка вместо отказа: на открытии PR папка ещё нужна. Решения подсказка не несёт,
|
|
113
124
|
# команда идёт дальше своим ходом.
|
|
114
125
|
hint() {
|
|
115
126
|
jq -n --arg c "$1" '{hookSpecificOutput:{hookEventName:"PreToolUse",additionalContext:$c}}' 2>/dev/null
|
|
@@ -165,6 +176,66 @@ if [ -n "$branch_arg" ]; then
|
|
|
165
176
|
exit 0
|
|
166
177
|
fi
|
|
167
178
|
|
|
179
|
+
# --- подпись машинного коммита ------------------------------------------------------------
|
|
180
|
+
#
|
|
181
|
+
# Служебный адрес хостинга состоит из числа, логина и домена, а сопоставляется по числу: логин
|
|
182
|
+
# рядом с ним не сверяет никто. Коммит с чужим числом хостинг припишет постороннему человеку, и
|
|
183
|
+
# изнутри это выглядит верным — имя учётной записи в истории то самое. Так одиннадцать коммитов
|
|
184
|
+
# и уехали в главную ветку за чужой подписью; нашёл это владелец, читая историю глазами.
|
|
185
|
+
#
|
|
186
|
+
# Точка — пуш: до него подпись переписывается на месте всей веткой сразу, после — только силовой
|
|
187
|
+
# отправкой. На коммите гард не стоит: почта задаётся переменными самой команды, и разбор её из
|
|
188
|
+
# текста ловил бы ту же строку, которая и так перед глазами у набравшего её.
|
|
189
|
+
#
|
|
190
|
+
# Судится коммит, НАЗВАВШИЙСЯ машинной записью: её логин стоит именем автора либо левой частью
|
|
191
|
+
# почты. Опознавать по самой почте нельзя — она в таком коммите как раз и неверна, а требовать
|
|
192
|
+
# машинную подпись от каждого коммита значило бы отбивать работу, сделанную человеком своими
|
|
193
|
+
# руками в том же дереве.
|
|
194
|
+
#
|
|
195
|
+
# Логин читается из объявленной почты, а не объявляется вторым свойством: два объявления одного
|
|
196
|
+
# логина разошлись бы молча. Исполнителем задачи он тоже не бывает — в дереве, где хостинг
|
|
197
|
+
# ограничил машинную запись, исполнителем ставят человека, а коммит остаётся машинным.
|
|
198
|
+
#
|
|
199
|
+
# Вызов пуша узнаётся по двум признакам сразу — команда `git` в начале строки или за
|
|
200
|
+
# разделителем и слово `push` отдельным словом. Одной подстрокой «git push» его не поймать:
|
|
201
|
+
# помощник учётных данных и заголовок запроса ставятся ключами `-c` между ними.
|
|
202
|
+
# Пробный пуш не отправляет ничего, и подпись у него не спрашивают.
|
|
203
|
+
if printf '%s' "$cmd" | grep -qE '(^|[;&|(]|&&|\|\|)[[:space:]]*git([[:space:]]|$)' \
|
|
204
|
+
&& printf '%s' "$cmd" | grep -qE '(^|[[:space:]])push([[:space:]]|$)' \
|
|
205
|
+
&& ! printf '%s' "$cmd" | grep -q -- '--dry-run'; then
|
|
206
|
+
# Два условия молчания. Дерево, не назвавшее почты машинной записи, требования не получает:
|
|
207
|
+
# своей машинной записи у пакета нет, а выдуманная отбивала бы работу в чужом дереве.
|
|
208
|
+
# Вершины главной ветки нет — вклад считать не от чего.
|
|
209
|
+
#
|
|
210
|
+
# Судится вклад ветки, а не вся история: влитое в главную этой веткой уже не чинится, и
|
|
211
|
+
# отказ за него отбивал бы работу вместо промаха.
|
|
212
|
+
if [ -n "$commit_email" ] \
|
|
213
|
+
&& git rev-parse --verify --quiet "refs/remotes/origin/${main_branch}" >/dev/null 2>&1; then
|
|
214
|
+
# Левая часть служебного адреса: `<число>+<логин>` либо просто логин.
|
|
215
|
+
bot_login="${commit_email%%@*}"
|
|
216
|
+
bot_login="${bot_login##*+}"
|
|
217
|
+
|
|
218
|
+
strangers=''
|
|
219
|
+
while IFS="$(printf '\t')" read -r short author email; do
|
|
220
|
+
[ -z "$short" ] && continue
|
|
221
|
+
login="${email%%@*}"
|
|
222
|
+
login="${login##*+}"
|
|
223
|
+
[ "$author" = "$bot_login" ] || [ "$login" = "$bot_login" ] || continue
|
|
224
|
+
[ "$email" = "$commit_email" ] && continue
|
|
225
|
+
strangers="${strangers}${strangers:+, }${short} <${email}>"
|
|
226
|
+
done <<EOF
|
|
227
|
+
$(git log --format='%h%x09%an%x09%ae' "origin/${main_branch}..HEAD" 2>/dev/null)
|
|
228
|
+
EOF
|
|
229
|
+
|
|
230
|
+
[ -n "$strangers" ] \
|
|
231
|
+
&& deny "BLOCKED: машинный коммит подписан не той почтой, что объявлена деревом. Хостинг сопоставляет служебный адрес по числу в нём, и коммит с чужим числом он припишет постороннему человеку — изнутри промах не виден, потому что имя учётной записи рядом верное. Расходятся: ${strangers}. Объявлено: ${commit_email} — почта берётся оттуда, а не набирается по памяти. Перепиши подпись до пуша, после него это чинит только силовая отправка:
|
|
232
|
+
последний коммит — GIT_AUTHOR_NAME=\"${bot_login}\" GIT_AUTHOR_EMAIL=\"${commit_email}\" GIT_COMMITTER_NAME=\"${bot_login}\" GIT_COMMITTER_EMAIL=\"${commit_email}\" git commit --amend --no-edit --reset-author
|
|
233
|
+
весь вклад ветки — git filter-branch -f --env-filter 'GIT_AUTHOR_EMAIL=\"${commit_email}\"; GIT_COMMITTER_EMAIL=\"${commit_email}\"' origin/${main_branch}..HEAD"
|
|
234
|
+
fi
|
|
235
|
+
fi
|
|
236
|
+
# Своего выхода у этой точки нет: пуш бывает и составной командой, а второе её звено судят
|
|
237
|
+
# разделы ниже.
|
|
238
|
+
|
|
168
239
|
# --- слияние заявки ----------------------------------------------------------------------
|
|
169
240
|
#
|
|
170
241
|
# Папку задачи разбирают тем же PR, что и работу. После слияния этого уже никто не сделает:
|
|
@@ -243,24 +314,46 @@ fi
|
|
|
243
314
|
|
|
244
315
|
if [ -n "$title" ]; then
|
|
245
316
|
printf '%s' "$title" | grep -qE "$title_re" \
|
|
246
|
-
|| deny "BLOCKED: заголовок заявки не начинается с номера задачи. В списке заявок тела не видно, а строка связи живёт именно там — без номера в заголовке
|
|
317
|
+
|| deny "BLOCKED: заголовок заявки не начинается с номера задачи. В списке заявок тела не видно, а строка связи живёт именно там — без номера в заголовке PR с задачей не сопоставить."
|
|
247
318
|
title_number="$(printf '%s' "$title" | sed -nE 's/^\[[A-Za-z]+-([0-9]+)\].*/\1/p')"
|
|
248
319
|
if [ -n "$number" ] && [ -n "$title_number" ]; then
|
|
249
320
|
[ "$title_number" = "$number" ] \
|
|
250
|
-
|| deny "BLOCKED: в заголовке заявки номер ${title_number}, у ветки — ${number}. Задача, ветка и
|
|
321
|
+
|| deny "BLOCKED: в заголовке заявки номер ${title_number}, у ветки — ${number}. Задача, ветка и PR несут один и тот же номер."
|
|
251
322
|
fi
|
|
252
323
|
fi
|
|
253
324
|
|
|
254
|
-
# Главная ветка влита до открытия
|
|
325
|
+
# Главная ветка влита до открытия PR. PR от разошедшейся ветки показывает ревьюверу свою
|
|
255
326
|
# правку вперемешку с чужой, а проверки на нём гоняются от устаревшего основания.
|
|
256
327
|
#
|
|
257
|
-
#
|
|
258
|
-
#
|
|
259
|
-
#
|
|
328
|
+
# Ярусов два, и порядок между ними такой же, как у проверки задачи. Первый читает локальную
|
|
329
|
+
# вершину и работает без сети. Второй спрашивает удалённую ссылку — без него молчание гарда
|
|
330
|
+
# значит лишь «твоя ссылка не старше твоей ветки», а читается как «главная ветка влита»: ровно
|
|
331
|
+
# так открытый PR и оказался конфликтующим, и узнал об этом владелец.
|
|
332
|
+
#
|
|
333
|
+
# Сеть здесь допустима по той же причине, по которой её зовёт проверка очереди работ ниже:
|
|
334
|
+
# отсутствие ответа пропускается молча, и проверка, падающая в самолёте, не отбивает работу.
|
|
335
|
+
# Предел ожидания задаётся переменными самого git — внешний `timeout` есть не на всякой машине.
|
|
260
336
|
if git rev-parse --verify --quiet "refs/remotes/origin/${main_branch}" >/dev/null 2>&1 \
|
|
261
337
|
&& ! git merge-base --is-ancestor "origin/${main_branch}" HEAD 2>/dev/null; then
|
|
262
338
|
behind="$(git rev-list --count "HEAD..origin/${main_branch}" 2>/dev/null)"
|
|
263
|
-
deny "BLOCKED: «${main_branch}» ушла вперёд на ${behind:-несколько} коммитов, а в ветку не влита.
|
|
339
|
+
deny "BLOCKED: «${main_branch}» ушла вперёд на ${behind:-несколько} коммитов, а в ветку не влита. PR от разошедшейся ветки показывает ревьюверу правку вперемешку с чужой, а проверки на нём идут от устаревшего основания. Влей и повтори: git fetch origin && git merge origin/${main_branch} — порядок и разбор конфликта в паттерне git-workflow-merge."
|
|
340
|
+
fi
|
|
341
|
+
|
|
342
|
+
# Второй ярус: локальная ссылка сама могла протухнуть. Ответа нет — ярус молчит.
|
|
343
|
+
remote_main="$(GIT_HTTP_LOW_SPEED_LIMIT=1000 GIT_HTTP_LOW_SPEED_TIME=5 GIT_TERMINAL_PROMPT=0 \
|
|
344
|
+
git ls-remote origin "refs/heads/${main_branch}" 2>/dev/null | cut -f1)"
|
|
345
|
+
local_main="$(git rev-parse --verify --quiet "refs/remotes/origin/${main_branch}" 2>/dev/null)"
|
|
346
|
+
if [ -n "$remote_main" ] && [ -n "$local_main" ] && [ "$remote_main" != "$local_main" ]; then
|
|
347
|
+
# Возраст ссылки — то, чего исполнитель не видит вовсе, и именно он отличает «ветка
|
|
348
|
+
# отстала» от «я не знаю, отстала ли». Формат `stat` на BSD и на GNU разный, поэтому
|
|
349
|
+
# спрашиваются оба, а не угадывается система.
|
|
350
|
+
fetch_head="$(git rev-parse --git-dir 2>/dev/null)/FETCH_HEAD"
|
|
351
|
+
fetched_at="$(stat -f %m "$fetch_head" 2>/dev/null || stat -c %Y "$fetch_head" 2>/dev/null)"
|
|
352
|
+
age=''
|
|
353
|
+
if [ -n "$fetched_at" ]; then
|
|
354
|
+
age=" Последний git fetch — $(( ( $(date +%s) - fetched_at ) / 60 )) мин. назад."
|
|
355
|
+
fi
|
|
356
|
+
deny "BLOCKED: твоя ссылка origin/${main_branch} отстала от удалённой — ${local_main:0:8} против ${remote_main:0:8}.${age} Гард сравнивает ветку с тем, что лежит в дереве, поэтому молчание первого яруса значит «ссылка не старше ветки», а не «главная ветка влита». Влей и повтори: git fetch origin && git merge origin/${main_branch}."
|
|
264
357
|
fi
|
|
265
358
|
|
|
266
359
|
check_task "$number" "заявка с ветки «${branch}»"
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# rt-hook: Stop
|
|
3
|
+
# Гард предложения: ход, в котором владелец сказал завести или отправить предложение слою правил,
|
|
4
|
+
# не заканчивается, пока отправки не было. Stop.
|
|
5
|
+
#
|
|
6
|
+
# Зачем именно так. Написанное и не отправленное лежит в дереве неотличимо от отправленного:
|
|
7
|
+
# своей записи в слое правил у него нет, и владелец читает работу сделанной, пока не спросит
|
|
8
|
+
# прямо. Отправка при этом неудобна ровно в одном месте — она пишет отметки в файлы предложений и
|
|
9
|
+
# делает дерево грязным, — и этим доводом исполнитель закрывает молчание правил.
|
|
10
|
+
#
|
|
11
|
+
# Ловится просьба образцами, а не пониманием смысла: оценку «владелец просил отправить» назначал
|
|
12
|
+
# бы тот, кому она мешает. Глагол обязателен: «разбери пропозалы» — работа над уже приехавшими, и
|
|
13
|
+
# отправкой она не кончается. Слово «предложение» само по себе не считается тоже — оно ходит в
|
|
14
|
+
# каждом втором ходе о другом, и рядом с ним нужно слово о слое правил или о пакете.
|
|
15
|
+
#
|
|
16
|
+
# Отправкой считается вызов команды пакета без сухого прогона, сделанный в том же ходе: сухой
|
|
17
|
+
# прогон показывает, что уехало бы, ничего не заводит и следа наружу не оставляет.
|
|
18
|
+
#
|
|
19
|
+
# ОТКАЗ В ПОЛЬЗУ РАБОТЫ: при любой ошибке, нехватке `jq`, отсутствии записи хода и повторном
|
|
20
|
+
# заходе ход РАЗРЕШАЕТСЯ (exit 0). Сломанный гард не имеет права заклинить разговор.
|
|
21
|
+
|
|
22
|
+
input="$(cat 2>/dev/null)"
|
|
23
|
+
[ -z "$input" ] && exit 0
|
|
24
|
+
|
|
25
|
+
command -v jq >/dev/null 2>&1 || exit 0
|
|
26
|
+
|
|
27
|
+
# Повторный заход по тому же ходу не судится: гард сказал своё один раз и отпускает.
|
|
28
|
+
active="$(printf '%s' "$input" | jq -r '.stop_hook_active // false' 2>/dev/null)"
|
|
29
|
+
[ "$active" = "true" ] && exit 0
|
|
30
|
+
|
|
31
|
+
transcript="$(printf '%s' "$input" | jq -r '.transcript_path // empty' 2>/dev/null)"
|
|
32
|
+
[ -z "$transcript" ] && exit 0
|
|
33
|
+
[ -f "$transcript" ] || exit 0
|
|
34
|
+
|
|
35
|
+
# Каталог предложений у дерева свой. Заданный пустым — отказ дерева от требования: дереву, не
|
|
36
|
+
# берущему переносимый слой правил, гард не навязывается.
|
|
37
|
+
proposals_dir="${RT_PROPOSALS_DIR-.claude/rt-kit/proposals}"
|
|
38
|
+
[ -z "$proposals_dir" ] && exit 0
|
|
39
|
+
|
|
40
|
+
# Просьба владельца. Набор открыт и пополняется правкой: полнота его — открытый вопрос
|
|
41
|
+
# договорённости, а не обещание.
|
|
42
|
+
asked_re='(завед|напиш|отправ|пошл|зашл|отошл|выгруз)[а-яё]*[^.!?]{0,40}(пропозал|предложени)|(пропозал|предложени)[а-яё]*[^.!?]{0,40}(завед|напиш|отправ|пошл|зашл|отошл|выгруз)'
|
|
43
|
+
|
|
44
|
+
# Сосед, без которого слово «предложение» не считается просьбой о слое правил.
|
|
45
|
+
context_re='пропозал|слою правил|слоя правил|слой правил|пакет|agent-kit|наверх'
|
|
46
|
+
|
|
47
|
+
# Ход — это всё, что записано после последнего настоящего ввода владельца. Ответ инструмента
|
|
48
|
+
# приходит той же ролью, поэтому строки с `tool_result` вводом не считаются.
|
|
49
|
+
#
|
|
50
|
+
# Хвост в 400 строк: запись хода растёт всю сессию, а судится только последний ход.
|
|
51
|
+
verdict="$(tail -n 400 "$transcript" 2>/dev/null | jq -s -r --arg asked "$asked_re" --arg ctx "$context_re" '
|
|
52
|
+
def is_input:
|
|
53
|
+
.type == "user"
|
|
54
|
+
and (((.message.content // []) | if type == "array"
|
|
55
|
+
then ([.[] | select(.type == "tool_result")] | length)
|
|
56
|
+
else 0 end) == 0);
|
|
57
|
+
|
|
58
|
+
def text_of:
|
|
59
|
+
(.message.content // []) | if type == "array"
|
|
60
|
+
then ([.[] | select(.type == "text") | .text] | join("\n"))
|
|
61
|
+
else (. // "") end;
|
|
62
|
+
|
|
63
|
+
(map(is_input) | rindex(true)) as $i
|
|
64
|
+
| (if $i == null then [] else .[$i:] end) as $turn
|
|
65
|
+
| (if $i == null then "" else ($turn[0] | text_of) end) as $said
|
|
66
|
+
| [$turn[] | select(.type == "assistant") | (.message.content // [])[] | select(.type == "tool_use")] as $uses
|
|
67
|
+
# Признак нечувствителен к регистру флагом, а не приведением: приведение знает только
|
|
68
|
+
# латиницу, и «Отправь Пропозал» с большой буквы проходило бы мимо набора образцов молча.
|
|
69
|
+
| (($said | test($asked; "i")) and ($said | test($ctx; "i"))) as $wanted
|
|
70
|
+
| ($uses | map(
|
|
71
|
+
((.name // "") | test("^Bash$"))
|
|
72
|
+
and ((.input.command // "") | test("agent-kit[^|;&]*propose"))
|
|
73
|
+
and ((.input.command // "") | test("--dry-run") | not)
|
|
74
|
+
) | any) as $sent
|
|
75
|
+
| if $wanted and ($sent | not) then "owe" else "pass" end
|
|
76
|
+
' 2>/dev/null)"
|
|
77
|
+
|
|
78
|
+
[ "$verdict" = "owe" ] || exit 0
|
|
79
|
+
|
|
80
|
+
reason="BLOCKED by proposal-guard: владелец сказал завести или отправить предложение слою правил, а отправки в этом ходе не было. Написанное и не отправленное лежит в дереве неотличимо от отправленного: своей записи в слое правил у него нет, и владелец читает работу сделанной, пока не спросит прямо.
|
|
81
|
+
|
|
82
|
+
Предложение пишется файлом в \`$proposals_dir/\` и уезжает в тот же ход:
|
|
83
|
+
|
|
84
|
+
npx agent-kit propose
|
|
85
|
+
|
|
86
|
+
Сухой прогон отправкой не является: он показывает, что уехало бы, и следа наружу не оставляет. Отправка пишет отметки в файлы предложений и делает дерево грязным — при открытом PR они ложатся вторым коммитом в ту же ветку, и это их место, а не повод отложить.
|
|
87
|
+
|
|
88
|
+
Гард судит один ход: следующий заход не отбивается."
|
|
89
|
+
|
|
90
|
+
jq -n --arg r "$reason" '{decision:"block",reason:$r}' 2>/dev/null \
|
|
91
|
+
|| printf '{"decision":"block","reason":"proposal-guard: владелец просил предложение — отправь его командой пакета."}\n'
|
|
92
|
+
|
|
93
|
+
exit 0
|
|
@@ -38,14 +38,46 @@ command -v jq >/dev/null 2>&1 || exit 0
|
|
|
38
38
|
command -v perl >/dev/null 2>&1 || exit 0
|
|
39
39
|
|
|
40
40
|
tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
|
|
41
|
+
shell_cmd=""
|
|
41
42
|
case "$tool" in
|
|
42
43
|
# Инструмент среды заводит файл теми же двумя данными, только называет их иначе — без этой
|
|
43
44
|
# ветки файл заводился мимо всех проверок.
|
|
44
45
|
Edit | Write | MultiEdit | mcp__webstorm__create_new_file) ;;
|
|
46
|
+
# Команда оболочки, которая пишет файл, — та же правка. Без этой ветки гард обходится
|
|
47
|
+
# сменой не инструмента, а способа записи; текстом правки тогда служит сама команда, и
|
|
48
|
+
# заведённое ею в heredoc читается наравне с телом правки. Разбор —
|
|
49
|
+
# `2026-08-15-guard-denied-shell-wrote-anyway.md`.
|
|
50
|
+
Bash)
|
|
51
|
+
shell_cmd="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null)"
|
|
52
|
+
[ -z "$shell_cmd" ] && exit 0
|
|
53
|
+
;;
|
|
45
54
|
*) exit 0 ;;
|
|
46
55
|
esac
|
|
47
56
|
|
|
48
|
-
|
|
57
|
+
# Профиль дерева: сперва умолчание пакета, поверх него — надстройка проекта, если она есть.
|
|
58
|
+
# Читается до разбора пути: пути из команды оболочки вынимает как раз профиль.
|
|
59
|
+
rt_hooks_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
60
|
+
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
|
|
61
|
+
# shellcheck disable=SC1090
|
|
62
|
+
[ -f "$profile" ] && . "$profile" 2>/dev/null
|
|
63
|
+
done
|
|
64
|
+
|
|
65
|
+
if [ -n "$shell_cmd" ]; then
|
|
66
|
+
command -v rt_shell_writes >/dev/null 2>&1 && command -v rt_shell_paths >/dev/null 2>&1 || exit 0
|
|
67
|
+
rt_shell_writes "$shell_cmd" || exit 0
|
|
68
|
+
# Из команды берётся первый путь, чьё расширение гарду интересно: остальные ему безразличны.
|
|
69
|
+
path=""
|
|
70
|
+
while IFS= read -r candidate; do
|
|
71
|
+
case "$candidate" in
|
|
72
|
+
*.html | *.scss | *.ts) path="$candidate"; break ;;
|
|
73
|
+
esac
|
|
74
|
+
done <<EOF
|
|
75
|
+
$(rt_shell_paths "$shell_cmd")
|
|
76
|
+
EOF
|
|
77
|
+
[ -z "$path" ] && exit 0
|
|
78
|
+
else
|
|
79
|
+
path="$(printf '%s' "$input" | jq -r '.tool_input.file_path // .tool_input.pathInProject // empty' 2>/dev/null)"
|
|
80
|
+
fi
|
|
49
81
|
# Путь от корня дерева приводится к абсолютному один раз, чтобы образцы не двоились.
|
|
50
82
|
case "$path" in
|
|
51
83
|
/*) ;;
|
|
@@ -56,13 +88,6 @@ case "$path" in
|
|
|
56
88
|
*) exit 0 ;;
|
|
57
89
|
esac
|
|
58
90
|
|
|
59
|
-
# Профиль дерева: сперва умолчание пакета, поверх него — надстройка проекта, если она есть.
|
|
60
|
-
rt_hooks_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
61
|
-
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
|
|
62
|
-
# shellcheck disable=SC1090
|
|
63
|
-
[ -f "$profile" ] && . "$profile" 2>/dev/null
|
|
64
|
-
done
|
|
65
|
-
|
|
66
91
|
# Слово о нехватке функции профиля: хук, вышедший молча, неотличим от работающего. Файл может
|
|
67
92
|
# быть не разложен — тогда остаётся прежнее поведение, молчаливое.
|
|
68
93
|
# shellcheck disable=SC1090
|
|
@@ -82,11 +107,16 @@ if [ -n "${RT_REUSE_SKIP_RE:-}" ] && printf '%s' "$path" | grep -qE "$RT_REUSE_S
|
|
|
82
107
|
exit 0
|
|
83
108
|
fi
|
|
84
109
|
|
|
85
|
-
# Только новый текст: строка, уже лежавшая в файле, этой правкой не заводилась.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
110
|
+
# Только новый текст: строка, уже лежавшая в файле, этой правкой не заводилась. У команды
|
|
111
|
+
# оболочки таким текстом служит она сама: что она кладёт в файл, лежит в ней же.
|
|
112
|
+
if [ -n "$shell_cmd" ]; then
|
|
113
|
+
added="$shell_cmd"
|
|
114
|
+
else
|
|
115
|
+
added="$(printf '%s' "$input" | jq -r '
|
|
116
|
+
[ .tool_input.content?, .tool_input.text?, .tool_input.new_string?, (.tool_input.edits[]?.new_string) ]
|
|
117
|
+
| map(select(. != null)) | join("\n")
|
|
118
|
+
' 2>/dev/null)"
|
|
119
|
+
fi
|
|
90
120
|
[ -z "$added" ] && exit 0
|
|
91
121
|
|
|
92
122
|
# Явный отказ от правила: готового такого нет, автор это осознал и пометил. Считается по
|
|
@@ -138,11 +168,19 @@ has_re() {
|
|
|
138
168
|
rt_checks_json="${CLAUDE_PROJECT_DIR:-.}/.claude/rt-kit/checks.json"
|
|
139
169
|
rt_bundles=''
|
|
140
170
|
rt_own_signals=''
|
|
171
|
+
rt_backend_roots=''
|
|
141
172
|
if [ -f "$rt_checks_json" ]; then
|
|
142
173
|
rt_bundles="$(jq -r '.reuse.bundles[]? // empty' "$rt_checks_json" 2>/dev/null | tr '\n' ' ')"
|
|
143
174
|
own="$(jq -r '.reuse.signals // empty' "$rt_checks_json" 2>/dev/null)"
|
|
144
175
|
[ -n "$own" ] && rt_own_signals="${CLAUDE_PROJECT_DIR:-.}/$own"
|
|
176
|
+
# Тем же ключом, каким их читает сплошная проверка: второе объявление тех же корней
|
|
177
|
+
# разошлось бы с первым молча.
|
|
178
|
+
rt_backend_roots="$(jq -r '.backendRoots[]? // empty' "$rt_checks_json" 2>/dev/null | tr '\n' ' ')"
|
|
145
179
|
fi
|
|
180
|
+
|
|
181
|
+
# Путь от корня дерева: образец в признаке пишет дерево, и писать его от корня машины оно не
|
|
182
|
+
# может. Абсолютный путь нужен только для чтения файла с диска.
|
|
183
|
+
rel_path="${path#"${CLAUDE_PROJECT_DIR:-.}"/}"
|
|
146
184
|
rt_signals_dir="${CLAUDE_PROJECT_DIR:-.}/${RT_REUSE_SIGNALS_DIR:-tools/signals}"
|
|
147
185
|
|
|
148
186
|
# Признаки объявленных наборов: те же файлы читает сплошная проверка. Ключ признака совпал с
|
|
@@ -172,9 +210,25 @@ while IFS= read -r signal; do
|
|
|
172
210
|
'') ;;
|
|
173
211
|
*) case "$path" in *"$ext") ;; *) continue ;; esac ;;
|
|
174
212
|
esac
|
|
213
|
+
# Пропуск корней бэкенда: признак, объявленный с ним, на бэкенде не действует вовсе — там
|
|
214
|
+
# принят другой способ, и базового класса, которого признак требует, у бэкенда нет. Поле
|
|
215
|
+
# читает и сплошная проверка; читать его одному из двоих значит отбивать гардом ту самую
|
|
216
|
+
# правку, которую проверка пропускает, — а провести её больше нечем: маркер отступления
|
|
217
|
+
# объявляет обход готового, а обхода тут не было.
|
|
218
|
+
if [ "$(field "$signal" '.skipBackendRoots')" = 'true' ] && [ -n "$rt_backend_roots" ]; then
|
|
219
|
+
skip_backend=''
|
|
220
|
+
for backend_root in $rt_backend_roots; do
|
|
221
|
+
case "$rel_path" in "$backend_root"*) skip_backend=1 ;; esac
|
|
222
|
+
done
|
|
223
|
+
[ -n "$skip_backend" ] && continue
|
|
224
|
+
fi
|
|
225
|
+
|
|
226
|
+
# Образец имени сверяется с путём от корня дерева, а не с именем файла: слои, которые
|
|
227
|
+
# признак и разделяет, зовут свои файлы одинаково, и по имени они неразличимы. Сплошная
|
|
228
|
+
# проверка сверяет с путём — расходиться им нельзя.
|
|
175
229
|
only_named="$(field "$signal" '.onlyNamed')"
|
|
176
230
|
if [ -n "$only_named" ]; then
|
|
177
|
-
printf '%s' "$
|
|
231
|
+
printf '%s' "$rel_path" | grep -qE "$only_named" || continue
|
|
178
232
|
fi
|
|
179
233
|
|
|
180
234
|
case "$(field "$signal" '.scope')" in
|
|
@@ -124,7 +124,7 @@ esac
|
|
|
124
124
|
|
|
125
125
|
# --- слой по пути: ведение работы -------------------------------------------------------------
|
|
126
126
|
#
|
|
127
|
-
# Папка задачи, договорённость о продукте до кода и
|
|
127
|
+
# Папка задачи, договорённость о продукте до кода и замысел эпика — первые файлы, которые
|
|
128
128
|
# заводятся в работе. Требование ловит на них того, кто пошёл мимо порядка: «пришла новая
|
|
129
129
|
# задача» инструментом не является, и поймать это больше нечем.
|
|
130
130
|
case "$target" in
|