@rt-tools/agent-kit 0.5.0 → 0.5.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 +73 -0
- package/assets/checks/check-doc-paths.mjs +200 -30
- package/assets/checks/check-specs.mjs +42 -7
- package/assets/checks/rt-kit-checks.config.mjs +12 -0
- package/assets/commands/agent-kit-digest.md +83 -0
- package/assets/commands/next-session.md +122 -0
- package/assets/commands/skill-curator.md +33 -1
- package/assets/defaults/gate-map.sh +23 -1
- package/assets/defaults/project.sh +37 -1
- package/assets/docs/GLOSSARY.md +77 -0
- package/assets/hooks/docs-guard.sh +18 -1
- package/assets/hooks/git-guard-delivery.sh +30 -3
- package/assets/hooks/git-guard-main.sh +8 -0
- package/assets/hooks/git-guard-push-tests.sh +8 -1
- package/assets/hooks/grill-gate.sh +38 -16
- package/assets/hooks/lint-after-edit.sh +10 -3
- package/assets/hooks/observe.sh +90 -0
- package/assets/hooks/postmortem-guard.sh +92 -0
- package/assets/hooks/profile-check.sh +43 -0
- package/assets/hooks/qa-dataid-guard.sh +9 -2
- package/assets/hooks/reuse-first-guard.sh +9 -2
- package/assets/hooks/skill-gate.sh +15 -0
- package/assets/hooks/skill-loaded.sh +7 -0
- package/assets/hooks/task-context-load.sh +16 -2
- package/assets/hooks/task-flow-guard.sh +9 -2
- package/assets/hooks/window-fill-guard.sh +157 -0
- package/assets/laws/code-structure.md +10 -0
- package/assets/laws/delivery.md +8 -1
- package/assets/laws/project-documentation.md +9 -0
- package/assets/laws/verifiability.md +9 -0
- package/assets/laws/work-conduct.md +47 -0
- package/assets/patterns/git-workflow-commit.azure.md +16 -12
- package/assets/patterns/git-workflow-commit.github.md +16 -12
- package/assets/patterns/git-workflow-commit.gitlab.md +16 -12
- package/assets/patterns/spec-driven-domain.md +19 -0
- package/assets/patterns/task-flow-close.md +4 -4
- package/assets/patterns/task-flow-handoff.md +115 -0
- package/assets/patterns/task-flow-resume.md +14 -2
- package/assets/patterns/task-flow-start.md +40 -2
- package/assets/rules/doc-style.md +39 -1
- package/assets/rules/git-workflow.azure.md +39 -0
- package/assets/rules/git-workflow.github.md +38 -0
- package/assets/rules/git-workflow.gitlab.md +38 -0
- package/assets/rules/spec-driven.md +14 -0
- package/assets/rules/task-flow.md +70 -3
- package/assets/skills/agent-kit.md +32 -0
- package/assets/templates/postmortem.md +32 -0
- package/assets/templates/proposal.md +39 -0
- package/bin/agent-kit.d.ts.map +1 -1
- package/bin/agent-kit.js +50 -1
- package/bin/agent-kit.js.map +1 -1
- package/lib/catalog.d.ts +33 -0
- package/lib/catalog.d.ts.map +1 -1
- package/lib/catalog.js +55 -1
- package/lib/catalog.js.map +1 -1
- package/lib/commands.d.ts +41 -0
- package/lib/commands.d.ts.map +1 -1
- package/lib/commands.js +315 -3
- package/lib/commands.js.map +1 -1
- package/lib/config.d.ts +11 -1
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +6 -0
- package/lib/config.js.map +1 -1
- package/lib/hooks-map.d.ts +15 -3
- package/lib/hooks-map.d.ts.map +1 -1
- package/lib/hooks-map.js +47 -11
- package/lib/hooks-map.js.map +1 -1
- package/lib/observations.d.ts +72 -0
- package/lib/observations.d.ts.map +1 -0
- package/lib/observations.js +126 -0
- package/lib/observations.js.map +1 -0
- package/lib/proposals.d.ts +48 -0
- package/lib/proposals.d.ts.map +1 -0
- package/lib/proposals.js +111 -0
- package/lib/proposals.js.map +1 -0
- package/lib/submit.d.ts +24 -0
- package/lib/submit.d.ts.map +1 -0
- package/lib/submit.js +26 -0
- package/lib/submit.js.map +1 -0
- package/lib/sync.d.ts +9 -1
- package/lib/sync.d.ts.map +1 -1
- package/lib/sync.js +4 -6
- package/lib/sync.js.map +1 -1
- package/package.json +1 -1
- package/rt-tools-agent-kit-0.5.2.tgz +0 -0
- package/rt-tools-agent-kit-0.5.0.tgz +0 -0
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
+
# rt-hook: PreToolUse AskUserQuestion
|
|
2
3
|
# rt-hook: Stop
|
|
3
|
-
# Гард разговора:
|
|
4
|
-
#
|
|
4
|
+
# Гард разговора: вопрос владельцу не задаётся, пока за этот же ход не читались законы и
|
|
5
|
+
# правила. Стоит на двух событиях, и это не дублирование.
|
|
5
6
|
#
|
|
6
|
-
# Зачем именно так. Требование «правила читаются до разговора»
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
7
|
+
# Зачем именно так. Требование «правила читаются до разговора» исполнимо ровно до отправки
|
|
8
|
+
# вопроса. Проверка на завершении хода отбивает задним числом: к моменту отказа вопрос уже у
|
|
9
|
+
# владельца, и владелец видит его вместе с отбитым ходом — требование срабатывает, но работу не
|
|
10
|
+
# спасает. Поэтому ход с вопросом судится на инструменте вопроса, до отправки.
|
|
10
11
|
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
12
|
+
# Одним этим перехватом дыра не закрывается: вопрос чаще задаётся прозой, и ровно так был задан
|
|
13
|
+
# тот, из-за которого гард заведён. Прозаический вопрос инструментом не является, и поймать его
|
|
14
|
+
# можно только на завершении хода — событие получает путь к записи хода и видит его целиком.
|
|
15
|
+
# Отсюда два события: меню ловится до отправки, проза — после.
|
|
13
16
|
#
|
|
14
17
|
# Чтением правил считается любой из трёх путей: загрузка правила, чтение файла законов или
|
|
15
18
|
# правил, поиск по ним. Требовать именно загрузку значило бы гнать на неё там, где хватило
|
|
@@ -23,10 +26,13 @@ input="$(cat 2>/dev/null)"
|
|
|
23
26
|
|
|
24
27
|
command -v jq >/dev/null 2>&1 || exit 0
|
|
25
28
|
|
|
29
|
+
# Какое событие пришло. У вызова инструмента есть его имя, у завершения хода — нет.
|
|
30
|
+
tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
|
|
31
|
+
|
|
26
32
|
# Повторный заход по тому же ходу не судится: иначе ход не кончится никогда — гард сказал своё
|
|
27
|
-
# один раз и отпускает.
|
|
33
|
+
# один раз и отпускает. К вызову инструмента это не относится: там судится сам вызов.
|
|
28
34
|
active="$(printf '%s' "$input" | jq -r '.stop_hook_active // false' 2>/dev/null)"
|
|
29
|
-
[ "$active" = "true" ] && exit 0
|
|
35
|
+
[ -z "$tool" ] && [ "$active" = "true" ] && exit 0
|
|
30
36
|
|
|
31
37
|
transcript="$(printf '%s' "$input" | jq -r '.transcript_path // empty' 2>/dev/null)"
|
|
32
38
|
[ -z "$transcript" ] && exit 0
|
|
@@ -61,7 +67,10 @@ read_re="$(printf '%s' "$laws_dir|$rules_dir|$specs_dir" | sed 's/^|*//; s/|*$//
|
|
|
61
67
|
# бы.
|
|
62
68
|
#
|
|
63
69
|
# Хвост в 400 строк: запись хода растёт всю сессию, а судится только последний ход.
|
|
64
|
-
|
|
70
|
+
# На событии вызова инструмента вопрос уже известен — он и есть вызов; судится только то,
|
|
71
|
+
# читались ли за этот ход правила. На завершении хода вопрос ищется в тексте реплик: меню к
|
|
72
|
+
# этому моменту уже отбито раньше.
|
|
73
|
+
verdict="$(tail -n 400 "$transcript" 2>/dev/null | jq -s -r --arg re "$read_re" --arg tool "$tool" '
|
|
65
74
|
def is_input:
|
|
66
75
|
.type == "user"
|
|
67
76
|
and (((.message.content // []) | if type == "array"
|
|
@@ -78,19 +87,32 @@ verdict="$(tail -n 400 "$transcript" 2>/dev/null | jq -s -r --arg re "$read_re"
|
|
|
78
87
|
or ((.name == "Bash") and ((.input.command // "") | test($re)))
|
|
79
88
|
) | any) as $read
|
|
80
89
|
| (($texts | join("\n")) | test("\\?[[:space:]]*$"; "m")) as $asked_prose
|
|
81
|
-
| ($
|
|
82
|
-
| if ($asked_prose or $
|
|
90
|
+
| ($tool != "") as $asking_now
|
|
91
|
+
| if ($asked_prose or $asking_now) and ($read | not) then "ask" else "pass" end
|
|
83
92
|
' 2>/dev/null)"
|
|
84
93
|
|
|
85
94
|
[ "$verdict" = "ask" ] || exit 0
|
|
86
95
|
|
|
87
|
-
|
|
96
|
+
if [ -n "$tool" ]; then
|
|
97
|
+
head="BLOCKED by grill-gate: вопрос владельцу ещё не ушёл, и это единственный момент, когда требование исполнимо."
|
|
98
|
+
else
|
|
99
|
+
head="BLOCKED by grill-gate: в ответе есть вопрос владельцу, а законы и правила за этот ход не читались."
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
reason="$head Вопрос, ответ на который уже записан, владельцу не задаётся — правило ведения работы. Прогони поиск по словам темы и ответь по найденному; спрашивай только то, что документацией не покрыто:
|
|
88
103
|
|
|
89
104
|
grep -rn -i \"<слово темы>\" $laws_dir $rules_dir $specs_dir
|
|
90
105
|
|
|
91
106
|
Гард судит один ход: следующий заход не отбивается."
|
|
92
107
|
|
|
93
|
-
|
|
94
|
-
|
|
108
|
+
# Форма отказа у двух событий разная: вызов инструмента отбивается решением о доступе, а
|
|
109
|
+
# завершение хода — решением о ходе. Одна форма на оба события молча не срабатывает.
|
|
110
|
+
if [ -n "$tool" ]; then
|
|
111
|
+
jq -n --arg r "$reason" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$r}}' 2>/dev/null \
|
|
112
|
+
|| printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"grill-gate: прочитай законы и правила по теме, прежде чем спрашивать владельца."}}\n'
|
|
113
|
+
else
|
|
114
|
+
jq -n --arg r "$reason" '{decision:"block",reason:$r}' 2>/dev/null \
|
|
115
|
+
|| printf '{"decision":"block","reason":"grill-gate: прочитай законы и правила по теме, прежде чем спрашивать владельца."}\n'
|
|
116
|
+
fi
|
|
95
117
|
|
|
96
118
|
exit 0
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# rt-hook: PostToolUse Edit|Write|MultiEdit|Bash|mcp__webstorm__create_new_file
|
|
3
|
+
# Требует: hooks/profile-check.sh
|
|
3
4
|
# Линтер по следам правки. PostToolUse.
|
|
4
5
|
#
|
|
5
6
|
# Два входа. Правка файла — линтуется один изменённый файл. Перенос файла командой — линтуются
|
|
@@ -62,7 +63,13 @@ for profile in "$rt_hooks_dir/../rt-kit/defaults/project.sh" "$rt_hooks_dir/../d
|
|
|
62
63
|
# shellcheck disable=SC1090
|
|
63
64
|
[ -f "$profile" ] && . "$profile" 2>/dev/null
|
|
64
65
|
done
|
|
65
|
-
|
|
66
|
+
|
|
67
|
+
# Слово о нехватке функции профиля: хук, вышедший молча, неотличим от работающего. Файл может
|
|
68
|
+
# быть не разложен — тогда остаётся прежнее поведение, молчаливое.
|
|
69
|
+
# shellcheck disable=SC1090
|
|
70
|
+
[ -f "$rt_hooks_dir/profile-check.sh" ] && . "$rt_hooks_dir/profile-check.sh"
|
|
71
|
+
command -v rt_needs >/dev/null 2>&1 || rt_needs() { command -v "$1" >/dev/null 2>&1; }
|
|
72
|
+
rt_needs rt_lint_for lint-after-edit || exit 0
|
|
66
73
|
|
|
67
74
|
# --- какие файлы проверяем -------------------------------------------------------------
|
|
68
75
|
|
|
@@ -133,7 +140,7 @@ fi
|
|
|
133
140
|
# --- отсев того, что линтерами дерева не покрыто ----------------------------------------
|
|
134
141
|
|
|
135
142
|
lintable() {
|
|
136
|
-
if
|
|
143
|
+
if rt_needs rt_is_app_code lint-after-edit; then
|
|
137
144
|
rt_is_app_code "$1" || return 1
|
|
138
145
|
fi
|
|
139
146
|
if [ -n "${RT_LINT_SKIP_RE:-}" ] && printf '%s' "$1" | grep -qE "$RT_LINT_SKIP_RE"; then
|
|
@@ -156,7 +163,7 @@ linter_name() {
|
|
|
156
163
|
|
|
157
164
|
# --- прогон -----------------------------------------------------------------------------
|
|
158
165
|
|
|
159
|
-
push_checks="$(
|
|
166
|
+
push_checks="$(rt_needs rt_push_checks lint-after-edit && rt_push_checks 2>/dev/null)"
|
|
160
167
|
|
|
161
168
|
report=""
|
|
162
169
|
linters=""
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Запись наблюдений о слое правил. НЕ гард: объявления `rt-hook:` у него нет, к событиям агента
|
|
3
|
+
# он не подключается. Его источают гарды — тем же приёмом, каким гейт источает карту.
|
|
4
|
+
#
|
|
5
|
+
# Зачем он есть. Правки в тексты пакета шли из головы того, кто их пишет: чем из разложенного
|
|
6
|
+
# пользуются каждый день, чем не пользовались ни разу и обо что спотыкаются по второму кругу,
|
|
7
|
+
# пакету было неизвестно. Часть данных при этом уже собиралась и выбрасывалась — запись загрузок
|
|
8
|
+
# жила во временном каталоге и гибла со сжатием контекста.
|
|
9
|
+
#
|
|
10
|
+
# ЧТО ЗАПИСЫВАЕТСЯ. Только имена ресурсов пакета и счётчики: имя правила или гарда, род события,
|
|
11
|
+
# род файла, версия пакета и признак сессии. Ни путей дерева, ни имён его доменов, ни имени
|
|
12
|
+
# самого дерева — и держится это не памятью зовущего, а `rt_observe_clean`: значение со слэшем
|
|
13
|
+
# не пишется вовсе. Наблюдение уезжает потом в чужой репозиторий, и запрет называть чужое дерево
|
|
14
|
+
# обязан держаться конструкцией.
|
|
15
|
+
#
|
|
16
|
+
# ОТКАЗ В ПОЛЬЗУ РАБОТЫ. Наблюдение — побочная работа гарда, и любая её поломка молча пропускает
|
|
17
|
+
# действие: недоступный каталог, отсутствие `date`, полный диск. Гард, упавший на записи
|
|
18
|
+
# наблюдения, останавливал бы работу ради статистики.
|
|
19
|
+
|
|
20
|
+
# Значение, годное к записи. Путь не выносится ни при каких обстоятельствах, остальное чистится
|
|
21
|
+
# до имени и обрезается: длинное значение в наблюдении не значит ничего, кроме утечки.
|
|
22
|
+
rt_observe_clean() {
|
|
23
|
+
case "$1" in
|
|
24
|
+
*/*) return 0 ;;
|
|
25
|
+
esac
|
|
26
|
+
printf '%s' "$1" | LC_ALL=C tr -cd 'A-Za-z0-9._:-' | cut -c1-48
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
# Признак сессии: считается по её имени и обратно не восстанавливается. Нужен, чтобы сводка
|
|
30
|
+
# знала число заходов, а не только число событий.
|
|
31
|
+
rt_observe_session() {
|
|
32
|
+
printf '%s' "$1" | cksum 2>/dev/null | cut -d' ' -f1
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# Версия пакета, разложившего этот файл. Стоит в шапке, которую ставит раскладка; в исходниках
|
|
36
|
+
# пакета шапки нет, и там версия — `dev`.
|
|
37
|
+
rt_observe_version() {
|
|
38
|
+
local found
|
|
39
|
+
found="$(sed -n 's/^# rt-kit v\([^ ]*\) .*/\1/p' "${BASH_SOURCE[0]}" 2>/dev/null | head -1)"
|
|
40
|
+
printf '%s' "${found:-dev}"
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# Каталог наблюдений этого дерева. Пустая строка — записи не будет.
|
|
44
|
+
rt_observe_dir() {
|
|
45
|
+
local root="${CLAUDE_PROJECT_DIR:-.}" config
|
|
46
|
+
config="$root/.claude/rt-kit.json"
|
|
47
|
+
# Выключатель дерева гасит запись целиком, а не частями: пакет стоит и у тех, о ком мы не
|
|
48
|
+
# знаем. Нечитаемая настройка выключателем не считается — иначе сломанный JSON тихо
|
|
49
|
+
# выключал бы наблюдения, и понять это было бы нечем.
|
|
50
|
+
if [ -f "$config" ] && command -v jq >/dev/null 2>&1; then
|
|
51
|
+
[ "$(jq -r 'if .observe == false then "off" else "on" end' "$config" 2>/dev/null)" = "off" ] && return 0
|
|
52
|
+
fi
|
|
53
|
+
printf '%s' "$root/.claude/rt-kit/observations"
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
# Одно наблюдение: род события и пары `ключ=значение`. Ключ `sid` хешируется, остальные
|
|
57
|
+
# чистятся. Пустое значение поля не заводит.
|
|
58
|
+
#
|
|
59
|
+
# rt_note gate-deny res=styling-bem kind=scss sid="$sid"
|
|
60
|
+
rt_note() {
|
|
61
|
+
local kind="$1" dir day stamp line pair key value
|
|
62
|
+
[ -n "$kind" ] || return 0
|
|
63
|
+
shift
|
|
64
|
+
|
|
65
|
+
dir="$(rt_observe_dir)"
|
|
66
|
+
[ -n "$dir" ] || return 0
|
|
67
|
+
mkdir -p "$dir" 2>/dev/null || return 0
|
|
68
|
+
|
|
69
|
+
day="$(date -u +%Y-%m-%d 2>/dev/null)" || return 0
|
|
70
|
+
stamp="$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null)" || return 0
|
|
71
|
+
[ -n "$day" ] || return 0
|
|
72
|
+
|
|
73
|
+
line="{\"t\":\"$stamp\",\"ev\":\"$(rt_observe_clean "$kind")\""
|
|
74
|
+
for pair in "$@"; do
|
|
75
|
+
key="$(rt_observe_clean "${pair%%=*}")"
|
|
76
|
+
value="${pair#*=}"
|
|
77
|
+
[ -n "$key" ] || continue
|
|
78
|
+
if [ "$key" = "sid" ]; then
|
|
79
|
+
value="$(rt_observe_session "$value")"
|
|
80
|
+
else
|
|
81
|
+
value="$(rt_observe_clean "$value")"
|
|
82
|
+
fi
|
|
83
|
+
[ -n "$value" ] || continue
|
|
84
|
+
line="$line,\"$key\":\"$value\""
|
|
85
|
+
done
|
|
86
|
+
line="$line,\"v\":\"$(rt_observe_version)\"}"
|
|
87
|
+
|
|
88
|
+
printf '%s\n' "$line" >> "$dir/$day.jsonl" 2>/dev/null || return 0
|
|
89
|
+
return 0
|
|
90
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
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
|
+
#
|
|
20
|
+
# ОТКАЗ В ПОЛЬЗУ РАБОТЫ: при любой ошибке, отсутствии записи хода и повторном заходе ход
|
|
21
|
+
# РАЗРЕШАЕТСЯ (exit 0). Сломанный гард не имеет права заклинить разговор.
|
|
22
|
+
|
|
23
|
+
input="$(cat 2>/dev/null)"
|
|
24
|
+
[ -z "$input" ] && exit 0
|
|
25
|
+
|
|
26
|
+
command -v jq >/dev/null 2>&1 || exit 0
|
|
27
|
+
|
|
28
|
+
# Повторный заход по тому же ходу не судится: гард сказал своё один раз и отпускает.
|
|
29
|
+
active="$(printf '%s' "$input" | jq -r '.stop_hook_active // false' 2>/dev/null)"
|
|
30
|
+
[ "$active" = "true" ] && exit 0
|
|
31
|
+
|
|
32
|
+
transcript="$(printf '%s' "$input" | jq -r '.transcript_path // empty' 2>/dev/null)"
|
|
33
|
+
[ -z "$transcript" ] && exit 0
|
|
34
|
+
[ -f "$transcript" ] || exit 0
|
|
35
|
+
|
|
36
|
+
root="${CLAUDE_PROJECT_DIR:-.}"
|
|
37
|
+
|
|
38
|
+
# Каталог записей у дерева свой. Заданный пустым — отказ дерева от требования: дереву, которое
|
|
39
|
+
# записей не ведёт, гард не навязывается.
|
|
40
|
+
notes_dir="${RT_POSTMORTEMS_DIR-docs/postmortems}"
|
|
41
|
+
[ -z "$notes_dir" ] && exit 0
|
|
42
|
+
[ -d "$root/$notes_dir" ] || exit 0
|
|
43
|
+
|
|
44
|
+
# Образцы признания промаха. Набор открыт и пополняется правкой: полнота его — открытый вопрос
|
|
45
|
+
# договорённости, а не обещание.
|
|
46
|
+
admitted_re='был неправ|был не прав|ошибс|моя ошибк|мой промах|промахнул|проглядел|не проверил|соврал|виноват|извин|прошу прощения|неверно утверждал|утверждение было ложн|принял на веру'
|
|
47
|
+
|
|
48
|
+
# Ход — это всё, что записано после последнего настоящего ввода владельца. Ответ инструмента
|
|
49
|
+
# приходит той же ролью, поэтому строки с `tool_result` вводом не считаются.
|
|
50
|
+
#
|
|
51
|
+
# Хвост в 400 строк: запись хода растёт всю сессию, а судится только последний ход.
|
|
52
|
+
verdict="$(tail -n 400 "$transcript" 2>/dev/null | jq -s -r --arg re "$admitted_re" '
|
|
53
|
+
def is_input:
|
|
54
|
+
.type == "user"
|
|
55
|
+
and (((.message.content // []) | if type == "array"
|
|
56
|
+
then ([.[] | select(.type == "tool_result")] | length)
|
|
57
|
+
else 0 end) == 0);
|
|
58
|
+
|
|
59
|
+
(map(is_input) | rindex(true)) as $i
|
|
60
|
+
| (if $i == null then . else .[$i + 1:] end) as $turn
|
|
61
|
+
| [$turn[] | select(.type == "assistant") | (.message.content // [])[] | select(.type == "text") | .text] as $texts
|
|
62
|
+
| [$turn[] | select(.type == "assistant") | (.message.content // [])[] | select(.type == "tool_use")] as $uses
|
|
63
|
+
# Признак нечувствителен к регистру флагом, а не приведением: приведение знает только
|
|
64
|
+
# латиницу, и «Был неправ» с большой буквы проходило бы мимо набора образцов молча.
|
|
65
|
+
| (($texts | join("\n")) | test($re; "i")) as $admitted
|
|
66
|
+
| ($uses | map(
|
|
67
|
+
((.name // "") | test("^(Write|Edit|MultiEdit)$"))
|
|
68
|
+
and ((.input.file_path // "") | test("postmortem"))
|
|
69
|
+
) | any) as $wrote
|
|
70
|
+
| if $admitted and ($wrote | not) then "admit" else "pass" end
|
|
71
|
+
' 2>/dev/null)"
|
|
72
|
+
|
|
73
|
+
[ "$verdict" = "admit" ] || exit 0
|
|
74
|
+
|
|
75
|
+
# Запись, сделанная за эту сессию, снимает требование и без правки в этом же ходе: разбор мог
|
|
76
|
+
# лечь файлом ходом раньше — тем, в котором промах и признали.
|
|
77
|
+
if [ -n "$(find "$root/$notes_dir" -name '*.md' -newermt '-1 day' 2>/dev/null | head -1)" ]; then
|
|
78
|
+
exit 0
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
reason="BLOCKED by postmortem-guard: в ответе признан промах, а записи о происшествии в \`$notes_dir/\` за сегодня нет. Происшествие — заход, в котором исполнитель сделал не то, а слой правил этого не отбил, — записывается в тот же заход: назавтра механизм промаха пересказывается уже приглаженно, и правило из него не выводится.
|
|
82
|
+
|
|
83
|
+
Запись называет: механизм промаха по шагам, что было доступно до него, чем ловилось и что из этого ушло в слой правил. Без последней строки это жалоба, а не разбор.
|
|
84
|
+
|
|
85
|
+
$notes_dir/<год>-<месяц>-<день>-<короткое имя>.md
|
|
86
|
+
|
|
87
|
+
Гард судит один ход: следующий заход не отбивается."
|
|
88
|
+
|
|
89
|
+
jq -n --arg r "$reason" '{decision:"block",reason:$r}' 2>/dev/null \
|
|
90
|
+
|| printf '{"decision":"block","reason":"postmortem-guard: признан промах — запиши разбор происшествия."}\n'
|
|
91
|
+
|
|
92
|
+
exit 0
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Нехватка функции профиля, сказанная вслух. НЕ гард: объявления `rt-hook:` у него нет, к
|
|
3
|
+
# событиям агента он не подключается. Его источают сами гарды — тем же приёмом, каким гейт
|
|
4
|
+
# источает карту, а гарды — запись наблюдений.
|
|
5
|
+
#
|
|
6
|
+
# Зачем он есть. Девять хуков пакета зовут функции профиля дерева, и у пяти проверка стоит до
|
|
7
|
+
# всякого поведения: нет функции — выход с нулём, не сделав ничего. Такой гард хуже
|
|
8
|
+
# отсутствующего: он лежит в дереве, стоит в настройке, виден в списке хуков и читается как
|
|
9
|
+
# работающий. Тишина при этом означает разом три разных вещи — «нечего проверять», «проверять
|
|
10
|
+
# нечем» и «всё в порядке», — и отличить их нечем.
|
|
11
|
+
#
|
|
12
|
+
# ЧТО ГОВОРИТСЯ. Имя функции и файл, в котором её определяют. Один раз за сессию: на каждый
|
|
13
|
+
# вызов та же строка повторялась бы десятки раз за заход и перестала бы читаться.
|
|
14
|
+
#
|
|
15
|
+
# ДЕЙСТВИЕ ВСЁ РАВНО ПРОПУСКАЕТСЯ. Хук сообщает о своей неполноте, а не судит правку: судить её
|
|
16
|
+
# нечем, и отбивать работу из-за ненастроенного дерева он не вправе.
|
|
17
|
+
|
|
18
|
+
# Куда кладётся отметка «об этом уже сказано». Каталог временных файлов, а не дерево: отметка
|
|
19
|
+
# живёт один заход и в историю не едет.
|
|
20
|
+
rt_needs_mark_dir() {
|
|
21
|
+
printf '%s' "${TMPDIR:-/tmp}"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
# Есть ли функция профиля. Успех — есть, и хук работает дальше. Отказ — нет, и об этом сказано.
|
|
25
|
+
#
|
|
26
|
+
# rt_needs rt_is_app_code task-flow-guard "$sid"
|
|
27
|
+
#
|
|
28
|
+
# Третий параметр — признак сессии, если хук его знает. Не знает — отметка кладётся на день:
|
|
29
|
+
# без признака «один раз за сессию» превратилось бы в «один раз за всю жизнь машины».
|
|
30
|
+
rt_needs() {
|
|
31
|
+
command -v "$1" >/dev/null 2>&1 && return 0
|
|
32
|
+
|
|
33
|
+
rt_needs_key="${3:-$(date +%Y%m%d 2>/dev/null || printf 'nosession')}"
|
|
34
|
+
rt_needs_mark="$(rt_needs_mark_dir)/rt-kit-needs-$1-$rt_needs_key"
|
|
35
|
+
if [ ! -f "$rt_needs_mark" ]; then
|
|
36
|
+
printf '%s: нет функции %s — её определяют в .claude/rt-kit/project.sh, а умолчание везёт пакет в .claude/rt-kit/defaults/project.sh\n' \
|
|
37
|
+
"${2:-хук}" "$1" >&2
|
|
38
|
+
printf 'проверка не работает, действие пропущено\n' >&2
|
|
39
|
+
: >"$rt_needs_mark" 2>/dev/null || true
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
return 1
|
|
43
|
+
}
|
|
@@ -1,5 +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
4
|
# Гард якоря для спек. PreToolUse на правке разметки.
|
|
4
5
|
#
|
|
5
6
|
# Спеки адресуют элементы только через этот атрибут. Классы оформления меняются вместе с
|
|
@@ -56,10 +57,16 @@ for profile in "$rt_hooks_dir/../rt-kit/defaults/project.sh" "$rt_hooks_dir/../d
|
|
|
56
57
|
[ -f "$profile" ] && . "$profile" 2>/dev/null
|
|
57
58
|
done
|
|
58
59
|
|
|
60
|
+
# Слово о нехватке функции профиля: хук, вышедший молча, неотличим от работающего. Файл может
|
|
61
|
+
# быть не разложен — тогда остаётся прежнее поведение, молчаливое.
|
|
62
|
+
# shellcheck disable=SC1090
|
|
63
|
+
[ -f "$rt_hooks_dir/profile-check.sh" ] && . "$rt_hooks_dir/profile-check.sh"
|
|
64
|
+
command -v rt_needs >/dev/null 2>&1 || rt_needs() { command -v "$1" >/dev/null 2>&1; }
|
|
65
|
+
|
|
59
66
|
# Разметка приложения живёт там же, где его код. Без этой положительной проверки гард
|
|
60
67
|
# распространялся на любой файл разметки на диске — черновик вне дерева отклонялся требованием
|
|
61
68
|
# проставить якоря.
|
|
62
|
-
if
|
|
69
|
+
if rt_needs rt_is_app_code qa-dataid-guard; then
|
|
63
70
|
rt_is_app_code "$path" || exit 0
|
|
64
71
|
fi
|
|
65
72
|
|
|
@@ -75,7 +82,7 @@ added="$(printf '%s' "$input" | jq -r '
|
|
|
75
82
|
' 2>/dev/null)"
|
|
76
83
|
[ -z "$added" ] && exit 0
|
|
77
84
|
|
|
78
|
-
decorative="$(
|
|
85
|
+
decorative="$(rt_needs rt_qa_decorative qa-dataid-guard && rt_qa_decorative 2>/dev/null)"
|
|
79
86
|
component_re="${RT_QA_COMPONENT_RE:--}"
|
|
80
87
|
|
|
81
88
|
# Открывающие теги разбираются ЦЕЛИКОМ: тег занимает несколько строк, и якорь часто стоит не в
|
|
@@ -1,5 +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
4
|
# Гард «ничего не пишется с нуля». PreToolUse на правке кода и разметки.
|
|
4
5
|
#
|
|
5
6
|
# Линтеры знают правила, но не знают ИНВЕНТАРЬ: линтер стилей поймает сырой цвет, линтер кода —
|
|
@@ -61,11 +62,17 @@ for profile in "$rt_hooks_dir/../rt-kit/defaults/project.sh" "$rt_hooks_dir/../d
|
|
|
61
62
|
# shellcheck disable=SC1090
|
|
62
63
|
[ -f "$profile" ] && . "$profile" 2>/dev/null
|
|
63
64
|
done
|
|
64
|
-
|
|
65
|
+
|
|
66
|
+
# Слово о нехватке функции профиля: хук, вышедший молча, неотличим от работающего. Файл может
|
|
67
|
+
# быть не разложен — тогда остаётся прежнее поведение, молчаливое.
|
|
68
|
+
# shellcheck disable=SC1090
|
|
69
|
+
[ -f "$rt_hooks_dir/profile-check.sh" ] && . "$rt_hooks_dir/profile-check.sh"
|
|
70
|
+
command -v rt_needs >/dev/null 2>&1 || rt_needs() { command -v "$1" >/dev/null 2>&1; }
|
|
71
|
+
rt_needs rt_reinvented_in reuse-first-guard || exit 0
|
|
65
72
|
|
|
66
73
|
# Правила этого дерева действуют на код этого дерева: без положительной проверки гард требовал
|
|
67
74
|
# бы собирать готовым и в черновике за пределами дерева.
|
|
68
|
-
if
|
|
75
|
+
if rt_needs rt_is_app_code reuse-first-guard; then
|
|
69
76
|
rt_is_app_code "$path" || exit 0
|
|
70
77
|
fi
|
|
71
78
|
|
|
@@ -38,6 +38,7 @@ command -v skill_for >/dev/null 2>&1 || exit 0
|
|
|
38
38
|
|
|
39
39
|
req=""
|
|
40
40
|
target=""
|
|
41
|
+
kind=""
|
|
41
42
|
case "$tool" in
|
|
42
43
|
# Инструмент среды заводит файл теми же двумя данными, только называет их иначе — без этой
|
|
43
44
|
# ветки файл заводился мимо гейта.
|
|
@@ -61,6 +62,12 @@ case "$tool" in
|
|
|
61
62
|
# приходит в обычный сервис, а число-настройка в обычный класс.
|
|
62
63
|
written="$(printf '%s' "$input" | jq -r '[.tool_input.content, .tool_input.text, .tool_input.new_string, (.tool_input.edits[]?.new_string)] | map(select(. != null)) | join("\n")' 2>/dev/null)"
|
|
63
64
|
req="$(skill_for edit "$target" "$written" 2>/dev/null)"
|
|
65
|
+
# Род правки для наблюдения. Одно расширение, без пути и без имени файла: наблюдение
|
|
66
|
+
# уезжает наружу, и всё, кроме рода, там было бы адресом этого дерева.
|
|
67
|
+
case "${target##*/}" in
|
|
68
|
+
*.*) kind="${target##*.}" ;;
|
|
69
|
+
*) kind="none" ;;
|
|
70
|
+
esac
|
|
64
71
|
;;
|
|
65
72
|
# Терминал среды исполняет ту же командную строку и кладёт её в то же поле: без этой ветки
|
|
66
73
|
# коммит из него не требовал правила, тогда как тот же коммит из оболочки требовал.
|
|
@@ -77,9 +84,11 @@ case "$tool" in
|
|
|
77
84
|
[ -n "$inner" ] && target="$inner"
|
|
78
85
|
fi
|
|
79
86
|
req="$(skill_for bash "$target" "" 2>/dev/null)"
|
|
87
|
+
kind="command"
|
|
80
88
|
;;
|
|
81
89
|
mcp__claude-in-chrome__*)
|
|
82
90
|
req="$(skill_for browser "$tool" "" 2>/dev/null)"
|
|
91
|
+
kind="browser"
|
|
83
92
|
;;
|
|
84
93
|
*) exit 0 ;;
|
|
85
94
|
esac
|
|
@@ -108,6 +117,12 @@ done
|
|
|
108
117
|
[ -z "$want" ] && exit 0
|
|
109
118
|
req="$want"
|
|
110
119
|
|
|
120
|
+
# Отбитие — наблюдение: правило, которое приходится требовать чаще прочего, и род правки, на
|
|
121
|
+
# котором это происходит, говорят о слое правил больше, чем список загруженного.
|
|
122
|
+
# shellcheck disable=SC1090
|
|
123
|
+
[ -f "$rt_hooks_dir/observe.sh" ] && . "$rt_hooks_dir/observe.sh" 2>/dev/null
|
|
124
|
+
command -v rt_note >/dev/null 2>&1 && rt_note gate-deny "res=$req" "kind=$kind" "sid=$sid"
|
|
125
|
+
|
|
111
126
|
reason="Отбито гейтом правил: загрузи правило «${req}» инструментом Skill и повтори действие. Для этой области это происходит один раз за сессию."
|
|
112
127
|
|
|
113
128
|
# Правило называет свой закон одним словом, а слоёв законов два: общий лежит в корне, закон
|
|
@@ -18,4 +18,11 @@ dir="${TMPDIR:-/tmp}/claude-skill-gate"
|
|
|
18
18
|
mkdir -p "$dir" 2>/dev/null || exit 0
|
|
19
19
|
printf '%s\n' "$skill" >> "$dir/${sid}.loaded" 2>/dev/null
|
|
20
20
|
|
|
21
|
+
# Та же загрузка вторым адресом — в наблюдения дерева. Запись выше живёт до сжатия контекста и
|
|
22
|
+
# гибнет вместе с ним: она отвечает гейту на вопрос «загружено ли», и больше ни на что.
|
|
23
|
+
rt_hooks_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
24
|
+
# shellcheck disable=SC1090
|
|
25
|
+
[ -f "$rt_hooks_dir/observe.sh" ] && . "$rt_hooks_dir/observe.sh" 2>/dev/null
|
|
26
|
+
command -v rt_note >/dev/null 2>&1 && rt_note skill-load "res=$skill" "sid=$sid"
|
|
27
|
+
|
|
21
28
|
exit 0
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# rt-hook: SessionStart startup|resume|compact|clear
|
|
3
|
+
# Требует: hooks/profile-check.sh
|
|
3
4
|
# SessionStart: состояние незаконченной работы уезжает в контекст на каждом запуске сессии.
|
|
4
5
|
#
|
|
5
6
|
# Памятью это не держится по той же причине, что и словарь: замысел читают перед правкой
|
|
@@ -28,6 +29,12 @@ for profile in "$rt_hooks_dir/../rt-kit/defaults/project.sh" "$rt_hooks_dir/../d
|
|
|
28
29
|
[ -f "$profile" ] && . "$profile" 2>/dev/null
|
|
29
30
|
done
|
|
30
31
|
|
|
32
|
+
# Слово о нехватке функции профиля: хук, вышедший молча, неотличим от работающего. Файл может
|
|
33
|
+
# быть не разложен — тогда остаётся прежнее поведение, молчаливое.
|
|
34
|
+
# shellcheck disable=SC1090
|
|
35
|
+
[ -f "$rt_hooks_dir/profile-check.sh" ] && . "$rt_hooks_dir/profile-check.sh"
|
|
36
|
+
command -v rt_needs >/dev/null 2>&1 || rt_needs() { command -v "$1" >/dev/null 2>&1; }
|
|
37
|
+
|
|
31
38
|
TASKS_DIR="${RT_TASKS_DIR:-docs/tasks}"
|
|
32
39
|
[ -z "$TASKS_DIR" ] && exit 0
|
|
33
40
|
|
|
@@ -43,13 +50,20 @@ emit() {
|
|
|
43
50
|
# Ветка под задачу без папки — работа идёт мимо. Сессию не рвём: SessionStart, отбивающий
|
|
44
51
|
# запуск, оставляет владельца без агента вовсе, а правку кода поймает `task-flow-guard`.
|
|
45
52
|
if [ ! -d "$DIR" ]; then
|
|
46
|
-
if
|
|
53
|
+
if rt_needs rt_task_branch_ok task-context-load && rt_task_branch_ok "$branch"; then
|
|
47
54
|
{
|
|
48
55
|
printf 'РАБОТА БЕЗ ПАПКИ ЗАДАЧИ.\n\n'
|
|
49
56
|
printf 'Ветка `%s` названа задачей, а `%s/` нет: ход работы записывать некуда,\n' "$branch" "$DIR"
|
|
50
57
|
printf 'и следующий заход начнёт с расспросов владельца.\n\n'
|
|
51
58
|
printf 'Собрать с образца:\n\n cp -r %s/_template %s\n\n' "$TASKS_DIR" "$DIR"
|
|
52
|
-
|
|
59
|
+
# О соседнем ресурсе — условно и по имени: пакет не знает, разложен ли он здесь,
|
|
60
|
+
# а сказанное безусловно приходит в контекст каждой сессии и врёт про дерево тем
|
|
61
|
+
# увереннее, что печатает это сам инструмент.
|
|
62
|
+
if [ -f "$rt_hooks_dir/task-flow-guard.sh" ]; then
|
|
63
|
+
printf 'Правку кода приложения до этого отбивает гард `task-flow-guard`. Правило — скил `task-flow`.\n'
|
|
64
|
+
else
|
|
65
|
+
printf 'Правило — скил `task-flow`. Гарда `task-flow-guard` в дереве нет: правку кода до этого не отбивает ничто.\n'
|
|
66
|
+
fi
|
|
53
67
|
} | emit
|
|
54
68
|
fi
|
|
55
69
|
exit 0
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# rt-hook: PreToolUse Edit|Write|MultiEdit
|
|
3
|
+
# Требует: hooks/profile-check.sh
|
|
3
4
|
# PreToolUse guard for Edit|Write|MultiEdit: код не пишется раньше замысла.
|
|
4
5
|
#
|
|
5
6
|
# Работа идёт много заходов, и между ними исполнитель не помнит ничего. Замысел, лежащий на
|
|
@@ -45,11 +46,17 @@ for profile in "$rt_hooks_dir/../rt-kit/defaults/project.sh" "$rt_hooks_dir/../d
|
|
|
45
46
|
[ -f "$profile" ] && . "$profile" 2>/dev/null
|
|
46
47
|
done
|
|
47
48
|
|
|
49
|
+
# Слово о нехватке функции профиля: хук, вышедший молча, неотличим от работающего. Файл может
|
|
50
|
+
# быть не разложен — тогда остаётся прежнее поведение, молчаливое.
|
|
51
|
+
# shellcheck disable=SC1090
|
|
52
|
+
[ -f "$rt_hooks_dir/profile-check.sh" ] && . "$rt_hooks_dir/profile-check.sh"
|
|
53
|
+
command -v rt_needs >/dev/null 2>&1 || rt_needs() { command -v "$1" >/dev/null 2>&1; }
|
|
54
|
+
|
|
48
55
|
# Признак «правка меняет поведение» — путь, а не оценка на глаз: оценку назначает тот, кому
|
|
49
56
|
# она мешает, и порог плывёт. Где живёт код приложения, знает профиль: правила, тексты, обвязка
|
|
50
57
|
# и зависимости под требование не попадают — иначе разбор задачи нельзя было бы вести до
|
|
51
58
|
# заведения ветки.
|
|
52
|
-
|
|
59
|
+
rt_needs rt_is_app_code task-flow-guard || exit 0
|
|
53
60
|
rt_is_app_code "$path" || exit 0
|
|
54
61
|
|
|
55
62
|
# Каталог папок задач: у дерева он свой, но имя обычно общее.
|
|
@@ -70,7 +77,7 @@ git rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0
|
|
|
70
77
|
branch="$(git branch --show-current 2>/dev/null)"
|
|
71
78
|
[ -z "$branch" ] && exit 0 # detached HEAD — не про наш случай
|
|
72
79
|
|
|
73
|
-
if
|
|
80
|
+
if rt_needs rt_task_branch_ok task-flow-guard && ! rt_task_branch_ok "$branch"; then
|
|
74
81
|
deny "BLOCKED by task-flow: правка кода идёт в ветке под задачу, а текущая ветка — '${branch}'. Заведи задачу (npm run task:new -- --title '…' --slug <slug>) и ветку под её номером, затем повтори. Правило — скил task-flow."
|
|
75
82
|
fi
|
|
76
83
|
|