@rt-tools/agent-kit 0.16.0 → 0.17.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-paths.github.mjs +88 -0
- package/assets/checks/board-runs.github.mjs +8 -0
- package/assets/checks/board-titles.github.mjs +66 -0
- package/assets/checks/check-board.github.mjs +17 -4
- package/assets/checks/check-file-size.mjs +10 -2
- package/assets/checks/check-glossary.mjs +170 -0
- package/assets/checks/check-push-gate.mjs +59 -1
- package/assets/checks/check-schema-drift.mjs +12 -5
- package/assets/checks/spec-contract.mjs +9 -0
- package/assets/defaults/gate-map.sh +13 -4
- package/assets/defaults/project.sh +22 -3
- package/assets/docs/GLOSSARY.md +1 -1
- package/assets/hooks/browser-device-id.sh +20 -4
- package/assets/hooks/browser-guard-device-id.sh +5 -2
- package/assets/hooks/git-guard-delivery-folder.sh +19 -0
- package/assets/hooks/git-guard-delivery.sh +39 -0
- package/assets/hooks/git-guard-push-tests.sh +21 -1
- package/assets/hooks/grill-gate-ask.sh +25 -0
- package/assets/hooks/grill-gate.sh +12 -5
- package/assets/hooks/lint-after-edit.sh +44 -16
- package/assets/hooks/proposal-guard.sh +17 -1
- package/assets/hooks/skill-gate-layers.sh +7 -0
- package/assets/hooks/skill-gate.sh +29 -0
- package/assets/hooks/task-context-load.sh +10 -0
- package/assets/hooks/task-flow-context.sh +185 -0
- package/assets/hooks/task-flow-draft-guard.sh +92 -0
- package/assets/hooks/task-flow-guard.sh +27 -159
- package/assets/hooks/turn-exit-guard.sh +8 -1
- package/assets/hooks/window-fill-guard.sh +5 -1
- package/assets/laws/delivery.md +19 -0
- package/assets/laws/frontend-application.md +4 -0
- package/assets/laws/verifiability.md +21 -0
- package/assets/laws/work-conduct.md +15 -0
- package/assets/patterns/browser-verification-stand.md +7 -2
- package/assets/patterns/doc-style-write.md +41 -1
- package/assets/patterns/git-workflow-commit.github.md +15 -2
- package/assets/patterns/git-workflow-docker.md +14 -0
- package/assets/patterns/git-workflow-merge.md +18 -0
- package/assets/patterns/git-workflow-migration.md +11 -0
- package/assets/patterns/git-workflow-pr.github.md +6 -1
- package/assets/patterns/git-workflow-secrets.md +14 -0
- package/assets/patterns/git-workflow-stack.md +63 -1
- package/assets/patterns/spec-driven-domain.md +34 -1
- package/assets/patterns/spec-driven-rule.md +11 -0
- package/assets/patterns/spec-driven-sweep.md +57 -0
- package/assets/patterns/task-flow-archive.md +46 -14
- package/assets/patterns/task-flow-close.md +78 -2
- package/assets/patterns/task-flow-resume.md +17 -5
- package/assets/patterns/task-flow-start.md +42 -4
- package/assets/pitfalls/agent-kit.md +73 -3
- package/assets/pitfalls/doc-style.md +5 -0
- package/assets/pitfalls/git-workflow.github.md +68 -0
- package/assets/pitfalls/spec-driven.md +16 -0
- package/assets/pitfalls/task-flow.md +93 -0
- package/assets/pitfalls/turn-conduct.md +14 -0
- package/assets/rules/browser-verification.md +39 -0
- package/assets/rules/deploy-flow.azure.md +8 -0
- package/assets/rules/deploy-flow.github.md +18 -0
- package/assets/rules/deploy-flow.gitlab.md +8 -0
- package/assets/rules/doc-style.md +14 -0
- package/assets/rules/git-workflow.azure.md +5 -0
- package/assets/rules/git-workflow.github.md +85 -75
- package/assets/rules/git-workflow.gitlab.md +5 -0
- package/assets/rules/reuse-first.md +8 -0
- package/assets/rules/shared-code.md +5 -0
- package/assets/rules/spec-driven.md +14 -0
- package/assets/rules/task-flow.md +112 -112
- package/assets/rules/testing.md +14 -1
- package/assets/rules/turn-conduct.md +40 -27
- package/assets/rules/turn-entry.md +6 -0
- package/assets/samples/tasks/_template/grill.md +5 -0
- package/assets/samples/tasks/_template/plan.md +3 -0
- package/assets/skills/agent-kit.md +99 -76
- package/assets/templates/postmortem.md +5 -1
- package/bin/agent-kit.d.ts.map +1 -1
- package/bin/agent-kit.js +30 -6
- package/bin/agent-kit.js.map +1 -1
- package/lib/catalog.d.ts.map +1 -1
- package/lib/catalog.js +2 -1
- package/lib/catalog.js.map +1 -1
- package/lib/commands.d.ts.map +1 -1
- package/lib/commands.js +96 -7
- package/lib/commands.js.map +1 -1
- package/lib/hooks-map.d.ts +26 -0
- package/lib/hooks-map.d.ts.map +1 -1
- package/lib/hooks-map.js +58 -2
- package/lib/hooks-map.js.map +1 -1
- package/lib/sections.d.ts +6 -0
- package/lib/sections.d.ts.map +1 -1
- package/lib/sections.js +19 -0
- package/lib/sections.js.map +1 -1
- package/lib/shipment.d.ts +2 -0
- package/lib/shipment.d.ts.map +1 -1
- package/lib/shipment.fixture.d.ts +5 -0
- package/lib/shipment.fixture.d.ts.map +1 -1
- package/lib/shipment.fixture.js +7 -0
- package/lib/shipment.fixture.js.map +1 -1
- package/lib/shipment.js +13 -1
- package/lib/shipment.js.map +1 -1
- package/lib/sync.d.ts +35 -3
- package/lib/sync.d.ts.map +1 -1
- package/lib/sync.js +59 -8
- package/lib/sync.js.map +1 -1
- package/package.json +1 -1
- package/rt-tools-agent-kit-0.17.0.tgz +0 -0
- package/rt-tools-agent-kit-0.16.0.tgz +0 -0
|
@@ -87,7 +87,7 @@ rt_push_checks_default() {
|
|
|
87
87
|
[ -x "$root/$RT_HOOKS_TESTS" ] && printf '%s\n' "bash $RT_HOOKS_TESTS"
|
|
88
88
|
|
|
89
89
|
for check in check-doc-paths check-specs check-file-size check-dupes check-styles \
|
|
90
|
-
check-lib-layers check-reuse check-schema-drift check-states check-state-next \
|
|
90
|
+
check-glossary check-lib-layers check-reuse check-schema-drift check-states check-state-next \
|
|
91
91
|
check-turn-map check-archive-age check-push-gate; do
|
|
92
92
|
[ -f "$root/$RT_CHECKS_DIR/$check.mjs" ] && printf '%s\n' "node $RT_CHECKS_DIR/$check.mjs"
|
|
93
93
|
done
|
|
@@ -183,6 +183,9 @@ RT_LAWS_DIR="${RT_LAWS_DIR:-docs/constitution}"
|
|
|
183
183
|
RT_RULES_DIR="${RT_RULES_DIR:-.claude/skills}"
|
|
184
184
|
RT_SPECS_DIR="${RT_SPECS_DIR:-docs/specs}"
|
|
185
185
|
|
|
186
|
+
# Каталог замыслов, переживающих одну задачу: порядок задач эпика лежит там, а не в правилах.
|
|
187
|
+
RT_PLANS_DIR="${RT_PLANS_DIR:-docs/plans}"
|
|
188
|
+
|
|
186
189
|
# Главная ветка. Гарду она нужна, чтобы найти общего предка и понять, что ветка сделала с
|
|
187
190
|
# папкой задачи и с архивом. Если общего предка нет, сравнивать не с чем — проверка молчит.
|
|
188
191
|
RT_MAIN_BRANCH="${RT_MAIN_BRANCH:-main}"
|
|
@@ -239,11 +242,21 @@ rt_is_app_code_default() {
|
|
|
239
242
|
# `grep -rn x libs/ 2>/dev/null` судится наравне с записью — правило требуется на чтение, а
|
|
240
243
|
# отбитий, пришедшихся не на правку файла, набирается большинство. Настоящая запись рядом с
|
|
241
244
|
# заглушённым потоком остаётся видной: снимается перенаправление, а не команда целиком.
|
|
245
|
+
#
|
|
246
|
+
# Стрелка снимается там же и по той же причине. `->` и `=>` в оболочке не значат ничего, а знак
|
|
247
|
+
# в них тот же: команда, печатавшая таблицу «путь -> правило», объявлялась пишущей и отдавала
|
|
248
|
+
# все свои пути под суд. Туда же закрывающая скобка комментария разметки.
|
|
249
|
+
#
|
|
250
|
+
# Цель перенаправления сужена до знаков, из которых собирают пути. Строка цитаты разметки —
|
|
251
|
+
# знак и слово через пробел — от записи в файл одним знаком неотличима, и различает их только
|
|
252
|
+
# цель: за настоящим знаком стоит путь, а не слово словами. Цена названа прямо: путь, набранный
|
|
253
|
+
# не латиницей, записью больше не считается — в дереве таких нет ни одного, а появятся, признак
|
|
254
|
+
# придётся расширить.
|
|
242
255
|
rt_shell_writes_default() {
|
|
243
256
|
printf '%s' "$1" \
|
|
244
|
-
| sed -E 's#(&|[0-9]*)>>?[[:space:]]*/dev/(null|stderr)##g; s#[0-9]*>&[0-9-]##g' \
|
|
257
|
+
| sed -E 's#(&|[0-9]*)>>?[[:space:]]*/dev/(null|stderr)##g; s#[0-9]*>&[0-9-]##g; s#[-=]+>##g' \
|
|
245
258
|
| grep -Eq \
|
|
246
|
-
'>>?[[:space:]]*[
|
|
259
|
+
'>>?[[: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'
|
|
247
260
|
}
|
|
248
261
|
|
|
249
262
|
# Пути, названные командой оболочки. Печатает по одному в строке; судит их зовущий.
|
|
@@ -372,6 +385,12 @@ RT_TASK_BOT="${RT_TASK_BOT:-}"
|
|
|
372
385
|
RT_PULL_TOKEN_VAR="${RT_PULL_TOKEN_VAR:-}"
|
|
373
386
|
RT_PULL_TOKEN_HINT="${RT_PULL_TOKEN_HINT:-}"
|
|
374
387
|
|
|
388
|
+
# Раздел, который тело заявки обязано нести с минуты открытия: решение о слиянии принимается на
|
|
389
|
+
# её странице, где переписки нет вовсе, и сказанного вслух там не остаётся. Умолчание молчит —
|
|
390
|
+
# заголовок пишется языком заявки, а чужих слов пакет не знает: не названный деревом, раздел не
|
|
391
|
+
# судится вовсе.
|
|
392
|
+
RT_PULL_BODY_SECTION="${RT_PULL_BODY_SECTION:-}"
|
|
393
|
+
|
|
375
394
|
# Команда перевода задачи между колонками очереди работ и имя первой колонки — той, из которой
|
|
376
395
|
# задача уходит, когда её берут в работу. Имя без умолчания: колонки дерево называет своими
|
|
377
396
|
# словами, а выдуманное не совпало бы ни с чем и молча выключило бы проверку колонки.
|
package/assets/docs/GLOSSARY.md
CHANGED
|
@@ -73,4 +73,4 @@
|
|
|
73
73
|
- **бэклог** — очередь работ
|
|
74
74
|
- **линия работ** — эпик
|
|
75
75
|
- **контекст-виндоу** — окно захода, а его доля — заполнение окна
|
|
76
|
-
- **скилл,
|
|
76
|
+
- **скилл, скиллы** — правило, паттерн или скил без закона — по тому, что это на самом деле
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
+
# Местное значение: .claude/rt-kit/browser-device-id — без него браузерные гарды пропускают любой профиль
|
|
2
3
|
# Общий помощник: печатает идентификатор закреплённого профиля браузера.
|
|
3
4
|
#
|
|
4
5
|
# Идентификатор локален для машины и в пакет не едет вовсе. Он берётся из переменной окружения,
|
|
5
|
-
# а если её нет — из файла рядом с конфигом раскладки. Ни там ни там ничего нет —
|
|
6
|
-
#
|
|
7
|
-
#
|
|
6
|
+
# а если её нет — из файла рядом с конфигом раскладки. Ни там ни там ничего нет — все браузерные
|
|
7
|
+
# гарды пропускают: гард, который не может назвать нужный профиль, ничего не предлагает взамен,
|
|
8
|
+
# и слепой отказ только заводил бы работу в тупик.
|
|
9
|
+
#
|
|
10
|
+
# Пропуск при этом не молчит. Ненастроенное дерево получает строку в поток ошибок — один раз на
|
|
11
|
+
# признак, чтобы она не тонула в каждом вызове: молчание тут неотличимо от «всё в порядке», и
|
|
12
|
+
# читается оно как разрешение водить браузер каким угодно профилем. Признак приходит первым
|
|
13
|
+
# доводом от того, кто зовёт; довода нет — метка дневная.
|
|
8
14
|
#
|
|
9
15
|
# Файл с идентификатором в репозиторий не коммитится: у каждой машины он свой.
|
|
10
16
|
|
|
@@ -16,7 +22,17 @@ if [ -n "${RT_BROWSER_DEVICE_ID:-}" ]; then
|
|
|
16
22
|
fi
|
|
17
23
|
|
|
18
24
|
file="${CLAUDE_PROJECT_DIR:-.}/.claude/rt-kit/browser-device-id"
|
|
19
|
-
|
|
25
|
+
|
|
26
|
+
if [ ! -f "$file" ]; then
|
|
27
|
+
key="${1:-$(date +%Y%m%d 2>/dev/null || printf 'nokey')}"
|
|
28
|
+
marker_dir="${TMPDIR:-/tmp}/claude-browser-guard"
|
|
29
|
+
marker="$marker_dir/silent-$key"
|
|
30
|
+
if [ ! -f "$marker" ]; then
|
|
31
|
+
mkdir -p "$marker_dir" 2>/dev/null && : >"$marker" 2>/dev/null
|
|
32
|
+
echo "Профиль браузера этому дереву не назван: нет ни RT_BROWSER_DEVICE_ID, ни .claude/rt-kit/browser-device-id. Браузерные гарды поэтому пропускают всё подряд — это не разрешение ехать, а повод остановиться и сказать владельцу." >&2
|
|
33
|
+
fi
|
|
34
|
+
exit 0
|
|
35
|
+
fi
|
|
20
36
|
|
|
21
37
|
tr -d '[:space:]' <"$file"
|
|
22
38
|
printf '\n'
|
|
@@ -17,13 +17,16 @@
|
|
|
17
17
|
rt_hook_read
|
|
18
18
|
input="$RT_HOOK_INPUT"
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
# Признак сессии помощнику передаётся: без него слово о ненастроенном дереве метится днём и
|
|
21
|
+
# приходит один раз в сутки, а не один раз за заход.
|
|
22
|
+
sid="$(printf '%s' "$input" | jq -r '.session_id // "nosession"' 2>/dev/null)"
|
|
23
|
+
|
|
24
|
+
device_id="$("${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/browser-device-id.sh" "$sid")"
|
|
21
25
|
[ -z "$device_id" ] && exit 0
|
|
22
26
|
|
|
23
27
|
requested="$(printf '%s' "$input" | jq -r '.tool_input.deviceId // empty' 2>/dev/null)"
|
|
24
28
|
|
|
25
29
|
if [ "$requested" = "$device_id" ]; then
|
|
26
|
-
sid="$(printf '%s' "$input" | jq -r '.session_id // "nosession"' 2>/dev/null)"
|
|
27
30
|
marker_dir="${TMPDIR:-/tmp}/claude-browser-guard"
|
|
28
31
|
mkdir -p "$marker_dir" 2>/dev/null && : >"$marker_dir/${sid}" 2>/dev/null
|
|
29
32
|
exit 0
|
|
@@ -61,6 +61,25 @@ rt_delivery_open_folder() {
|
|
|
61
61
|
&& fault "папку задачи удалили, но в «${archive_dir}» ветка ничего не добавила. Удалить проще, чем разобрать, — и вместе с папкой пропадает разбор просьбы, единственная запись слов владельца. Перенеси то, что объясняет принятые решения, одним файлом с понятным именем и повтори."
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
# Условие снятия черновика: тот же предмет между открытием и слиянием. Снятый черновик читается
|
|
65
|
+
# владельцем как приглашение влить, и кнопку он нажимает, не дожидаясь коммита уборки: гард
|
|
66
|
+
# слияния сюда не достаёт вовсе — нажимает человек на хостинге. Три работы подряд уехали в
|
|
67
|
+
# главную ветку именно так, и в теле каждой заявки стояло обещание убрать папку после одобрения.
|
|
68
|
+
#
|
|
69
|
+
# Ветку функция спрашивает сама: на этом ходе гард её ещё не посчитал.
|
|
70
|
+
rt_delivery_ready_folder() {
|
|
71
|
+
[ -n "$tasks_dir" ] || return 0
|
|
72
|
+
printf '%s' "$cmd" | grep -qiE "$folder_skip_re" && return 0
|
|
73
|
+
|
|
74
|
+
_branch="$(git branch --show-current 2>/dev/null)"
|
|
75
|
+
[ -z "$_branch" ] && return 0
|
|
76
|
+
rt_task_branch_ok "$_branch" || return 0 # за беззадачной веткой папки не стоит
|
|
77
|
+
|
|
78
|
+
_lying="$(rt_folder_in_branch "$tasks_dir/$_branch")"
|
|
79
|
+
[ -n "$_lying" ] \
|
|
80
|
+
&& fault "в ветке лежит папка задачи «${_lying}» — снятый черновик читается как «можно вливать», а вливать её туда нельзя. Кнопку нажимает человек на хостинге, и до его руки папка уедет в главную. Перенеси в «${archive_dir:-архив}» то, что объясняет принятые решения, остальное удали, закоммить и повтори. Если работа вливается частями, поставь в команду комментарий «# Task-folder-skip: <причина>»."
|
|
81
|
+
}
|
|
82
|
+
|
|
64
83
|
# Условие слияния: тот же предмет вторым рубежом. Он ловит слияние, идущее командой, — то, до
|
|
65
84
|
# которого гард достаёт. Отсюда выходят только отказом или молчанием: слияние решается целиком.
|
|
66
85
|
rt_delivery_merge_folder() {
|
|
@@ -107,6 +107,11 @@ task_bot="${RT_TASK_BOT:-}"
|
|
|
107
107
|
# у него может не быть отдельной машинной записи.
|
|
108
108
|
pull_token_var="${RT_PULL_TOKEN_VAR:-}"
|
|
109
109
|
pull_token_hint="${RT_PULL_TOKEN_HINT:-}"
|
|
110
|
+
# Раздел, который тело заявки обязано нести с минуты открытия. Кнопку слияния нажимает человек
|
|
111
|
+
# на хостинге, куда гард не достаёт: всё, чем требование там держится, — то, что владелец увидел
|
|
112
|
+
# на странице. Заголовок раздела пишется языком заявки, поэтому образец называет дерево, а не
|
|
113
|
+
# пакет: чужих слов пакет не знает, и выдуманное умолчание не совпало бы ни с чем.
|
|
114
|
+
pull_body_section="${RT_PULL_BODY_SECTION:-}"
|
|
110
115
|
commit_email="${RT_COMMIT_EMAIL:-}"
|
|
111
116
|
tasks_dir="${RT_TASKS_DIR:-}"
|
|
112
117
|
archive_dir="${RT_ARCHIVE_DIR:-}"
|
|
@@ -327,6 +332,11 @@ if printf '%s' "$cmd" | grep -qE "${RT_CMD_BOUND}(gh[[:space:]]+pr[[:space:]]+re
|
|
|
327
332
|
fi
|
|
328
333
|
fi
|
|
329
334
|
fi
|
|
335
|
+
|
|
336
|
+
# Папка задачи: тот же предмет, что на открытии и на слиянии, третьим рубежом. Условие
|
|
337
|
+
# местное — оно читает ветку, а не хостинг, — и потому стоит вне сетевого яруса выше.
|
|
338
|
+
command -v rt_delivery_ready_folder >/dev/null 2>&1 && rt_delivery_ready_folder
|
|
339
|
+
|
|
330
340
|
deny_faults
|
|
331
341
|
fi
|
|
332
342
|
|
|
@@ -424,6 +434,35 @@ if [ -n "$pull_token_var" ] \
|
|
|
424
434
|
fault "заявка открывается без токена машинной записи: в команде нет подстановки «${pull_token_var}». Открытая залогиненной записью, она выйдет от владельца — ревьювером его тогда не назначить, и чинится это только переоткрытием.${pull_token_hint:+ Подставь токен: ${pull_token_hint} …}"
|
|
425
435
|
fi
|
|
426
436
|
|
|
437
|
+
# Тело заявки несёт раздел об оставшемся шаге, и несёт его с минуты открытия. Требование
|
|
438
|
+
# записано было и прежде — прозой в паттерне закрытия работы, ниже того места, где описано само
|
|
439
|
+
# открытие: читающий доходил до открытия, открывал и раздела ещё не прочитал. Так заявка и
|
|
440
|
+
# уехала без него, а владелец влил её кнопкой, пока шёл прогон.
|
|
441
|
+
#
|
|
442
|
+
# Судится текст команды: тело приходит доводом либо файлом, и оба вида читаются здесь же. Файл
|
|
443
|
+
# читается с диска — к моменту разбора он уже написан. Ни того ни другого в команде нет —
|
|
444
|
+
# требования нет: заявка без тела вовсе судится строкой выше, а не этой.
|
|
445
|
+
if [ -n "$pull_body_section" ]; then
|
|
446
|
+
body=''
|
|
447
|
+
if command -v perl >/dev/null 2>&1; then
|
|
448
|
+
body="$(printf '%s' "$cmd" | perl -0ne '
|
|
449
|
+
if (/(?:--body|-b)(?:=|\s+)(?:"((?:[^"\\]|\\.)*)"|\x27([^\x27]*)\x27|(\S+))/s) {
|
|
450
|
+
print defined $1 ? $1 : (defined $2 ? $2 : $3);
|
|
451
|
+
}
|
|
452
|
+
' 2>/dev/null)"
|
|
453
|
+
body_file="$(printf '%s' "$cmd" | perl -0ne '
|
|
454
|
+
if (/(?:--body-file|-F)(?:=|\s+)(?:"((?:[^"\\]|\\.)*)"|\x27([^\x27]*)\x27|(\S+))/s) {
|
|
455
|
+
print defined $1 ? $1 : (defined $2 ? $2 : $3);
|
|
456
|
+
}
|
|
457
|
+
' 2>/dev/null)"
|
|
458
|
+
[ -z "$body" ] && [ -n "$body_file" ] && [ -f "$body_file" ] && body="$(cat "$body_file" 2>/dev/null)"
|
|
459
|
+
fi
|
|
460
|
+
|
|
461
|
+
if [ -n "$body" ] && ! printf '%s' "$body" | grep -qE "$pull_body_section"; then
|
|
462
|
+
fault "в теле заявки нет раздела об оставшемся шаге. Кнопку слияния нажимает человек на хостинге, где гардов нет, и вливает он, как только видит зелёное: всё, чем требование там держится, — то, что владелец прочитал на странице. Раздел стоит последним и говорит ровно одно — осталось ли что-то до слияния; переписывается он тем же вызовом, которым правится тело."
|
|
463
|
+
fi
|
|
464
|
+
fi
|
|
465
|
+
|
|
427
466
|
check_task "$number" "заявка с ветки «${branch}»" да
|
|
428
467
|
|
|
429
468
|
# Папка задачи разбирается до открытия заявки, а не после одобрения. Прежде здесь стояло
|
|
@@ -119,11 +119,25 @@ if git fetch --quiet origin "$main_branch" 2>/dev/null && git rev-parse --verify
|
|
|
119
119
|
base="origin/$main_branch"
|
|
120
120
|
fi
|
|
121
121
|
|
|
122
|
+
# Код, которым проверка объявляет, что смотреть было не на что: ни «сошлось», ни «расхождение».
|
|
123
|
+
# Прежде такая проверка говорила о себе строкой вывода и выходила нулём — в наборе этот ноль
|
|
124
|
+
# стоял рядом с пройденными и ничем от них не отличался, а сводка читалась как проверенная
|
|
125
|
+
# целиком. Пуш он не отбивает: проверка, которой нечего смотреть, поломкой не является.
|
|
126
|
+
rt_skip_code="${RT_SKIP_CODE:-7}"
|
|
127
|
+
|
|
122
128
|
failed=""
|
|
123
129
|
output=""
|
|
130
|
+
skipped=""
|
|
124
131
|
while IFS= read -r check; do
|
|
125
132
|
[ -z "$check" ] && continue
|
|
126
|
-
out="$(eval "$check" 2>&1)"
|
|
133
|
+
out="$(eval "$check" 2>&1)"
|
|
134
|
+
status=$?
|
|
135
|
+
[ "$status" -eq 0 ] && continue
|
|
136
|
+
if [ "$status" -eq "$rt_skip_code" ]; then
|
|
137
|
+
skipped="${skipped}${skipped:+
|
|
138
|
+
}${check}"
|
|
139
|
+
continue
|
|
140
|
+
fi
|
|
127
141
|
failed="$check"
|
|
128
142
|
output="$out"
|
|
129
143
|
break
|
|
@@ -131,6 +145,12 @@ done <<EOF
|
|
|
131
145
|
$(rt_push_checks "$base")
|
|
132
146
|
EOF
|
|
133
147
|
|
|
148
|
+
# Пропущенное называется вслух и тогда, когда набор прошёл: молчание о нём и есть та самая
|
|
149
|
+
# неотличимость, ради которой код заведён. Пуш при этом идёт — отказа здесь нет.
|
|
150
|
+
if [ -z "$failed" ] && [ -n "$skipped" ]; then
|
|
151
|
+
printf 'гейт пуша: набор прошёл, но эти проверки смотреть было не на что:\n%s\n' "$skipped" >&2
|
|
152
|
+
fi
|
|
153
|
+
|
|
134
154
|
[ -z "$failed" ] && exit 0
|
|
135
155
|
|
|
136
156
|
# Хвост вывода, а не весь: у прогонщика он длинный, а нужна причина отказа.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# rt-hook: PreToolUse AskUserQuestion
|
|
3
|
+
# Требует: hooks/grill-gate.sh
|
|
4
|
+
# Половина гарда разговора, стоящая на инструменте вопроса: меню судится до отправки.
|
|
5
|
+
#
|
|
6
|
+
# Своим ресурсом, а не строкой в шапке соседа, потому что инструмент вопроса — единственный
|
|
7
|
+
# способ спросить владельца, и у дерева на нём уже может стоять свой гард со своей причиной
|
|
8
|
+
# отказа. Второй отказ на том же инструменте делает вопрос недоступным вовсе, и дерево
|
|
9
|
+
# отказывается от гарда целиком вместо того, чтобы взять его половину. Разведённые по двум
|
|
10
|
+
# ресурсам, половины отменяются порознь: строка отказа снимает одну и оставляет другую.
|
|
11
|
+
#
|
|
12
|
+
# Судит вызов то же тело, что и завершение хода: требование одно, и расходиться двум его
|
|
13
|
+
# редакциям нельзя. Здесь только объявление события и передача ввода.
|
|
14
|
+
#
|
|
15
|
+
# ОТКАЗ В ПОЛЬЗУ РАБОТЫ: нет соседнего файла — ход РАЗРЕШАЕТСЯ. Половина гарда, потерявшая
|
|
16
|
+
# тело, не имеет права заклинить разговор.
|
|
17
|
+
|
|
18
|
+
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
|
|
19
|
+
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
|
|
20
|
+
|
|
21
|
+
rt_hooks_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
22
|
+
[ -f "$rt_hooks_dir/grill-gate.sh" ] || exit 0
|
|
23
|
+
|
|
24
|
+
rt_hook_read
|
|
25
|
+
printf '%s' "$RT_HOOK_INPUT" | "$rt_hooks_dir/grill-gate.sh"
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# rt-hook: PreToolUse AskUserQuestion
|
|
3
2
|
# Требует: hooks/deny-tail.sh
|
|
4
3
|
# rt-hook: Stop
|
|
5
4
|
# Гард разговора: вопрос владельцу не задаётся, пока за этот же ход не читались законы и
|
|
6
|
-
# правила.
|
|
5
|
+
# правила. Судит два события, и это не дублирование; второе объявлено соседним ресурсом
|
|
6
|
+
# `hooks/grill-gate-ask.sh`, который отдаёт вызов сюда.
|
|
7
7
|
#
|
|
8
8
|
# Зачем именно так. Требование «правила читаются до разговора» исполнимо ровно до отправки
|
|
9
9
|
# вопроса. Проверка на завершении хода отбивает задним числом: к моменту отказа вопрос уже у
|
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
# Одним этим перехватом дыра не закрывается: вопрос чаще задаётся прозой, и ровно так был задан
|
|
14
14
|
# тот, из-за которого гард заведён. Прозаический вопрос инструментом не является, и поймать его
|
|
15
15
|
# можно только на завершении хода — событие получает путь к записи хода и видит его целиком.
|
|
16
|
-
# Отсюда два события: меню ловится до отправки, проза — после.
|
|
16
|
+
# Отсюда два события: меню ловится до отправки, проза — после. Объявлены они разными ресурсами
|
|
17
|
+
# затем, чтобы дерево, у которого инструмент вопроса занят своим гардом, могло взять половину, а
|
|
18
|
+
# не отказаться от требования целиком.
|
|
17
19
|
#
|
|
18
20
|
# Чтением правил считается любой из трёх путей: загрузка правила, чтение файла законов или
|
|
19
21
|
# правил, поиск по ним. Требовать именно загрузку значило бы гнать на неё там, где хватило
|
|
@@ -57,13 +59,18 @@ done
|
|
|
57
59
|
laws_dir="${RT_LAWS_DIR-docs/constitution}"
|
|
58
60
|
rules_dir="${RT_RULES_DIR-.claude/skills}"
|
|
59
61
|
specs_dir="${RT_SPECS_DIR-docs/specs}"
|
|
62
|
+
# Замысел эпика и описание прошлого читаются наравне с законами: решение, связывающее задачи
|
|
63
|
+
# эпика, лежит именно там. Прочитавший замысел получал отказ наравне с не читавшим ничего, и
|
|
64
|
+
# снимался тот отказ поиском по трём каталогам, среди которых нужного не было.
|
|
65
|
+
plans_dir="${RT_PLANS_DIR-docs/plans}"
|
|
66
|
+
archive_dir="${RT_ARCHIVE_DIR-docs/archive}"
|
|
60
67
|
|
|
61
68
|
# Дерево, у которого нет ни законов, ни правил, требования не получает: читать нечего.
|
|
62
69
|
[ -z "$laws_dir" ] && [ -z "$rules_dir" ] && exit 0
|
|
63
70
|
|
|
64
71
|
# Образец, по которому вызов инструмента считается чтением правил. Каталоги идут в него как
|
|
65
72
|
# есть: точка в `.claude` совпадает с любым знаком и лишнего сюда не приводит.
|
|
66
|
-
read_re="$(printf '%s' "$laws_dir|$rules_dir|$specs_dir" | sed 's/^|*//; s/|*$//; s/||*/|/g')"
|
|
73
|
+
read_re="$(printf '%s' "$laws_dir|$rules_dir|$specs_dir|$plans_dir|$archive_dir" | sed 's/^|*//; s/|*$//; s/||*/|/g')"
|
|
67
74
|
[ -z "$read_re" ] && exit 0
|
|
68
75
|
|
|
69
76
|
# Ход — это всё, что записано после последнего настоящего ввода владельца. Ответ инструмента
|
|
@@ -106,7 +113,7 @@ fi
|
|
|
106
113
|
|
|
107
114
|
reason="$head Вопрос, ответ на который уже записан, владельцу не задаётся — правило ведения работы. Прогони поиск по словам темы и ответь по найденному; спрашивай только то, что документацией не покрыто:
|
|
108
115
|
|
|
109
|
-
grep -rn -i \"<слово темы>\" $laws_dir $rules_dir $specs_dir
|
|
116
|
+
grep -rn -i \"<слово темы>\" $laws_dir $rules_dir $specs_dir $plans_dir $archive_dir
|
|
110
117
|
|
|
111
118
|
Гард судит один ход: следующий заход не отбивается."
|
|
112
119
|
|
|
@@ -3,11 +3,15 @@
|
|
|
3
3
|
# Требует: hooks/profile-check.sh
|
|
4
4
|
# Линтер по следам правки. PostToolUse.
|
|
5
5
|
#
|
|
6
|
-
# Два входа. Правка файла — линтуется один изменённый файл.
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
6
|
+
# Два входа. Правка файла — линтуется один изменённый файл. Команда оболочки — линтуется то,
|
|
7
|
+
# что она записала: перенос меняет либу, а вместе с ней и границы, и импорт, законный на прежнем
|
|
8
|
+
# месте, на новом уже запрещён; запись перенаправлением, дозаписью или интерпретатором меняет
|
|
9
|
+
# сам файл, и линтера он прежде не получал вовсе. Ровно так запрещённый импорт уезжает в общую
|
|
10
|
+
# либу молча, а правленая проверка выходит из-под форматтера: правило сработало бы — но его
|
|
11
|
+
# никто не запустил.
|
|
12
|
+
#
|
|
13
|
+
# Что команда пишет и куда, знает профиль дерева, и знает он это одним способом для всех: тот
|
|
14
|
+
# же признак читает гейт правил.
|
|
11
15
|
#
|
|
12
16
|
# Полный набор гоняет гард на пуше, но это конец работы: к моменту, когда правило срабатывает,
|
|
13
17
|
# поверх нарушения лежит десяток правок, и разбор превращается в археологию. Здесь тот же
|
|
@@ -19,6 +23,8 @@
|
|
|
19
23
|
#
|
|
20
24
|
# Что здесь чем зовётся, знает профиль дерева:
|
|
21
25
|
# rt_lint_for — чем линтуется этот файл;
|
|
26
|
+
# rt_shell_writes — пишет ли эта команда;
|
|
27
|
+
# rt_shell_paths — какие пути она записала;
|
|
22
28
|
# rt_is_app_code — где лежит код, к которому линтеры вообще относятся;
|
|
23
29
|
# RT_LINT_SKIP_RE — что из него исключено;
|
|
24
30
|
# rt_push_checks — что гоняет гейт пуша. По нему же решается, догонит ли замечание позже:
|
|
@@ -46,14 +52,12 @@ case "$tool" in
|
|
|
46
52
|
*) exit 0 ;;
|
|
47
53
|
esac
|
|
48
54
|
|
|
49
|
-
# Отсев до всякой работы: хук висит на каждой команде оболочки, а
|
|
55
|
+
# Отсев до всякой работы: хук висит на каждой команде оболочки, а пишет из них меньшинство.
|
|
56
|
+
# Пустая команда отсеивается здесь же — признак записи у неё спрашивать не о чем.
|
|
50
57
|
command_text=""
|
|
51
58
|
if [ "$mode" = "move" ]; then
|
|
52
59
|
command_text="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null)"
|
|
53
|
-
|
|
54
|
-
*"git mv "*) ;;
|
|
55
|
-
*) exit 0 ;;
|
|
56
|
-
esac
|
|
60
|
+
[ -z "$command_text" ] && exit 0
|
|
57
61
|
fi
|
|
58
62
|
|
|
59
63
|
workdir="$(rt_hook_cwd)"
|
|
@@ -75,6 +79,15 @@ done
|
|
|
75
79
|
command -v rt_needs >/dev/null 2>&1 || rt_needs() { command -v "$1" >/dev/null 2>&1; }
|
|
76
80
|
rt_needs rt_lint_for lint-after-edit || exit 0
|
|
77
81
|
|
|
82
|
+
# Признак записи спрашивается после профиля: до него функций ещё нет. Дерево, которое признака
|
|
83
|
+
# не объявило, остаётся при прежнем поведении — разбирается один перенос.
|
|
84
|
+
if [ "$mode" = "move" ] && rt_needs rt_shell_writes lint-after-edit; then
|
|
85
|
+
case "$command_text" in
|
|
86
|
+
*"git mv "*) ;;
|
|
87
|
+
*) rt_shell_writes "$command_text" || exit 0 ;;
|
|
88
|
+
esac
|
|
89
|
+
fi
|
|
90
|
+
|
|
78
91
|
# --- какие файлы проверяем -------------------------------------------------------------
|
|
79
92
|
|
|
80
93
|
# Пути назначения всех переносов в команде. Команда бывает составной, поэтому режется по
|
|
@@ -136,7 +149,17 @@ if [ "$mode" = "edit" ]; then
|
|
|
136
149
|
esac
|
|
137
150
|
candidates="$(expand "$path")"
|
|
138
151
|
else
|
|
139
|
-
|
|
152
|
+
# Перенос разворачивает каталог: он двигает целые слои. Запись — нет: пишут всегда в файл,
|
|
153
|
+
# а каталог в команде записи — это рабочий каталог, и развёрнутый он отдаёт линтеру половину
|
|
154
|
+
# дерева. Поэтому у записанных путей берутся только существующие файлы.
|
|
155
|
+
written=""
|
|
156
|
+
if rt_needs rt_shell_paths lint-after-edit; then
|
|
157
|
+
written="$(rt_shell_paths "$command_text" 2>/dev/null | while IFS= read -r path; do
|
|
158
|
+
[ -f "$path" ] && printf '%s\n' "$path"
|
|
159
|
+
done)"
|
|
160
|
+
fi
|
|
161
|
+
candidates="$( { collect_moved "$command_text" | while IFS= read -r moved; do expand "$moved"; done
|
|
162
|
+
printf '%s\n' "$written"; } | sort -u)"
|
|
140
163
|
fi
|
|
141
164
|
|
|
142
165
|
[ -z "$candidates" ] && exit 0
|
|
@@ -214,11 +237,16 @@ else
|
|
|
214
237
|
tail_line="Почини их сейчас: этот линтер в гейт пуша не входит, и отложенное замечание уедет в главную ветку молча."
|
|
215
238
|
fi
|
|
216
239
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
240
|
+
case "$mode:$command_text" in
|
|
241
|
+
# Перенос назван отдельно: замечание после него объясняется не правкой файла, а сменой его
|
|
242
|
+
# места, и без этой строки читатель ищет промах в тексте, которого никто не менял.
|
|
243
|
+
move:*"git mv "*)
|
|
244
|
+
head_line="ЛИНТЕР (${linters}) НАШЁЛ ЗАМЕЧАНИЯ ПОСЛЕ ПЕРЕНОСА. Перенос меняет либу, а вместе с ней границы: импорт, законный на прежнем месте, на новом может быть запрещён." ;;
|
|
245
|
+
move:*)
|
|
246
|
+
head_line="ЛИНТЕР (${linters}) НАШЁЛ ЗАМЕЧАНИЯ ПОСЛЕ ЗАПИСИ КОМАНДОЙ:" ;;
|
|
247
|
+
*)
|
|
248
|
+
head_line="ЛИНТЕР (${linters}) НАШЁЛ ЗАМЕЧАНИЯ:" ;;
|
|
249
|
+
esac
|
|
222
250
|
|
|
223
251
|
ctx="${head_line}
|
|
224
252
|
${report}
|
|
@@ -82,11 +82,27 @@ verdict="$(tail -n 400 "$transcript" 2>/dev/null | jq -s -r --arg asked "$asked_
|
|
|
82
82
|
|
|
83
83
|
[ "$verdict" = "owe" ] || exit 0
|
|
84
84
|
|
|
85
|
+
# Команда отправки называется той, которая в этом дереве исполняется. Дерево, поставившее пакет
|
|
86
|
+
# зависимостью, зовёт бинарь из зависимостей; дерево, где пакет живёт исходниками, бинаря не
|
|
87
|
+
# имеет вовсе — там зовут собранный bin. Названная наугад команда стоит исполнителю хода: отказ
|
|
88
|
+
# читается как указание, и вызов `npx agent-kit` отвечает в таком дереве отказом установки.
|
|
89
|
+
root="${CLAUDE_PROJECT_DIR:-.}"
|
|
90
|
+
if [ -x "$root/node_modules/.bin/agent-kit" ]; then
|
|
91
|
+
propose_cmd="npx agent-kit propose"
|
|
92
|
+
else
|
|
93
|
+
built="$(ls "$root"/dist/*/bin/agent-kit.js 2>/dev/null | head -1)"
|
|
94
|
+
if [ -n "$built" ]; then
|
|
95
|
+
propose_cmd="node ${built#"$root"/} propose"
|
|
96
|
+
else
|
|
97
|
+
propose_cmd="npx agent-kit propose"
|
|
98
|
+
fi
|
|
99
|
+
fi
|
|
100
|
+
|
|
85
101
|
reason="BLOCKED by proposal-guard: владелец сказал завести или отправить предложение слою правил, а отправки в этом ходе не было. Написанное и не отправленное лежит в дереве неотличимо от отправленного: своей записи в слое правил у него нет, и владелец читает работу сделанной, пока не спросит прямо.
|
|
86
102
|
|
|
87
103
|
Предложение пишется файлом в \`$proposals_dir/\` и уезжает в тот же ход:
|
|
88
104
|
|
|
89
|
-
|
|
105
|
+
$propose_cmd
|
|
90
106
|
|
|
91
107
|
Сухой прогон отправкой не является: он показывает, что уехало бы, и следа наружу не оставляет. Отправка пишет отметки в файлы предложений и делает дерево грязным — при открытом PR они ложатся вторым коммитом в ту же ветку, и это их место, а не повод отложить.
|
|
92
108
|
|
|
@@ -22,6 +22,13 @@
|
|
|
22
22
|
# как два разных требования.
|
|
23
23
|
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
|
|
24
24
|
|
|
25
|
+
# Собранное дерево кодом не бывает: путь к артефакту приходит из команды, которая его запускает.
|
|
26
|
+
# Карта гейта такой путь уже пропускает, и слои обязаны молчать вместе с ней — иначе выход из
|
|
27
|
+
# карты снимает одно требование и оставляет полтора десятка других.
|
|
28
|
+
case "$target" in
|
|
29
|
+
*/node_modules/* | */dist/* | */build/* | */.nx/* | */coverage/*) return 0 ;;
|
|
30
|
+
esac
|
|
31
|
+
|
|
25
32
|
rt_layer_add() {
|
|
26
33
|
case " $req " in
|
|
27
34
|
*" $1 "*) return 0 ;;
|
|
@@ -151,6 +151,20 @@ for name in $req; do
|
|
|
151
151
|
break
|
|
152
152
|
done
|
|
153
153
|
[ -z "$want" ] && exit 0
|
|
154
|
+
|
|
155
|
+
# Что потребуется дальше по этой же команде. Требуется по-прежнему одно правило за раз, но
|
|
156
|
+
# длину пути заход видит с первого отказа: на сквозной правке отказы идут по одному на вызов и
|
|
157
|
+
# читаются как разные требования — тринадцать подряд за одну задачу, и каждый стоил хода.
|
|
158
|
+
ahead=""
|
|
159
|
+
for name in $req; do
|
|
160
|
+
[ "$name" = "$want" ] && continue
|
|
161
|
+
[ -f "$root/$rules_dir/${name}/SKILL.md" ] || continue
|
|
162
|
+
if [ -f "$loaded" ] && grep -qE "^([^:]*:)?$(printf '%s' "$name" | sed 's/[][\.*^$/]/\\&/g')$" "$loaded" 2>/dev/null; then
|
|
163
|
+
continue
|
|
164
|
+
fi
|
|
165
|
+
ahead="${ahead}${ahead:+, }${name}"
|
|
166
|
+
done
|
|
167
|
+
|
|
154
168
|
req="$want"
|
|
155
169
|
|
|
156
170
|
# Отбитие — наблюдение: правило, которое приходится требовать чаще прочего, и род правки, на
|
|
@@ -197,6 +211,21 @@ ${article}
|
|
|
197
211
|
fi
|
|
198
212
|
fi
|
|
199
213
|
|
|
214
|
+
# Что потребуется дальше по этой же команде. Строка стоит после всех веток текста: их три, и
|
|
215
|
+
# каждая переписывает причину целиком.
|
|
216
|
+
[ -n "$ahead" ] && reason="${reason} Дальше по этой команде потребуются: ${ahead}."
|
|
217
|
+
|
|
218
|
+
# Спутник зовётся отдельным предложением и ко всем видам отказа разом. Прежде он был назван
|
|
219
|
+
# только запасным ходом — «если инструмент такого имени не знает», — и читатель, у которого имя
|
|
220
|
+
# известно, до него не доходил вовсе. Цена этого: правило называет способ, а спутник рядом
|
|
221
|
+
# объявляет его здесь невозможным; работа, сделанная способом из правила, кончилась результатом,
|
|
222
|
+
# которого для её заказчика не существовало, а разделы спутника за тот заход не открывались ни
|
|
223
|
+
# разу.
|
|
224
|
+
companion="$rules_dir/${req}/implementation.md"
|
|
225
|
+
[ -f "$root/$companion" ] && reason="${reason}
|
|
226
|
+
|
|
227
|
+
Спутник правила — ${companion} — читается вместе с ним: правило говорит, что должно быть верно, а спутник — чем это верно здесь и что здесь названо невозможным. Инструментом он не грузится, его читают файлом."
|
|
228
|
+
|
|
200
229
|
# Общий хвост отказа: два законных хода и законная форма обхода, если она у отказа есть.
|
|
201
230
|
# Файл может быть не разложен — тогда хвоста нет, а причина отказа остаётся прежней.
|
|
202
231
|
# shellcheck disable=SC1090
|
|
@@ -58,6 +58,16 @@ if [ ! -d "$DIR" ]; then
|
|
|
58
58
|
printf 'Ветка `%s` названа задачей, а `%s/` нет: ход работы записывать некуда,\n' "$branch" "$DIR"
|
|
59
59
|
printf 'и следующий заход начнёт с расспросов владельца.\n\n'
|
|
60
60
|
printf 'Собрать с образца:\n\n cp -r %s/_template %s\n\n' "$TASKS_DIR" "$DIR"
|
|
61
|
+
# Папка бывает названа не именем ветки: этапы одной большой задачи идут отдельными
|
|
62
|
+
# ветками при одной общей папке. Названная поимённо папка — единственное, по чему
|
|
63
|
+
# заход её найдёт; иначе он читает отказ как «записей нет» и отвечает владельцу из
|
|
64
|
+
# кода, минуя всё, что в этих записях решено.
|
|
65
|
+
others="$(find "$TASKS_DIR" -mindepth 1 -maxdepth 1 -type d ! -name '_template' 2>/dev/null | sort)"
|
|
66
|
+
if [ -n "$others" ]; then
|
|
67
|
+
printf 'В каталоге задач при этом лежит:\n\n%s\n\n' "$others"
|
|
68
|
+
printf 'Этапы одной задачи идут отдельными ветками при общей папке — прежде чем\n'
|
|
69
|
+
printf 'считать, что записей нет, смотрят в названные.\n\n'
|
|
70
|
+
fi
|
|
61
71
|
# О соседнем ресурсе — условно и по имени: пакет не знает, разложен ли он здесь,
|
|
62
72
|
# а сказанное безусловно приходит в контекст каждой сессии и врёт про дерево тем
|
|
63
73
|
# увереннее, что печатает это сам инструмент.
|