@rt-tools/agent-kit 0.9.0 → 0.10.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 +21 -7
- package/assets/agents/conscience.md +58 -0
- package/assets/agents/prose-editor.md +44 -0
- package/assets/agents/strict-teacher.md +59 -0
- package/assets/checks/board.github.mjs +56 -1
- package/assets/checks/check-board.github.mjs +24 -0
- package/assets/checks/check-doc-paths.mjs +4 -15
- package/assets/checks/check-dupes.mjs +5 -6
- package/assets/checks/check-file-size.mjs +6 -20
- package/assets/checks/check-prose-style.mjs +137 -0
- package/assets/checks/check-reuse.mjs +5 -5
- package/assets/checks/check-styles.mjs +5 -5
- package/assets/checks/lib-common.mjs +3 -3
- package/assets/checks/rt-kit-checks.config.mjs +85 -1
- package/assets/defaults/project.sh +50 -0
- package/assets/docs/GLOSSARY.md +17 -15
- package/assets/hooks/browser-guard-device-id.sh +9 -1
- package/assets/hooks/browser-guard-no-asking.sh +9 -1
- package/assets/hooks/browser-guard-no-listing.sh +9 -1
- package/assets/hooks/browser-guard-no-other-drivers.sh +7 -1
- package/assets/hooks/browser-guard-require-select.sh +11 -2
- package/assets/hooks/claim-guard.sh +113 -0
- package/assets/hooks/conscience-guard.sh +98 -0
- package/assets/hooks/deny-tail.sh +32 -0
- package/assets/hooks/dev-server-guard.sh +8 -2
- package/assets/hooks/docs-guard.sh +14 -2
- package/assets/hooks/exam-guard.sh +121 -0
- package/assets/hooks/git-guard-delivery-signature.sh +75 -0
- package/assets/hooks/git-guard-delivery.sh +154 -68
- package/assets/hooks/git-guard-main.sh +12 -0
- package/assets/hooks/git-guard-push-tests.sh +49 -2
- package/assets/hooks/grill-gate.sh +12 -0
- package/assets/hooks/handoff-entry-guard.sh +71 -0
- package/assets/hooks/postmortem-guard.sh +12 -0
- package/assets/hooks/proposal-guard.sh +12 -0
- package/assets/hooks/prose-style-guard.sh +73 -0
- package/assets/hooks/qa-dataid-guard.sh +12 -1
- package/assets/hooks/rerun-guard.sh +86 -0
- package/assets/hooks/reuse-first-guard.sh +12 -1
- package/assets/hooks/roles.sh +32 -0
- package/assets/hooks/skill-gate.sh +12 -0
- package/assets/hooks/sql-guard-write.sh +2 -1
- package/assets/hooks/sql-guard.sh +12 -1
- package/assets/hooks/task-flow-guard.sh +71 -7
- package/assets/hooks/turn-exit-guard.sh +189 -0
- package/assets/hooks/waiting-turn-guard.sh +12 -0
- package/assets/hooks/window-fill-guard.sh +12 -1
- package/assets/patterns/task-flow-close.md +8 -8
- package/assets/patterns/task-flow-handoff.md +1 -1
- package/assets/patterns/task-flow-resume.md +30 -4
- package/assets/patterns/task-flow-start.md +15 -6
- package/assets/rules/git-workflow.azure.md +36 -9
- package/assets/rules/git-workflow.github.md +74 -9
- package/assets/rules/git-workflow.gitlab.md +40 -12
- package/assets/rules/task-flow.md +123 -71
- package/assets/rules/testing.md +15 -1
- package/assets/samples/tasks/_template/plan.md +4 -1
- package/assets/samples/tasks/_template/progress.md +1 -0
- package/assets/skills/agent-kit.md +33 -0
- package/assets/templates/project.sh +16 -0
- package/bin/agent-kit.d.ts.map +1 -1
- package/bin/agent-kit.js +4 -2
- package/bin/agent-kit.js.map +1 -1
- package/lib/config.d.ts +8 -0
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +1 -0
- package/lib/config.js.map +1 -1
- package/lib/enroll.d.ts +9 -1
- package/lib/enroll.d.ts.map +1 -1
- package/lib/enroll.js +65 -18
- package/lib/enroll.js.map +1 -1
- package/package.json +1 -1
- package/rt-tools-agent-kit-0.10.0.tgz +0 -0
- package/rt-tools-agent-kit-0.9.0.tgz +0 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# rt-hook: PreToolUse Edit|Write|MultiEdit
|
|
3
|
+
# Требует: checks/check-prose-style.mjs, hooks/deny-tail.sh
|
|
4
|
+
# Гард слога: канцелярит и слова, которых в этом дереве не пишут, не уезжают в файл.
|
|
5
|
+
#
|
|
6
|
+
# Правило о текстах требует простых слов, а держалось это памятью того, кто пишет: ни одна
|
|
7
|
+
# формулировочная договорённость не проверялась. Владелец читает написанное и видит машинный
|
|
8
|
+
# слог там, где договорённость требует человеческого.
|
|
9
|
+
#
|
|
10
|
+
# Судится только новый текст правки, а не файл целиком: накопленное чинится отдельной работой, и
|
|
11
|
+
# отбивать за него правку соседней строки — значит сделать гард обходимым по необходимости.
|
|
12
|
+
#
|
|
13
|
+
# FAIL-OPEN: нет узла, нет проверки, чужой инструмент, не `.md` → пропуск.
|
|
14
|
+
|
|
15
|
+
input="$(cat 2>/dev/null)"
|
|
16
|
+
[ -z "$input" ] && exit 0
|
|
17
|
+
command -v jq >/dev/null 2>&1 || exit 0
|
|
18
|
+
command -v node >/dev/null 2>&1 || exit 0
|
|
19
|
+
|
|
20
|
+
tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
|
|
21
|
+
case "$tool" in
|
|
22
|
+
Edit | Write | MultiEdit) ;;
|
|
23
|
+
*) exit 0 ;;
|
|
24
|
+
esac
|
|
25
|
+
|
|
26
|
+
path="$(printf '%s' "$input" | jq -r '.tool_input.file_path // empty' 2>/dev/null)"
|
|
27
|
+
case "$path" in
|
|
28
|
+
*.md) ;;
|
|
29
|
+
*) exit 0 ;;
|
|
30
|
+
esac
|
|
31
|
+
|
|
32
|
+
# Описание прошлого и папки задач не судятся: архив не правится вовсе, а ход работы пишется
|
|
33
|
+
# наспех и живёт до слияния.
|
|
34
|
+
case "$path" in
|
|
35
|
+
*/docs/archive/* | */docs/tasks/*) exit 0 ;;
|
|
36
|
+
esac
|
|
37
|
+
|
|
38
|
+
added="$(printf '%s' "$input" | jq -r '.tool_input.new_string // .tool_input.content // ([.tool_input.edits[]?.new_string] | join("\n")) // empty' 2>/dev/null)"
|
|
39
|
+
[ -z "$added" ] && exit 0
|
|
40
|
+
|
|
41
|
+
rt_hooks_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
42
|
+
check=""
|
|
43
|
+
for candidate in "$rt_hooks_dir/../checks/check-prose-style.mjs" "$rt_hooks_dir/../rt-kit/checks/check-prose-style.mjs" "${CLAUDE_PROJECT_DIR:-.}/tools/check-prose-style.mjs"; do
|
|
44
|
+
[ -f "$candidate" ] && check="$candidate" && break
|
|
45
|
+
done
|
|
46
|
+
[ -z "$check" ] && exit 0
|
|
47
|
+
|
|
48
|
+
tmp="$(mktemp -t prose)" || exit 0
|
|
49
|
+
printf '%s\n' "$added" > "$tmp"
|
|
50
|
+
found="$(node "$check" "$tmp" 2>&1 | grep -- '—' | sed 's|.*proba*[^:]*:| строка |' | head -8)"
|
|
51
|
+
rm -f "$tmp"
|
|
52
|
+
[ -z "$found" ] && exit 0
|
|
53
|
+
|
|
54
|
+
reason="BLOCKED by prose-style-guard: в новом тексте канцелярит или слово, которого в этом дереве не пишут.
|
|
55
|
+
|
|
56
|
+
${found}
|
|
57
|
+
|
|
58
|
+
Правь текст, а не обходи находку: замена названа у каждой. Слог — правило о текстах, и проверка видит перечисленные признаки, а не стиль вообще: чистый по ней абзац может быть плохим, но грязный плохой точно."
|
|
59
|
+
|
|
60
|
+
# Общий хвост отказа: два законных хода и законная форма обхода, если она у отказа есть.
|
|
61
|
+
# Файл может быть не разложен — тогда хвоста нет, а причина отказа остаётся прежней.
|
|
62
|
+
# shellcheck disable=SC1090
|
|
63
|
+
[ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" ] \
|
|
64
|
+
&& . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" 2>/dev/null
|
|
65
|
+
command -v rt_deny_tail >/dev/null 2>&1 || rt_deny_tail() { :; }
|
|
66
|
+
deny_tail_text="$(rt_deny_tail "")"
|
|
67
|
+
[ -n "$deny_tail_text" ] && reason="${reason}
|
|
68
|
+
|
|
69
|
+
${deny_tail_text}"
|
|
70
|
+
|
|
71
|
+
jq -n --arg r "$reason" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$r}}' 2>/dev/null \
|
|
72
|
+
|| printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"prose-style-guard: канцелярит в новом тексте."}}\n'
|
|
73
|
+
exit 0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# rt-hook: PreToolUse Edit|Write|MultiEdit|mcp__webstorm__create_new_file
|
|
3
|
-
# Требует: hooks/profile-check.sh
|
|
3
|
+
# Требует: hooks/profile-check.sh, hooks/deny-tail.sh
|
|
4
4
|
# Гард якоря для спек. PreToolUse на правке разметки.
|
|
5
5
|
#
|
|
6
6
|
# Спеки адресуют элементы только через этот атрибут. Классы оформления меняются вместе с
|
|
@@ -122,6 +122,17 @@ missing="$(printf '%s' "$added" | RT_QA_DECORATIVE="$decorative" RT_QA_COMPONENT
|
|
|
122
122
|
|
|
123
123
|
reason="BLOCKED: интерактивные элементы без якоря для спек в ${path##*/}: ${missing}. Спеки адресуют элементы только через этот атрибут: классы оформления меняются вместе с вёрсткой, а поиск по роли и тексту ломается на переводах — оба вида селекторов делают сквозные спеки хрупкими. Проставь якорь через дефис по смыслу элемента; повторяющиеся элементы списка носят ОДИН якорь и различаются атрибутами данных. Если элемент чисто декоративный и спека его никогда не тронет — поставь атрибут пропуска НА САМ ЭТОТ ТЕГ, соседние элементы правки при этом продолжают проверяться."
|
|
124
124
|
|
|
125
|
+
# Общий хвост отказа: два законных хода и законная форма обхода, если она у отказа есть.
|
|
126
|
+
# Файл может быть не разложен — тогда хвоста нет, а причина отказа остаётся прежней.
|
|
127
|
+
# shellcheck disable=SC1090
|
|
128
|
+
[ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" ] \
|
|
129
|
+
&& . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" 2>/dev/null
|
|
130
|
+
command -v rt_deny_tail >/dev/null 2>&1 || rt_deny_tail() { :; }
|
|
131
|
+
deny_tail_text="$(rt_deny_tail "атрибут пропуска на самом декоративном теге")"
|
|
132
|
+
[ -n "$deny_tail_text" ] && reason="${reason}
|
|
133
|
+
|
|
134
|
+
${deny_tail_text}"
|
|
135
|
+
|
|
125
136
|
jq -n --arg r "$reason" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$r}}' 2>/dev/null \
|
|
126
137
|
|| printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Интерактивный элемент без якоря для спек."}}\n'
|
|
127
138
|
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# rt-hook: PreToolUse Bash|mcp__webstorm__execute_terminal_command|mcp__webstorm__execute_tool
|
|
3
|
+
# Требует: hooks/deny-tail.sh
|
|
4
|
+
# Гард перезапуска прогона: упавшее задание не перезапускается, пока его журнал не прочитан.
|
|
5
|
+
#
|
|
6
|
+
# Красное на прогоне бывает двух родов, и со стороны списка они выглядят одинаково: отказ
|
|
7
|
+
# хостинга на шаге подготовки — раннер не скачал действие, ответ `429` — и дефект самой ветки.
|
|
8
|
+
# Первый лечится перезапуском, второй перезапуском не лечится вовсе: та же ветка падает тем же
|
|
9
|
+
# местом, и круг повторяется, пока кто-нибудь не откроет журнал. Три прогона одного дня так и
|
|
10
|
+
# перезапускались подряд.
|
|
11
|
+
#
|
|
12
|
+
# Гард судит ПОРЯДОК, а не причину падения: журнал раньше перезапуска. Что в журнале написано,
|
|
13
|
+
# он не читает и читать не может — красное по существу судит человек.
|
|
14
|
+
#
|
|
15
|
+
# Признак чтения — вызов за тот же ход, показывающий журнал этого задания: номер задания в
|
|
16
|
+
# команде тот же, что и в перезапуске. Номер сверяется, потому что прочитанный журнал соседнего
|
|
17
|
+
# задания о нашем не говорит ничего, а по списку прогонов они стоят рядом.
|
|
18
|
+
#
|
|
19
|
+
# ОТКАЗ В ПОЛЬЗУ РАБОТЫ: нет `jq`, нет записи хода, номер задания в команде не назван, вызов не
|
|
20
|
+
# похож на перезапуск — пропуск. Гард без номера судить не берётся: перезапуск последнего
|
|
21
|
+
# упавшего прогона зовут и без него, а угадывать, о каком задании речь, значит отбивать наугад.
|
|
22
|
+
|
|
23
|
+
input="$(cat 2>/dev/null)"
|
|
24
|
+
[ -z "$input" ] && exit 0
|
|
25
|
+
command -v jq >/dev/null 2>&1 || exit 0
|
|
26
|
+
|
|
27
|
+
tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
|
|
28
|
+
case "$tool" in
|
|
29
|
+
# Терминал среды и универсальный исполнитель кладут команду в то же поле.
|
|
30
|
+
Bash | mcp__webstorm__execute_terminal_command | mcp__webstorm__execute_tool) ;;
|
|
31
|
+
*) exit 0 ;;
|
|
32
|
+
esac
|
|
33
|
+
|
|
34
|
+
cmd="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null)"
|
|
35
|
+
[ -z "$cmd" ] && exit 0
|
|
36
|
+
|
|
37
|
+
# Профиль дерева: сперва умолчание пакета, поверх него — надстройка проекта, если она есть.
|
|
38
|
+
# Имя клиента хостинга дерево называет само: у каждого вида оно своё, а угаданное не совпадает
|
|
39
|
+
# ни с чем.
|
|
40
|
+
rt_hooks_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
41
|
+
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
|
|
42
|
+
# shellcheck disable=SC1090
|
|
43
|
+
[ -f "$profile" ] && . "$profile" 2>/dev/null
|
|
44
|
+
done
|
|
45
|
+
|
|
46
|
+
# Вызов перезапуска: слово перезапуска отдельным словом рядом со словом прогона.
|
|
47
|
+
host_cli="${RT_HOST_CLI:-gh}"
|
|
48
|
+
printf '%s' "$cmd" | grep -qE "(^|[;&|(]|&&|\|\|)[[:space:]]*${host_cli}([[:space:]]|$)" || exit 0
|
|
49
|
+
printf '%s' "$cmd" | grep -qE '(^|[[:space:]])(run|workflow)([[:space:]]|$)' || exit 0
|
|
50
|
+
printf '%s' "$cmd" | grep -qE '(^|[[:space:]])rerun([[:space:]]|$)' || exit 0
|
|
51
|
+
|
|
52
|
+
# Номер задания. Не назван — гард молчит: судить не о чем.
|
|
53
|
+
run_id="$(printf '%s' "$cmd" | grep -oE '(^|[[:space:]])[0-9]{6,}([[:space:]]|$)' | tr -d ' ' | head -1)"
|
|
54
|
+
[ -z "$run_id" ] && exit 0
|
|
55
|
+
|
|
56
|
+
transcript="$(printf '%s' "$input" | jq -r '.transcript_path // empty' 2>/dev/null)"
|
|
57
|
+
[ -z "$transcript" ] && exit 0
|
|
58
|
+
[ -f "$transcript" ] || exit 0
|
|
59
|
+
|
|
60
|
+
# Читался ли журнал ЭТОГО задания за тот же ход. Ход — вызовы после последней реплики владельца:
|
|
61
|
+
# журнал, прочитанный вчера, о сегодняшнем состоянии прогона не говорит ничего.
|
|
62
|
+
seen="$(jq -s -r --arg id "$run_id" '
|
|
63
|
+
[.[] | select(.type == "assistant") | (.message.content // [])[] | select(.type == "tool_use")
|
|
64
|
+
| ((.input.command // "") | tostring)] as $used
|
|
65
|
+
| (($used | join("\n")) | test("run[[:space:]]+view[^\\n]*" + $id + "|" + $id + "[^\\n]*--log")) as $read
|
|
66
|
+
| if $read then "читал" else "не-читал" end
|
|
67
|
+
' "$transcript" 2>/dev/null)"
|
|
68
|
+
|
|
69
|
+
[ "$seen" = "читал" ] && exit 0
|
|
70
|
+
|
|
71
|
+
reason="BLOCKED by rerun-guard: перезапуск задания ${run_id} без прочитанного журнала. Красное на прогоне бывает двух родов, и в списке они выглядят одинаково: отказ хостинга на шаге подготовки лечится перезапуском, дефект ветки — не лечится им вовсе, и круг повторяется, пока журнал не открыт. Прочитай журнал этого задания — ${host_cli} run view ${run_id} --log-failed — и повтори вызов. Гард судит порядок, а не причину падения: что в журнале написано, судишь ты."
|
|
72
|
+
|
|
73
|
+
# Общий хвост отказа: два законных хода и законная форма обхода, если она у отказа есть.
|
|
74
|
+
# Файл может быть не разложен — тогда хвоста нет, а причина отказа остаётся прежней.
|
|
75
|
+
# shellcheck disable=SC1090
|
|
76
|
+
[ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" ] \
|
|
77
|
+
&& . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" 2>/dev/null
|
|
78
|
+
command -v rt_deny_tail >/dev/null 2>&1 || rt_deny_tail() { :; }
|
|
79
|
+
deny_tail_text="$(rt_deny_tail "")"
|
|
80
|
+
[ -n "$deny_tail_text" ] && reason="${reason}
|
|
81
|
+
|
|
82
|
+
${deny_tail_text}"
|
|
83
|
+
|
|
84
|
+
jq -n --arg r "$reason" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$r}}' 2>/dev/null \
|
|
85
|
+
|| printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"rerun-guard: журнал задания за этот ход не читался."}}\n'
|
|
86
|
+
exit 0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# rt-hook: PreToolUse Edit|Write|MultiEdit|Bash|mcp__webstorm__create_new_file|mcp__webstorm__execute_terminal_command|mcp__webstorm__execute_tool
|
|
3
|
-
# Требует: hooks/profile-check.sh
|
|
3
|
+
# Требует: hooks/profile-check.sh, hooks/deny-tail.sh
|
|
4
4
|
# Гард «ничего не пишется с нуля». PreToolUse на правке кода и разметки.
|
|
5
5
|
#
|
|
6
6
|
# Линтеры знают правила, но не знают ИНВЕНТАРЬ: линтер стилей поймает сырой цвет, линтер кода —
|
|
@@ -312,6 +312,17 @@ ${found}
|
|
|
312
312
|
Порядок действий: 1) открой готовое — барель кита или базовый класс — и используй его; 2) найди в дереве экран, где этот случай уже собран, и повтори сборку; 3) если готового правда не хватает — расширяй его на месте, у готового, а не клонируй рядом: клон забирает правки на себя и расходится с оригиналом с первой же.
|
|
313
313
|
Свой примитив, своя основа и свои инлайновые стили заводятся только с явного одобрения владельца, и спрашивается это до первого написанного файла. Разовое исключение помечается маркером отступления в той же строке, с объяснением, чего именно нет в готовом. Переименованием файла это не обходится."
|
|
314
314
|
|
|
315
|
+
# Общий хвост отказа: два законных хода и законная форма обхода, если она у отказа есть.
|
|
316
|
+
# Файл может быть не разложен — тогда хвоста нет, а причина отказа остаётся прежней.
|
|
317
|
+
# shellcheck disable=SC1090
|
|
318
|
+
[ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" ] \
|
|
319
|
+
&& . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" 2>/dev/null
|
|
320
|
+
command -v rt_deny_tail >/dev/null 2>&1 || rt_deny_tail() { :; }
|
|
321
|
+
deny_tail_text="$(rt_deny_tail "маркер отступления в той же строке, с объяснением, чего именно нет в готовом")"
|
|
322
|
+
[ -n "$deny_tail_text" ] && reason="${reason}
|
|
323
|
+
|
|
324
|
+
${deny_tail_text}"
|
|
325
|
+
|
|
315
326
|
jq -n --arg r "$reason" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$r}}' 2>/dev/null \
|
|
316
327
|
|| printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Написано своё там, где готовое уже есть."}}\n'
|
|
317
328
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Состояние роли слоя правил. НЕ гард: объявления `rt-hook:` у него нет, к событиям агента он не
|
|
3
|
+
# подключается. Его источают гарды, стоящие при ролях, — тем же приёмом, каким гейт источает
|
|
4
|
+
# карту, а гарды — профиль дерева.
|
|
5
|
+
#
|
|
6
|
+
# Зачем он есть. Роль, при которой стоит гард, зовётся не по усмотрению исполнителя: гард держит
|
|
7
|
+
# работу, пока роль не сказала своё. Выключить её было нечем — отказ от файла роли в настройке
|
|
8
|
+
# убирал роль и оставлял гард, и дереву, которому роль в эту минуту мешает, оставалось вырезать
|
|
9
|
+
# гард из настроек руками. Теперь дерево называет выключенные роли одним списком, а гард при
|
|
10
|
+
# такой роли выходит молча.
|
|
11
|
+
#
|
|
12
|
+
# Выключается обязательность вызова, а не сама роль: файл роли остаётся разложенным, и позвать
|
|
13
|
+
# её руками можно в любую минуту.
|
|
14
|
+
#
|
|
15
|
+
# ОТКАЗ В ПОЛЬЗУ РАБОТЫ наоборот: всё, чего не хватило, читается как «роль включена». Нет `jq`,
|
|
16
|
+
# нет настройки, настройка не разбирается — гард работает, как работал. Сломанное чтение,
|
|
17
|
+
# гасящее роль, выключало бы слой правил молча, и заметить это было бы нечем.
|
|
18
|
+
|
|
19
|
+
# Выключена ли роль. Успех — выключена.
|
|
20
|
+
#
|
|
21
|
+
# rt_role_off strict-teacher && exit 0
|
|
22
|
+
rt_role_off() {
|
|
23
|
+
local role="$1" config
|
|
24
|
+
[ -n "$role" ] || return 1
|
|
25
|
+
command -v jq >/dev/null 2>&1 || return 1
|
|
26
|
+
config="${CLAUDE_PROJECT_DIR:-.}/.claude/rt-kit.json"
|
|
27
|
+
[ -f "$config" ] || return 1
|
|
28
|
+
[ "$(jq -r --arg role "$role" '
|
|
29
|
+
if ((.rolesOff // []) | type) == "array" and (((.rolesOff // []) | index($role)) != null)
|
|
30
|
+
then "off" else "on" end
|
|
31
|
+
' "$config" 2>/dev/null)" = "off" ]
|
|
32
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# rt-hook: PreToolUse Edit|Write|MultiEdit|Bash|mcp__webstorm__create_new_file|mcp__webstorm__execute_terminal_command|mcp__webstorm__execute_tool|mcp__claude-in-chrome__.*
|
|
3
|
+
# Требует: hooks/deny-tail.sh
|
|
3
4
|
# Гейт правил: не даёт править файл, пока не загружено правило, под которое он подпадает.
|
|
4
5
|
#
|
|
5
6
|
# Закон и правило, которых никто не открывает, не действуют. Напоминание в подсказке помогает
|
|
@@ -172,6 +173,17 @@ if [ -n "$law" ]; then
|
|
|
172
173
|
&& reason="Отбито гейтом правил: загрузи правило «${req}» инструментом Skill — оно применяет закон ${law_path} к этому дереву — и повтори действие. ${fallback} Для этой области это происходит один раз за сессию."
|
|
173
174
|
fi
|
|
174
175
|
|
|
176
|
+
# Общий хвост отказа: два законных хода и законная форма обхода, если она у отказа есть.
|
|
177
|
+
# Файл может быть не разложен — тогда хвоста нет, а причина отказа остаётся прежней.
|
|
178
|
+
# shellcheck disable=SC1090
|
|
179
|
+
[ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" ] \
|
|
180
|
+
&& . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" 2>/dev/null
|
|
181
|
+
command -v rt_deny_tail >/dev/null 2>&1 || rt_deny_tail() { :; }
|
|
182
|
+
deny_tail_text="$(rt_deny_tail "")"
|
|
183
|
+
[ -n "$deny_tail_text" ] && reason="${reason}
|
|
184
|
+
|
|
185
|
+
${deny_tail_text}"
|
|
186
|
+
|
|
175
187
|
jq -n --arg r "$reason" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$r}}' 2>/dev/null \
|
|
176
188
|
|| printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Загрузи правило %s и повтори."}}\n' "$req"
|
|
177
189
|
|
|
@@ -67,7 +67,8 @@ verdict() {
|
|
|
67
67
|
if [ -n "$soft" ]; then
|
|
68
68
|
ask "Запрос помечен маркером destructive-ok, но остаётся разрушительным: $1 Подтверди выполнение, если это осознанно."
|
|
69
69
|
fi
|
|
70
|
-
deny "BLOCKED: $1 Адресуй строки по первичному ключу — \`WHERE id IN ('…','…')\`: так затрагивается ровно столько строк, сколько перечислено, и промах виден до выполнения. Удаление по маске (email LIKE '%test%') однажды унесло вместе с тестовыми записями демонстрационные брони владельца. Если адресация по id действительно не подходит — сначала выполни SELECT с тем же
|
|
70
|
+
deny "BLOCKED: $1 Адресуй строки по первичному ключу — \`WHERE id IN ('…','…')\`: так затрагивается ровно столько строк, сколько перечислено, и промах виден до выполнения. Удаление по маске (email LIKE '%test%') однажды унесло вместе с тестовыми записями демонстрационные брони владельца. Если адресация по id действительно не подходит — сначала выполни SELECT с тем же условием и покажи пользователю, что попадает под удаление." \
|
|
71
|
+
"маркер destructive-ok при запросе, с объяснением"
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
sql_check_destructive() {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# rt-hook: PreToolUse Bash|mcp__webstorm__execute_sql_query|mcp__webstorm__execute_terminal_command|mcp__webstorm__execute_tool
|
|
3
|
+
# Требует: hooks/deny-tail.sh
|
|
3
4
|
# Гард пишущих запросов к хранилищу. PreToolUse.
|
|
4
5
|
#
|
|
5
6
|
# Правка данных — единственное действие, которое нельзя откатить правкой кода. Удаление по
|
|
@@ -69,8 +70,18 @@ done
|
|
|
69
70
|
|
|
70
71
|
PROD_DSN="${RT_PROD_DSN:-}"
|
|
71
72
|
|
|
73
|
+
# Общий хвост отказа: два законных хода и законная форма обхода, если она у отказа есть. Файл
|
|
74
|
+
# может быть не разложен — тогда хвоста нет, а причина отказа остаётся прежней.
|
|
75
|
+
# shellcheck disable=SC1090
|
|
76
|
+
[ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" ] \
|
|
77
|
+
&& . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" 2>/dev/null
|
|
78
|
+
command -v rt_deny_tail >/dev/null 2>&1 || rt_deny_tail() { :; }
|
|
79
|
+
|
|
72
80
|
deny() {
|
|
73
|
-
|
|
81
|
+
reason="$1"
|
|
82
|
+
tail_text="$(rt_deny_tail "$2")"
|
|
83
|
+
[ -n "$tail_text" ] && reason="$1 ${tail_text}"
|
|
84
|
+
jq -n --arg r "$reason" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$r}}' 2>/dev/null \
|
|
74
85
|
|| printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Destructive SQL blocked. Address rows by id."}}\n'
|
|
75
86
|
exit 0
|
|
76
87
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# rt-hook: PreToolUse Edit|Write|MultiEdit|Bash|mcp__webstorm__create_new_file|mcp__webstorm__execute_terminal_command|mcp__webstorm__execute_tool
|
|
3
|
-
# Требует: hooks/profile-check.sh
|
|
3
|
+
# Требует: hooks/profile-check.sh, hooks/deny-tail.sh
|
|
4
4
|
# PreToolUse guard for Edit|Write|MultiEdit: код не пишется раньше замысла.
|
|
5
5
|
#
|
|
6
6
|
# Работа идёт много заходов, и между ними исполнитель не помнит ничего. Замысел, лежащий на
|
|
@@ -8,9 +8,14 @@
|
|
|
8
8
|
# закоммичены, PR не открыт, а очередь работ показывает задачу начатой и молчит о том, что
|
|
9
9
|
# внутри неё сделано.
|
|
10
10
|
#
|
|
11
|
-
# Гард требует
|
|
12
|
-
#
|
|
13
|
-
# (решения в
|
|
11
|
+
# Гард требует четыре вещи и ровно их: папку задачи по имени ветки, замысел в ней, объявленное
|
|
12
|
+
# в ходе работы состояние — из тех, в которых код правится, — и названную в замысле
|
|
13
|
+
# договорённость о продукте. Полноту написанного он не судит — это за владельцем (решения в
|
|
14
|
+
# законе `docs/constitution/work-conduct.md`).
|
|
15
|
+
#
|
|
16
|
+
# Состояние судится раньше договорённости и её обхода: обход снимает требование договорённости,
|
|
17
|
+
# а не требование дойти до правки кода. Судится объявленный переход, а не наличие файлов —
|
|
18
|
+
# артефакт, положенный ради снятия отказа, лежит так же, как написанный.
|
|
14
19
|
#
|
|
15
20
|
# Правило целиком — скил `task-flow`.
|
|
16
21
|
#
|
|
@@ -102,9 +107,20 @@ EOF
|
|
|
102
107
|
# Каталог папок задач: у дерева он свой, но имя обычно общее.
|
|
103
108
|
tasks_dir="${RT_TASKS_DIR:-docs/tasks}"
|
|
104
109
|
|
|
110
|
+
# Общий хвост отказа: два законных хода и законная форма обхода, если она у отказа есть. Файл
|
|
111
|
+
# может быть не разложен — тогда хвоста нет, а причина отказа остаётся прежней.
|
|
112
|
+
# shellcheck disable=SC1090
|
|
113
|
+
[ -f "$rt_hooks_dir/deny-tail.sh" ] && . "$rt_hooks_dir/deny-tail.sh"
|
|
114
|
+
command -v rt_deny_tail >/dev/null 2>&1 || rt_deny_tail() { :; }
|
|
115
|
+
|
|
116
|
+
# Отказ: причина первым параметром, законная форма обхода — вторым. Хвост дописывается здесь, а
|
|
117
|
+
# не в каждом тексте: пропущенный в одном месте, он читается как «у этого отказа ходов нет».
|
|
105
118
|
deny() {
|
|
106
|
-
|
|
107
|
-
|
|
119
|
+
reason="$1"
|
|
120
|
+
tail_text="$(rt_deny_tail "$2")"
|
|
121
|
+
[ -n "$tail_text" ] && reason="$1 ${tail_text}"
|
|
122
|
+
jq -n --arg r "$reason" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$r}}' 2>/dev/null \
|
|
123
|
+
|| printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"%s"}}\n' "$reason"
|
|
108
124
|
exit 0
|
|
109
125
|
}
|
|
110
126
|
|
|
@@ -130,6 +146,53 @@ if [ ! -f "$plan" ]; then
|
|
|
130
146
|
deny "BLOCKED by task-flow: нет замысла — '${tasks_dir}/${branch}/plan.md'. Собери папку задачи с образца (cp -r ${tasks_dir}/_template ${tasks_dir}/${branch}) и заполни шапку, след задачи и этапы, затем повтори. Правило — скил task-flow."
|
|
131
147
|
fi
|
|
132
148
|
|
|
149
|
+
# Состояние работы. Артефакт на диске не говорит, дошла ли работа до правки кода: пустой
|
|
150
|
+
# `plan.md`, положенный ради снятия отказа, лежит точно так же, как написанный замысел, и
|
|
151
|
+
# требование снимает сам собой. Единица работы — состояние, а не файл: исполнитель объявляет
|
|
152
|
+
# его строкой в ходе работы, гард судит объявленный переход, а не наличие файлов.
|
|
153
|
+
#
|
|
154
|
+
# Состояние стоит в разделе «Где стоим» и перезаписывается вместе с ним. Имя берётся из
|
|
155
|
+
# перечня — своё имя состоянием не является: перечень называет вход, выход и обязательное
|
|
156
|
+
# действие каждого, и слово вне перечня не говорит ни о чём из трёх.
|
|
157
|
+
progress="$dir/progress.md"
|
|
158
|
+
|
|
159
|
+
if [ ! -f "$progress" ]; then
|
|
160
|
+
deny "BLOCKED by task-flow: нет хода работы — '${tasks_dir}/${branch}/progress.md'. В нём объявляется состояние работы, и без него не видно, дошла ли она до правки кода. Собери папку задачи с образца (cp -r ${tasks_dir}/_template ${tasks_dir}/${branch}), затем повтори. Правило — скил task-flow."
|
|
161
|
+
fi
|
|
162
|
+
|
|
163
|
+
state="$(sed -n 's/^[[:space:]]*[-*][[:space:]]*\*\*Состояние:\*\*[[:space:]]*`\([^`]*\)`.*/\1/p' "$progress" 2>/dev/null | head -1)"
|
|
164
|
+
|
|
165
|
+
# Обязательное действие состояния. Отказ называет его целиком: исполнитель, которому сказано
|
|
166
|
+
# только «не в том состоянии», переписывает строку состояния вместо того, чтобы сделать шаг.
|
|
167
|
+
state_action() {
|
|
168
|
+
case "$1" in
|
|
169
|
+
просьба-не-разобрана) printf '%s' 'разведка по дереву, затем вопросы владельцу' ;;
|
|
170
|
+
разбор-закрыт) printf '%s' 'договорённость о продукте либо названная причина её отсутствия' ;;
|
|
171
|
+
договорённость-записана) printf '%s' 'завести задачу, ветку и папку задачи' ;;
|
|
172
|
+
задача-взята) printf '%s' 'написать замысел' ;;
|
|
173
|
+
замысел-записан) printf '%s' 'делать первый этап' ;;
|
|
174
|
+
папка-разобрана) printf '%s' 'снять черновик и попросить владельца влить' ;;
|
|
175
|
+
влито) printf '%s' 'разбор работы правилами и сверка очереди работ' ;;
|
|
176
|
+
*) printf '%s' '' ;;
|
|
177
|
+
esac
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if [ -z "$state" ]; then
|
|
181
|
+
deny "BLOCKED by task-flow: в '${tasks_dir}/${branch}/progress.md' не объявлено состояние работы. Впиши в раздел «Где стоим» строку '- **Состояние:** \`<имя>\`' — имя из перечня состояний правила, — затем повтори. Код правится в состояниях 'этап-идёт', 'этапы-кончились', 'работа-отдана' и 'разбор-кончился'. Правило — скил task-flow."
|
|
182
|
+
fi
|
|
183
|
+
|
|
184
|
+
case "$state" in
|
|
185
|
+
# Состояния, в которых код приложения правится. Три последних — не про первый заход:
|
|
186
|
+
# прогон бывает красным, а разбор — с замечаниями, и починка идёт в ту же ветку.
|
|
187
|
+
этап-идёт | этапы-кончились | работа-отдана | разбор-кончился) ;;
|
|
188
|
+
просьба-не-разобрана | разбор-закрыт | договорённость-записана | задача-взята | замысел-записан | папка-разобрана | влито)
|
|
189
|
+
deny "BLOCKED by task-flow: в ходе работы объявлено состояние '${state}', а код в нём не правится. Обязательное действие этого состояния — $(state_action "$state"). Дошла работа до правки кода — перепиши строку состояния в '${tasks_dir}/${branch}/progress.md' на '- **Состояние:** \`этап-идёт\`'. Правило — скил task-flow."
|
|
190
|
+
;;
|
|
191
|
+
*)
|
|
192
|
+
deny "BLOCKED by task-flow: в '${tasks_dir}/${branch}/progress.md' объявлено состояние '${state}', а такого в перечне нет. Имя берётся из перечня состояний правила — своё слово не говорит ни о входе, ни о выходе, ни об обязательном действии. Правило — скил task-flow."
|
|
193
|
+
;;
|
|
194
|
+
esac
|
|
195
|
+
|
|
133
196
|
# Строка обхода: поведение не меняется, договорённость о продукте не нужна. Причина обязана
|
|
134
197
|
# стоять — без неё обход становится умолчанием.
|
|
135
198
|
if grep -qE '^\*\*Поведение:\*\*[[:space:]]*не меняется[[:space:]]*—[[:space:]]*\S' "$plan" 2>/dev/null; then
|
|
@@ -139,7 +202,8 @@ fi
|
|
|
139
202
|
draft="$(sed -n 's/^\*\*Драфт:\*\*[[:space:]]*`\([^`]*\)`.*/\1/p' "$plan" 2>/dev/null | head -1)"
|
|
140
203
|
|
|
141
204
|
if [ -z "$draft" ]; then
|
|
142
|
-
deny "BLOCKED by task-flow: в '${tasks_dir}/${branch}/plan.md' не названа договорённость о продукте. Заведи её в docs/specs/<домен>/proposed/<фича>/ и укажи строкой '**Драфт:** \`путь\`'.
|
|
205
|
+
deny "BLOCKED by task-flow: в '${tasks_dir}/${branch}/plan.md' не названа договорённость о продукте. Заведи её в docs/specs/<домен>/proposed/<фича>/ и укажи строкой '**Драфт:** \`путь\`'. Правило — скил task-flow." \
|
|
206
|
+
"строка '**Поведение:** не меняется — <причина владельца>' в замысле; пустая причина не принимается"
|
|
143
207
|
fi
|
|
144
208
|
|
|
145
209
|
case "$draft" in
|