@rt-tools/agent-kit 0.17.0 → 0.18.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-epics.github.mjs +123 -0
- package/assets/checks/board-gh.github.mjs +121 -0
- package/assets/checks/board.github.mjs +77 -73
- package/assets/checks/check-board.github.mjs +39 -3
- package/assets/checks/check-hook-scope.mjs +126 -0
- package/assets/checks/check-profile-drift.mjs +195 -0
- package/assets/checks/check-specs.mjs +1 -1
- package/assets/checks/rt-kit-checks.config.mjs +13 -0
- package/assets/checks/spec-anchors.mjs +2 -2
- package/assets/defaults/project.sh +21 -117
- package/assets/defaults/shell.sh +139 -0
- package/assets/hooks/browser-guard-no-other-drivers.sh +41 -2
- package/assets/hooks/claim-guard.sh +22 -1
- package/assets/hooks/docs-guard.sh +1 -1
- package/assets/hooks/exam-guard.sh +85 -12
- package/assets/hooks/git-guard-delivery-conflict.sh +85 -0
- package/assets/hooks/git-guard-delivery-signature.sh +14 -5
- package/assets/hooks/git-guard-delivery.sh +15 -2
- package/assets/hooks/grill-gate.sh +63 -6
- package/assets/hooks/proposal-guard.sh +11 -4
- package/assets/hooks/rule-source-guard.sh +8 -13
- package/assets/hooks/task-context-load.sh +22 -0
- package/assets/hooks/task-flow-context.sh +9 -0
- package/assets/hooks/task-flow-draft-guard.sh +17 -0
- package/assets/hooks/turn-exit-guard.sh +45 -87
- package/assets/hooks/waiting-turn-guard.sh +65 -2
- package/assets/hooks/work-start-guard.sh +166 -0
- package/assets/hooks/write-targets.sh +24 -0
- package/assets/laws/verifiability.md +23 -0
- package/assets/patterns/browser-verification-measure.md +3 -1
- package/assets/patterns/browser-verification-stand.md +10 -2
- package/assets/patterns/doc-style-write.md +21 -1
- package/assets/patterns/git-workflow-commit.github.md +18 -3
- package/assets/patterns/git-workflow-docker.md +8 -0
- package/assets/patterns/git-workflow-freshness.md +87 -0
- package/assets/patterns/git-workflow-merge.md +23 -0
- package/assets/patterns/git-workflow-pr.github.md +6 -0
- package/assets/patterns/git-workflow-restart.md +18 -0
- package/assets/patterns/git-workflow-stack.md +30 -0
- package/assets/patterns/lib-layers-move.md +4 -0
- package/assets/patterns/spec-driven-domain.md +13 -1
- package/assets/patterns/spec-driven-rule.md +5 -4
- package/assets/patterns/spec-driven-sweep.md +1 -1
- package/assets/patterns/status-report-table.github.md +1 -1
- package/assets/patterns/task-flow-archive.md +12 -2
- package/assets/patterns/task-flow-close.md +31 -24
- package/assets/patterns/task-flow-handoff.md +14 -2
- package/assets/patterns/task-flow-resume.md +18 -3
- package/assets/patterns/task-flow-start.md +23 -23
- package/assets/patterns/testing-e2e.md +23 -0
- package/assets/patterns/turn-entry-map.md +1 -1
- package/assets/pitfalls/agent-kit.md +4 -6
- package/assets/pitfalls/doc-style.md +10 -0
- package/assets/pitfalls/git-workflow.github.md +20 -0
- package/assets/pitfalls/spec-driven.md +6 -0
- package/assets/pitfalls/task-flow.md +20 -0
- package/assets/pitfalls/testing.md +6 -0
- package/assets/pitfalls/turn-conduct.md +35 -0
- package/assets/rules/browser-verification.md +19 -0
- package/assets/rules/deploy-flow.github.md +9 -0
- package/assets/rules/doc-style.md +6 -0
- package/assets/rules/git-workflow.azure.md +17 -2
- package/assets/rules/git-workflow.github.md +89 -90
- package/assets/rules/git-workflow.gitlab.md +12 -4
- package/assets/rules/lists.md +5 -0
- package/assets/rules/observability.needs-app.md +4 -0
- package/assets/rules/reuse-first.md +6 -0
- package/assets/rules/spec-driven.md +29 -25
- package/assets/rules/task-flow.md +52 -46
- package/assets/rules/testing.md +27 -1
- package/assets/rules/turn-conduct.md +46 -43
- package/assets/skills/agent-kit.md +9 -0
- package/package.json +1 -1
- package/rt-tools-agent-kit-0.18.0.tgz +0 -0
- package/rt-tools-agent-kit-0.17.0.tgz +0 -0
|
@@ -21,6 +21,15 @@
|
|
|
21
21
|
# правил, поиск по ним. Требовать именно загрузку значило бы гнать на неё там, где хватило
|
|
22
22
|
# одного поиска, — гард мешал бы работе вместо того, чтобы её выправлять.
|
|
23
23
|
#
|
|
24
|
+
# Но чтение чего угодно из слоя правил вопроса не закрывает: прочитанный разбор чужого промаха
|
|
25
|
+
# и поиск по каталогу засчитывались наравне с правилом, которое этой работе и требуется, — а
|
|
26
|
+
# ответ на заданный владельцу вопрос лежал ровно в нём. Поэтому, когда область работы этого хода
|
|
27
|
+
# известна, чтением считается только правило этой области. Область берётся оттуда же, откуда её
|
|
28
|
+
# берёт гейт правил: по путям правок хода.
|
|
29
|
+
#
|
|
30
|
+
# ОТКАЗ В ПОЛЬЗУ РАБОТЫ и здесь: правок в ходу нет, карты гейта нет, область не определилась —
|
|
31
|
+
# засчитывается любое чтение, как прежде.
|
|
32
|
+
#
|
|
24
33
|
# ОТКАЗ В ПОЛЬЗУ РАБОТЫ: при любой ошибке, отсутствии записи хода и повторном заходе ход
|
|
25
34
|
# РАЗРЕШАЕТСЯ (exit 0). Сломанный гард не имеет права заклинить разговор.
|
|
26
35
|
|
|
@@ -68,11 +77,47 @@ archive_dir="${RT_ARCHIVE_DIR-docs/archive}"
|
|
|
68
77
|
# Дерево, у которого нет ни законов, ни правил, требования не получает: читать нечего.
|
|
69
78
|
[ -z "$laws_dir" ] && [ -z "$rules_dir" ] && exit 0
|
|
70
79
|
|
|
80
|
+
# Карта гейта: по ней имя правила достаётся из пути правки. Нет её — область не определяется, и
|
|
81
|
+
# всё остаётся прежним.
|
|
82
|
+
rt_hooks_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
83
|
+
for map in "$rt_hooks_dir/../rt-kit/defaults/gate-map.sh" "$rt_hooks_dir/../defaults/gate-map.sh" "${CLAUDE_PROJECT_DIR:-.}/.claude/rt-kit/defaults/gate-map.sh" "${CLAUDE_PROJECT_DIR:-.}/.claude/rt-kit/gate-map.sh"; do
|
|
84
|
+
# shellcheck disable=SC1090
|
|
85
|
+
[ -f "$map" ] && . "$map" 2>/dev/null
|
|
86
|
+
done
|
|
87
|
+
|
|
71
88
|
# Образец, по которому вызов инструмента считается чтением правил. Каталоги идут в него как
|
|
72
89
|
# есть: точка в `.claude` совпадает с любым знаком и лишнего сюда не приводит.
|
|
73
90
|
read_re="$(printf '%s' "$laws_dir|$rules_dir|$specs_dir|$plans_dir|$archive_dir" | sed 's/^|*//; s/|*$//; s/||*/|/g')"
|
|
74
91
|
[ -z "$read_re" ] && exit 0
|
|
75
92
|
|
|
93
|
+
# Область работы этого хода: правила, которых требует гейт от путей, правленных в ходу. Пути
|
|
94
|
+
# достаются тем же разбором хода, что и ниже, — своим вызовом, чтобы образец чтения был готов
|
|
95
|
+
# к главному разбору.
|
|
96
|
+
need_re=''
|
|
97
|
+
if command -v skill_for >/dev/null 2>&1 && [ -n "$rules_dir" ]; then
|
|
98
|
+
edited="$(tail -n 400 "$transcript" 2>/dev/null | jq -s -r '
|
|
99
|
+
def is_input:
|
|
100
|
+
.type == "user"
|
|
101
|
+
and (((.message.content // []) | if type == "array"
|
|
102
|
+
then ([.[] | select(.type == "tool_result")] | length)
|
|
103
|
+
else 0 end) == 0);
|
|
104
|
+
(map(is_input) | rindex(true)) as $i
|
|
105
|
+
| (if $i == null then . else .[$i + 1:] end)
|
|
106
|
+
| [.[] | select(.type == "assistant") | (.message.content // [])[]
|
|
107
|
+
| select(.type == "tool_use") | select(.name == "Edit" or .name == "Write" or .name == "NotebookEdit")
|
|
108
|
+
| (.input.file_path // .input.notebook_path // "")]
|
|
109
|
+
| map(select(. != "")) | unique | .[]
|
|
110
|
+
' 2>/dev/null)"
|
|
111
|
+
for path in $edited; do
|
|
112
|
+
for rule in $(skill_for edit "$path" '' 2>/dev/null); do
|
|
113
|
+
case "|$need_re|" in
|
|
114
|
+
*"|$rule|"*) ;;
|
|
115
|
+
*) need_re="${need_re}${need_re:+|}${rule}" ;;
|
|
116
|
+
esac
|
|
117
|
+
done
|
|
118
|
+
done
|
|
119
|
+
fi
|
|
120
|
+
|
|
76
121
|
# Ход — это всё, что записано после последнего настоящего ввода владельца. Ответ инструмента
|
|
77
122
|
# приходит той же ролью `user`, поэтому строки с `tool_result` вводом не считаются: иначе ходом
|
|
78
123
|
# оказался бы кусок после последнего вызова инструмента, и чтение правил в его начале потерялось
|
|
@@ -82,7 +127,7 @@ read_re="$(printf '%s' "$laws_dir|$rules_dir|$specs_dir|$plans_dir|$archive_dir"
|
|
|
82
127
|
# На событии вызова инструмента вопрос уже известен — он и есть вызов; судится только то,
|
|
83
128
|
# читались ли за этот ход правила. На завершении хода вопрос ищется в тексте реплик: меню к
|
|
84
129
|
# этому моменту уже отбито раньше.
|
|
85
|
-
verdict="$(tail -n 400 "$transcript" 2>/dev/null | jq -s -r --arg re "$read_re" --arg tool "$tool" '
|
|
130
|
+
verdict="$(tail -n 400 "$transcript" 2>/dev/null | jq -s -r --arg re "$read_re" --arg tool "$tool" --arg need "$need_re" --arg rules "$rules_dir" '
|
|
86
131
|
def is_input:
|
|
87
132
|
.type == "user"
|
|
88
133
|
and (((.message.content // []) | if type == "array"
|
|
@@ -93,11 +138,23 @@ verdict="$(tail -n 400 "$transcript" 2>/dev/null | jq -s -r --arg re "$read_re"
|
|
|
93
138
|
| (if $i == null then . else .[$i + 1:] end) as $turn
|
|
94
139
|
| [$turn[] | select(.type == "assistant") | (.message.content // [])[] | select(.type == "text") | .text] as $texts
|
|
95
140
|
| [$turn[] | select(.type == "assistant") | (.message.content // [])[] | select(.type == "tool_use")] as $uses
|
|
96
|
-
| ($
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
141
|
+
| (if $need == "" then
|
|
142
|
+
$uses | map(
|
|
143
|
+
(.name == "Skill")
|
|
144
|
+
or ((.name // "") | test("^(Read|Grep|Glob)$")) and ((.input | tostring) | test($re))
|
|
145
|
+
or ((.name == "Bash") and ((.input.command // "") | test($re)))
|
|
146
|
+
) | any
|
|
147
|
+
else
|
|
148
|
+
# Область работы известна — засчитывается только правило этой области: прочитанный
|
|
149
|
+
# разбор чужого промаха на заданный вопрос не отвечает.
|
|
150
|
+
($need | split("|")) as $rules_needed
|
|
151
|
+
| $uses | map(
|
|
152
|
+
((.name == "Skill") and (((.input.skill // "") | tostring) as $s | $rules_needed | index($s) != null))
|
|
153
|
+
or (((.name // "") | test("^(Read|Grep|Glob|Bash)$"))
|
|
154
|
+
and ((.input | tostring) as $text
|
|
155
|
+
| $rules_needed | map(. as $rule | $text | test($rules + "/" + $rule + "(/|\\b)")) | any))
|
|
156
|
+
) | any
|
|
157
|
+
end) as $read
|
|
101
158
|
| (($texts | join("\n")) | test("\\?[[:space:]]*$"; "m")) as $asked_prose
|
|
102
159
|
| ($tool != "") as $asking_now
|
|
103
160
|
| if ($asked_prose or $asking_now) and ($read | not) then "ask" else "pass" end
|
|
@@ -38,16 +38,24 @@ transcript="$(printf '%s' "$input" | jq -r '.transcript_path // empty' 2>/dev/nu
|
|
|
38
38
|
[ -f "$transcript" ] || exit 0
|
|
39
39
|
|
|
40
40
|
# Каталог предложений у дерева свой. Заданный пустым — отказ дерева от требования: дереву, не
|
|
41
|
-
# берущему переносимый слой правил, гард не навязывается.
|
|
41
|
+
# берущему переносимый слой правил, гард не навязывается. Каталога нет на диске — то же самое:
|
|
42
|
+
# дерево механизмом не пользуется, и стража ему не навязывают. Судить по одной переменной значило
|
|
43
|
+
# бы требовать отправки там, где отправлять нечем и некуда.
|
|
44
|
+
root="${CLAUDE_PROJECT_DIR:-.}"
|
|
42
45
|
proposals_dir="${RT_PROPOSALS_DIR-.claude/rt-kit/proposals}"
|
|
43
46
|
[ -z "$proposals_dir" ] && exit 0
|
|
47
|
+
[ -d "$root/$proposals_dir" ] || exit 0
|
|
44
48
|
|
|
45
49
|
# Просьба владельца. Набор открыт и пополняется правкой: полнота его — открытый вопрос
|
|
46
50
|
# договорённости, а не обещание.
|
|
47
|
-
|
|
51
|
+
#
|
|
52
|
+
# Латиница читается наравне: заход, который владелец ведёт по-английски, отличается от русского
|
|
53
|
+
# словами, а требование в нём то же. Своей ветки ей не заводится — обе пары стоят в одном
|
|
54
|
+
# образце, иначе одну из них правили бы, забывая про вторую.
|
|
55
|
+
asked_re='(завед|напиш|отправ|пошл|зашл|отошл|выгруз|send|file|open|submit|raise|report)[а-яёa-z]*[^.!?]{0,40}(пропозал|предложени|proposal)|(пропозал|предложени|proposal)[а-яёa-z]*[^.!?]{0,40}(завед|напиш|отправ|пошл|зашл|отошл|выгруз|send|file|open|submit|raise|report)'
|
|
48
56
|
|
|
49
57
|
# Сосед, без которого слово «предложение» не считается просьбой о слое правил.
|
|
50
|
-
context_re='пропозал|слою правил|слоя правил|слой правил|пакет|agent-kit
|
|
58
|
+
context_re='пропозал|слою правил|слоя правил|слой правил|пакет|agent-kit|наверх|proposal|rule layer|upstream|package'
|
|
51
59
|
|
|
52
60
|
# Ход — это всё, что записано после последнего настоящего ввода владельца. Ответ инструмента
|
|
53
61
|
# приходит той же ролью, поэтому строки с `tool_result` вводом не считаются.
|
|
@@ -86,7 +94,6 @@ verdict="$(tail -n 400 "$transcript" 2>/dev/null | jq -s -r --arg asked "$asked_
|
|
|
86
94
|
# зависимостью, зовёт бинарь из зависимостей; дерево, где пакет живёт исходниками, бинаря не
|
|
87
95
|
# имеет вовсе — там зовут собранный bin. Названная наугад команда стоит исполнителю хода: отказ
|
|
88
96
|
# читается как указание, и вызов `npx agent-kit` отвечает в таком дереве отказом установки.
|
|
89
|
-
root="${CLAUDE_PROJECT_DIR:-.}"
|
|
90
97
|
if [ -x "$root/node_modules/.bin/agent-kit" ]; then
|
|
91
98
|
propose_cmd="npx agent-kit propose"
|
|
92
99
|
else
|
|
@@ -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, hooks/deny-tail.sh
|
|
3
|
+
# Требует: hooks/profile-check.sh, hooks/deny-tail.sh, hooks/write-targets.sh
|
|
4
4
|
# Гард места правки: слой правил чинится там, где сломано, а не там, где виден.
|
|
5
5
|
#
|
|
6
6
|
# Слой правил правит тот же исполнитель, которым слой правил управляет, и разницы между
|
|
@@ -53,18 +53,13 @@ deny() {
|
|
|
53
53
|
exit 0
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
# Цели
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
s/(^|.*[[:space:]])sed[[:space:]]+-i[[:space:]]+([^[:space:]]+[[:space:]]+)*([^[:space:]|&;]+)$/\3/p
|
|
64
|
-
s/(^|.*[[:space:]])(cp|mv|install)[[:space:]]+([^[:space:]]+[[:space:]]+)+([^[:space:]|&;]+).*/\4/p
|
|
65
|
-
' \
|
|
66
|
-
| sort -u
|
|
67
|
-
}
|
|
56
|
+
# Цели записи разбирает общий помощник: тот же признак нужен гарду экзамена, и разойдясь, две
|
|
57
|
+
# копии пропустили бы разные формы записи. Файла нет — остаётся молчаливое умолчание, чтобы гард
|
|
58
|
+
# не сломался на неполной раскладке.
|
|
59
|
+
# shellcheck disable=SC1090
|
|
60
|
+
[ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/write-targets.sh" ] \
|
|
61
|
+
&& . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/write-targets.sh" 2>/dev/null
|
|
62
|
+
command -v rt_write_targets >/dev/null 2>&1 || rt_write_targets() { cat >/dev/null; }
|
|
68
63
|
|
|
69
64
|
tool="$(rt_hook_tool)"
|
|
70
65
|
candidates=""
|
|
@@ -52,6 +52,28 @@ emit() {
|
|
|
52
52
|
# Ветка под задачу без папки — работа идёт мимо. Сессию не рвём: SessionStart, отбивающий
|
|
53
53
|
# запуск, оставляет владельца без агента вовсе, а правку кода поймает `task-flow-guard`.
|
|
54
54
|
if [ ! -d "$DIR" ]; then
|
|
55
|
+
# Папки нет по двум разным причинам, и говорить о них надо разное. Первая — работа шла мимо
|
|
56
|
+
# правила. Вторая — папку разобрала сама ветка последним коммитом перед заявкой: это законный
|
|
57
|
+
# исход, и указание собрать её заново уводит заход с хвоста работы обратно в её начало.
|
|
58
|
+
#
|
|
59
|
+
# Различает их история ветки: снос папки её же коммитом после общего предка с главной. Ход
|
|
60
|
+
# работы к этой минуте уехал вместе с папкой, и состояние держат заявка и передача захода.
|
|
61
|
+
main_branch="${RT_MAIN_BRANCH:-main}"
|
|
62
|
+
base="$(git merge-base "origin/${main_branch}" HEAD 2>/dev/null || git merge-base "$main_branch" HEAD 2>/dev/null)"
|
|
63
|
+
dropped=''
|
|
64
|
+
[ -n "$base" ] && dropped="$(git log "${base}..HEAD" --diff-filter=D --name-only --pretty=format: -- "$DIR" 2>/dev/null | head -1)"
|
|
65
|
+
|
|
66
|
+
if [ -n "$dropped" ]; then
|
|
67
|
+
{
|
|
68
|
+
printf 'РАБОТА ЗАКРЫВАЕТСЯ — папка задачи разобрана этой веткой.\n\n'
|
|
69
|
+
printf 'Ход работы удалён вместе с папкой: состояние держат заявка и передача захода.\n'
|
|
70
|
+
printf 'Папка заново не собирается. Правка кода после разбора требует восстановить её\n'
|
|
71
|
+
printf 'на время правки и повторить разбор тем же коммитом. Порядок — скил `task-flow`,\n'
|
|
72
|
+
printf 'паттерны `task-flow-close` и `task-flow-archive`.\n'
|
|
73
|
+
} | emit
|
|
74
|
+
exit 0
|
|
75
|
+
fi
|
|
76
|
+
|
|
55
77
|
if rt_needs rt_task_branch_ok task-context-load && rt_task_branch_ok "$branch"; then
|
|
56
78
|
{
|
|
57
79
|
printf 'РАБОТА БЕЗ ПАПКИ ЗАДАЧИ.\n\n'
|
|
@@ -97,6 +97,15 @@ rt_task_flow_context() {
|
|
|
97
97
|
' 2>/dev/null)"
|
|
98
98
|
[ -n "$rt_tf_inner" ] && rt_tf_cmd="$rt_tf_inner"
|
|
99
99
|
fi
|
|
100
|
+
# Команда заведения задачи не судится вовсе. Она пишет папку задачи и карточку в
|
|
101
|
+
# очереди работ, а текст её несёт тело задачи целиком: цитата со знаком «больше»
|
|
102
|
+
# подходит под признак записи, путь к коду в прозе тела — под признак пути. Отбитая,
|
|
103
|
+
# она отбивается тем самым гардом, который печатает её в тексте своего отказа, и
|
|
104
|
+
# завести задачу становится нечем — ни с ветки закрытой задачи, ни с главной. Имя
|
|
105
|
+
# команды берётся у профиля дерева: своя копия разошлась бы с ним молча.
|
|
106
|
+
case "$rt_tf_cmd" in
|
|
107
|
+
*"${RT_TASK_NEW_CMD:-npm run task:new}"*) return 1 ;;
|
|
108
|
+
esac
|
|
100
109
|
rt_needs rt_shell_writes task-flow-guard || return 1
|
|
101
110
|
rt_needs rt_shell_paths task-flow-guard || return 1
|
|
102
111
|
rt_shell_writes "$rt_tf_cmd" || return 1
|
|
@@ -89,4 +89,21 @@ if git -C "$root" log --oneline -1 -- "$draft" 2>/dev/null | grep -q .; then
|
|
|
89
89
|
exit 0
|
|
90
90
|
fi
|
|
91
91
|
|
|
92
|
+
# Вливание бывает и первым коммитом, который вообще завёл названный путь: черновик писали, не
|
|
93
|
+
# коммитя, а в историю уехал уже спек домена. Тогда истории у пути нет, а работа сделана ровно
|
|
94
|
+
# так, как велит паттерн закрытия, — и отказ запирал бы ветку на последнем шаге. Признак:
|
|
95
|
+
# договорённость названа путём вида `<домен>/proposed/<фича>`, каталог домена в дереве есть, и
|
|
96
|
+
# имя фичи в нём встречается. Пока такого спека нет, отказ остаётся прежним.
|
|
97
|
+
case "$draft" in
|
|
98
|
+
*/proposed/*)
|
|
99
|
+
domain_dir="${draft%%/proposed/*}"
|
|
100
|
+
feature="${draft##*/proposed/}"
|
|
101
|
+
feature="${feature%/}"
|
|
102
|
+
if [ -n "$feature" ] && [ -d "$root/$domain_dir" ] \
|
|
103
|
+
&& grep -rq -- "$feature" "$root/$domain_dir" 2>/dev/null; then
|
|
104
|
+
exit 0
|
|
105
|
+
fi
|
|
106
|
+
;;
|
|
107
|
+
esac
|
|
108
|
+
|
|
92
109
|
deny "BLOCKED by task-flow: замысел называет договорённость '${draft}', а её на диске нет и в истории ветки не было. Заведи её с образца (docs/specs/_template) или поправь путь в '${tasks_dir}/${branch}/plan.md'. Правило — скил task-flow."
|
|
@@ -38,6 +38,27 @@
|
|
|
38
38
|
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
|
|
39
39
|
|
|
40
40
|
rt_hook_read
|
|
41
|
+
|
|
42
|
+
# Отказ печатается одной формой: причина, общий хвост о двух законных ходах и запасная строка
|
|
43
|
+
# на случай, если разборщика нет. Прежде эти двенадцать строк стояли восемью копиями — файл рос
|
|
44
|
+
# на каждом новом ярусе быстрее, чем на самом требовании.
|
|
45
|
+
rt_te_deny() {
|
|
46
|
+
rt_te_reason="$1"
|
|
47
|
+
rt_te_short="$2"
|
|
48
|
+
# shellcheck disable=SC1090
|
|
49
|
+
[ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" ] \
|
|
50
|
+
&& . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" 2>/dev/null
|
|
51
|
+
command -v rt_deny_tail >/dev/null 2>&1 || rt_deny_tail() { :; }
|
|
52
|
+
rt_te_tail="$(rt_deny_tail "")"
|
|
53
|
+
[ -n "$rt_te_tail" ] && rt_te_reason="${rt_te_reason}
|
|
54
|
+
|
|
55
|
+
${rt_te_tail}"
|
|
56
|
+
|
|
57
|
+
jq -n --arg r "$rt_te_reason" '{decision:"block",reason:$r}' 2>/dev/null \
|
|
58
|
+
|| printf '{"decision":"block","reason":"turn-exit-guard: %s"}\n' "$rt_te_short"
|
|
59
|
+
exit 0
|
|
60
|
+
}
|
|
61
|
+
|
|
41
62
|
input="$RT_HOOK_INPUT"
|
|
42
63
|
[ -z "$input" ] && exit 0
|
|
43
64
|
command -v jq >/dev/null 2>&1 || exit 0
|
|
@@ -97,20 +118,7 @@ if [ "$state" = "замысел-записан" ]; then
|
|
|
97
118
|
|
|
98
119
|
Страж судит один ход: следующий заход не отбивается."
|
|
99
120
|
|
|
100
|
-
|
|
101
|
-
# а причина отказа остаётся прежней.
|
|
102
|
-
# shellcheck disable=SC1090
|
|
103
|
-
[ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" ] \
|
|
104
|
-
&& . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" 2>/dev/null
|
|
105
|
-
command -v rt_deny_tail >/dev/null 2>&1 || rt_deny_tail() { :; }
|
|
106
|
-
deny_tail_text="$(rt_deny_tail "")"
|
|
107
|
-
[ -n "$deny_tail_text" ] && reason="${reason}
|
|
108
|
-
|
|
109
|
-
${deny_tail_text}"
|
|
110
|
-
|
|
111
|
-
jq -n --arg r "$reason" '{decision:"block",reason:$r}' 2>/dev/null \
|
|
112
|
-
|| printf '{"decision":"block","reason":"turn-exit-guard: замысел записан, а первый этап не начат."}\n'
|
|
113
|
-
exit 0
|
|
121
|
+
rt_te_deny "$reason" "замысел записан, а первый этап не начат."
|
|
114
122
|
fi
|
|
115
123
|
|
|
116
124
|
# То же самое, но объявить это на диске уже нечем: папка задачи разбирается до открытия заявки,
|
|
@@ -277,18 +285,7 @@ if [ "$archived" != "true" ] && [ -z "$progress" ] && [ -n "$branch" ] && [ ! -d
|
|
|
277
285
|
|
|
278
286
|
Страж судит один ход: следующий заход не отбивается."
|
|
279
287
|
|
|
280
|
-
|
|
281
|
-
[ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" ] \
|
|
282
|
-
&& . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" 2>/dev/null
|
|
283
|
-
command -v rt_deny_tail >/dev/null 2>&1 || rt_deny_tail() { :; }
|
|
284
|
-
deny_tail_text="$(rt_deny_tail "")"
|
|
285
|
-
[ -n "$deny_tail_text" ] && reason="${reason}
|
|
286
|
-
|
|
287
|
-
${deny_tail_text}"
|
|
288
|
-
|
|
289
|
-
jq -n --arg r "$reason" '{decision:"block",reason:$r}' 2>/dev/null \
|
|
290
|
-
|| printf '{"decision":"block","reason":"turn-exit-guard: работа взята и не начата — напиши замысел."}\n'
|
|
291
|
-
exit 0
|
|
288
|
+
rt_te_deny "$reason" "работа взята и не начата — напиши замысел."
|
|
292
289
|
fi
|
|
293
290
|
fi
|
|
294
291
|
|
|
@@ -327,20 +324,7 @@ EOF
|
|
|
327
324
|
Запусти их этим же ходом либо верни прежний номер этапа в ход работы.
|
|
328
325
|
|
|
329
326
|
Страж судит один ход: следующий заход не отбивается."
|
|
330
|
-
|
|
331
|
-
# а причина отказа остаётся прежней.
|
|
332
|
-
# shellcheck disable=SC1090
|
|
333
|
-
[ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" ] \
|
|
334
|
-
&& . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" 2>/dev/null
|
|
335
|
-
command -v rt_deny_tail >/dev/null 2>&1 || rt_deny_tail() { :; }
|
|
336
|
-
deny_tail_text="$(rt_deny_tail "")"
|
|
337
|
-
[ -n "$deny_tail_text" ] && reason="${reason}
|
|
338
|
-
|
|
339
|
-
${deny_tail_text}"
|
|
340
|
-
|
|
341
|
-
jq -n --arg r "$reason" '{decision:"block",reason:$r}' 2>/dev/null \
|
|
342
|
-
|| printf '{"decision":"block","reason":"turn-exit-guard: закрытый этап не подтверждён выводом команды."}\n'
|
|
343
|
-
exit 0
|
|
327
|
+
rt_te_deny "$reason" "закрытый этап не подтверждён выводом команды."
|
|
344
328
|
fi
|
|
345
329
|
fi
|
|
346
330
|
|
|
@@ -363,18 +347,7 @@ if [ "$handed_over" = "true" ] && [ "$started_next" != "true" ]; then
|
|
|
363
347
|
|
|
364
348
|
Страж судит один ход: следующий заход не отбивается."
|
|
365
349
|
|
|
366
|
-
|
|
367
|
-
[ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" ] \
|
|
368
|
-
&& . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" 2>/dev/null
|
|
369
|
-
command -v rt_deny_tail >/dev/null 2>&1 || rt_deny_tail() { :; }
|
|
370
|
-
deny_tail_text="$(rt_deny_tail "")"
|
|
371
|
-
[ -n "$deny_tail_text" ] && reason="${reason}
|
|
372
|
-
|
|
373
|
-
${deny_tail_text}"
|
|
374
|
-
|
|
375
|
-
jq -n --arg r "$reason" '{decision:"block",reason:$r}' 2>/dev/null \
|
|
376
|
-
|| printf '{"decision":"block","reason":"turn-exit-guard: работа отдана, а следующая не начата."}\n'
|
|
377
|
-
exit 0
|
|
350
|
+
rt_te_deny "$reason" "работа отдана, а следующая не начата."
|
|
378
351
|
fi
|
|
379
352
|
|
|
380
353
|
# Ход кончился ожиданием чужого шага. Работа в нём была — тем он и обманчив: полон, и пустоты за
|
|
@@ -390,18 +363,7 @@ if [ "$waited" = "true" ]; then
|
|
|
390
363
|
|
|
391
364
|
Страж судит один ход: следующий заход не отбивается."
|
|
392
365
|
|
|
393
|
-
|
|
394
|
-
[ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" ] \
|
|
395
|
-
&& . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" 2>/dev/null
|
|
396
|
-
command -v rt_deny_tail >/dev/null 2>&1 || rt_deny_tail() { :; }
|
|
397
|
-
deny_tail_text="$(rt_deny_tail "")"
|
|
398
|
-
[ -n "$deny_tail_text" ] && reason="${reason}
|
|
399
|
-
|
|
400
|
-
${deny_tail_text}"
|
|
401
|
-
|
|
402
|
-
jq -n --arg r "$reason" '{decision:"block",reason:$r}' 2>/dev/null \
|
|
403
|
-
|| printf '{"decision":"block","reason":"turn-exit-guard: ход кончился ожиданием чужого шага."}\n'
|
|
404
|
-
exit 0
|
|
366
|
+
rt_te_deny "$reason" "ход кончился ожиданием чужого шага."
|
|
405
367
|
fi
|
|
406
368
|
|
|
407
369
|
# Общий рубеж. Работа за ход была — но последним действием стал не она, а текст владельцу.
|
|
@@ -417,18 +379,26 @@ if [ "$worked" = "true" ] && [ "$ended_working" != "true" ]; then
|
|
|
417
379
|
|
|
418
380
|
Страж судит один ход: следующий заход не отбивается."
|
|
419
381
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
&& . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" 2>/dev/null
|
|
423
|
-
command -v rt_deny_tail >/dev/null 2>&1 || rt_deny_tail() { :; }
|
|
424
|
-
deny_tail_text="$(rt_deny_tail "")"
|
|
425
|
-
[ -n "$deny_tail_text" ] && reason="${reason}
|
|
382
|
+
rt_te_deny "$reason" "последним действием хода стала не работа."
|
|
383
|
+
fi
|
|
426
384
|
|
|
427
|
-
|
|
385
|
+
# Работа, оставшаяся в рабочем дереве. Ветка ушла вперёд своей удалённой ссылки, а заявки при
|
|
386
|
+
# ней в этом ходе не открывали: сделанное лежит там, где его не видит никто, кроме сделавшего.
|
|
387
|
+
# Признак читается без сети и молчит там, где ссылки нет вовсе — под пробу заводят и такие ветки.
|
|
388
|
+
# Разбор — `docs/postmortems/2026-08-25-fifteen-branches-over-one-index.md`.
|
|
389
|
+
if [ "$worked" = "true" ] && [ "$handed_over" != "true" ]; then
|
|
390
|
+
unpushed="$(git -C "$root" rev-list --count '@{u}..HEAD' 2>/dev/null)"
|
|
391
|
+
if [ -n "$unpushed" ] && [ "$unpushed" -gt 0 ] 2>/dev/null; then
|
|
392
|
+
reason="BLOCKED by turn-exit-guard: за ход была работа, но она осталась в рабочем дереве — неотданных коммитов ${unpushed}.
|
|
428
393
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
394
|
+
Владелец видит прежнее состояние и читает его как «не сделано ничего». Ходов отсюда два: довести работу до хостинга — запушить ветку и открыть заявку — либо снять сделанное, если оно не нужно.
|
|
395
|
+
|
|
396
|
+
Оставшееся в дереве называется причиной — словом владельца, а не списком остатков.
|
|
397
|
+
|
|
398
|
+
Страж судит один ход: следующий заход не отбивается."
|
|
399
|
+
|
|
400
|
+
rt_te_deny "$reason" "работа осталась в рабочем дереве."
|
|
401
|
+
fi
|
|
432
402
|
fi
|
|
433
403
|
|
|
434
404
|
[ "$worked" = "true" ] && exit 0
|
|
@@ -463,18 +433,6 @@ else
|
|
|
463
433
|
Страж судит один ход: следующий заход не отбивается."
|
|
464
434
|
fi
|
|
465
435
|
|
|
466
|
-
|
|
467
|
-
# а причина отказа остаётся прежней.
|
|
468
|
-
# shellcheck disable=SC1090
|
|
469
|
-
[ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" ] \
|
|
470
|
-
&& . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" 2>/dev/null
|
|
471
|
-
command -v rt_deny_tail >/dev/null 2>&1 || rt_deny_tail() { :; }
|
|
472
|
-
deny_tail_text="$(rt_deny_tail "")"
|
|
473
|
-
[ -n "$deny_tail_text" ] && reason="${reason}
|
|
474
|
-
|
|
475
|
-
${deny_tail_text}"
|
|
476
|
-
|
|
477
|
-
jq -n --arg r "$reason" '{decision:"block",reason:$r}' 2>/dev/null \
|
|
478
|
-
|| printf '{"decision":"block","reason":"turn-exit-guard: работа не кончена — следующий шаг стоит в ходе работы."}\n'
|
|
436
|
+
rt_te_deny "$reason" "работа не кончена — следующий шаг стоит в ходе работы."
|
|
479
437
|
|
|
480
438
|
exit 0
|
|
@@ -69,13 +69,20 @@ moved_re='task:new|task:move|checkout[[:space:]]+-b|docs/tasks/'
|
|
|
69
69
|
# следующая задача была взята вместо этого, а не сверх этого.
|
|
70
70
|
ready_re='pr[[:space:]]+ready|run[[:space:]]+(list|view|watch)|pr[[:space:]]+checks|check-runs|check:board|board\.mjs'
|
|
71
71
|
|
|
72
|
+
# Своё названное действие. Пустой ход, объявивший, что сделает дальше, ничем себя не выдаёт:
|
|
73
|
+
# заявку он не открывал, прогона не читал, и оба прежних признака молчат. Ловится он формой —
|
|
74
|
+
# набором образцов будущего времени о собственном шаге, — а не пониманием смысла; за словами
|
|
75
|
+
# при этом не должно стоять ни одного вызова, иначе объявление сказано по ходу работы, а не
|
|
76
|
+
# вместо неё.
|
|
77
|
+
vow_re='дальше беру|дальше возьму|дальше иду|следующим шагом|следующий шаг:|затем сделаю|затем возьму|потом сделаю|после этого сделаю|далее беру'
|
|
78
|
+
|
|
72
79
|
# Ход — это всё, что записано после последнего настоящего ввода владельца. Ответ инструмента
|
|
73
80
|
# приходит той же ролью, поэтому строки с `tool_result` вводом не считаются.
|
|
74
81
|
#
|
|
75
82
|
# Хвост в 400 строк: запись хода растёт всю сессию, а судится только последний ход.
|
|
76
83
|
verdict="$(tail -n 400 "$transcript" 2>/dev/null | jq -s -r \
|
|
77
84
|
--arg opened "$opened_re" --arg moved "$moved_re" --arg read "$read_re" --arg red "$red_re" \
|
|
78
|
-
--arg ready "$ready_re" '
|
|
85
|
+
--arg ready "$ready_re" --arg vow "$vow_re" '
|
|
79
86
|
def is_input:
|
|
80
87
|
.type == "user"
|
|
81
88
|
and (((.message.content // []) | if type == "array"
|
|
@@ -98,13 +105,69 @@ verdict="$(tail -n 400 "$transcript" 2>/dev/null | jq -s -r \
|
|
|
98
105
|
| (($ran | test($read; "i")) and ($out | test($red; "i"))) as $red_run
|
|
99
106
|
| ($ran | test($moved; "i")) as $went_on
|
|
100
107
|
| ($ran | test($ready; "i")) as $checked
|
|
101
|
-
|
|
|
108
|
+
| ([$turn[] | select(.type == "assistant") | (.message.content // [])[]
|
|
109
|
+
| select(.type == "text") | .text] | join("\n")) as $said
|
|
110
|
+
| ($said | test($vow; "i")) as $announced
|
|
111
|
+
| ($uses | length) as $tools
|
|
112
|
+
| if $opened_pr and ($went_on | not) and ($checked | not) then "owe:both"
|
|
113
|
+
elif $opened_pr and ($went_on | not) then "owe:pr"
|
|
102
114
|
elif $opened_pr and ($checked | not) then "owe:draft"
|
|
103
115
|
elif $went_on then "pass"
|
|
104
116
|
elif $red_run then "owe:run"
|
|
117
|
+
elif $announced and $tools == 0 then "owe:vow"
|
|
105
118
|
else "pass" end
|
|
106
119
|
' 2>/dev/null)"
|
|
107
120
|
|
|
121
|
+
# Ни одного из двух действий: отказ называет оба разом. Прежде он называл только первое, и
|
|
122
|
+
# снимался тоже первым — исполнитель брал следующую задачу, признак открытой заявки уходил
|
|
123
|
+
# вместе с ходом, и второе требование испарялось, ни разу не прозвучав.
|
|
124
|
+
if [ "$verdict" = "owe:both" ]; then
|
|
125
|
+
reason="BLOCKED by waiting-turn-guard: в этом ходе открыт PR, а по отданной работе не сделано ни одного из двух действий — ни состояние её не спрошено, ни следующая задача не взята.
|
|
126
|
+
|
|
127
|
+
Действий именно два, и снять отказ одним нельзя: отданное доводится до снятого черновика тем, кто его отдал, а следующая берётся сверх этого, а не вместо. Взятая следующая уносит признак открытой заявки с собой — второе требование после неё не прозвучит уже никогда.
|
|
128
|
+
|
|
129
|
+
gh run list # состояние отданного
|
|
130
|
+
npm run task:new -- <заголовок> # следующая работа
|
|
131
|
+
|
|
132
|
+
Гард судит один ход: следующий заход не отбивается."
|
|
133
|
+
|
|
134
|
+
# shellcheck disable=SC1090
|
|
135
|
+
[ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" ] \
|
|
136
|
+
&& . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" 2>/dev/null
|
|
137
|
+
command -v rt_deny_tail >/dev/null 2>&1 || rt_deny_tail() { :; }
|
|
138
|
+
deny_tail_text="$(rt_deny_tail "")"
|
|
139
|
+
[ -n "$deny_tail_text" ] && reason="${reason}
|
|
140
|
+
|
|
141
|
+
${deny_tail_text}"
|
|
142
|
+
|
|
143
|
+
jq -n --arg r "$reason" '{decision:"block",reason:$r}' 2>/dev/null \
|
|
144
|
+
|| printf '{"decision":"block","reason":"waiting-turn-guard: по отданной работе не сделано ни одного из двух действий."}\n'
|
|
145
|
+
exit 0
|
|
146
|
+
fi
|
|
147
|
+
|
|
148
|
+
# Ход, объявивший своё следующее действие и не сделавший по нему ничего. Отказ называет само
|
|
149
|
+
# объявление: исполнитель, которому сказано «ход пуст», перепишет слова, а не сделает шаг.
|
|
150
|
+
if [ "$verdict" = "owe:vow" ]; then
|
|
151
|
+
reason="BLOCKED by waiting-turn-guard: за ход не сделано ни одной правки и не позвана ни одна команда, а следующее действие названо словами — «дальше беру», «следующим шагом», «затем сделаю».
|
|
152
|
+
|
|
153
|
+
Объявление своего же шага работой не бывает: оно точнее всякого обещания и пустоты за ним не видно никому. Сделай названное этим же ходом — либо скажи владельцу, что работу останавливает, и назови, чем именно.
|
|
154
|
+
|
|
155
|
+
Гард судит один ход: следующий заход не отбивается."
|
|
156
|
+
|
|
157
|
+
# shellcheck disable=SC1090
|
|
158
|
+
[ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" ] \
|
|
159
|
+
&& . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" 2>/dev/null
|
|
160
|
+
command -v rt_deny_tail >/dev/null 2>&1 || rt_deny_tail() { :; }
|
|
161
|
+
deny_tail_text="$(rt_deny_tail "")"
|
|
162
|
+
[ -n "$deny_tail_text" ] && reason="${reason}
|
|
163
|
+
|
|
164
|
+
${deny_tail_text}"
|
|
165
|
+
|
|
166
|
+
jq -n --arg r "$reason" '{decision:"block",reason:$r}' 2>/dev/null \
|
|
167
|
+
|| printf '{"decision":"block","reason":"waiting-turn-guard: следующее действие названо словами, а за ход не сделано ничего."}\n'
|
|
168
|
+
exit 0
|
|
169
|
+
fi
|
|
170
|
+
|
|
108
171
|
# Черновик, оставленный при взятой следующей задаче, — отдельный отказ: там требование не про
|
|
109
172
|
# следующую задачу, а про доведение отданной.
|
|
110
173
|
if [ "$verdict" = "owe:draft" ]; then
|