@rt-tools/agent-kit 0.9.1 → 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.
Files changed (67) hide show
  1. package/README.md +5 -0
  2. package/assets/agents/conscience.md +58 -0
  3. package/assets/agents/prose-editor.md +44 -0
  4. package/assets/agents/strict-teacher.md +59 -0
  5. package/assets/checks/board.github.mjs +56 -1
  6. package/assets/checks/check-board.github.mjs +24 -0
  7. package/assets/checks/check-doc-paths.mjs +4 -15
  8. package/assets/checks/check-dupes.mjs +5 -6
  9. package/assets/checks/check-file-size.mjs +6 -20
  10. package/assets/checks/check-prose-style.mjs +137 -0
  11. package/assets/checks/check-reuse.mjs +5 -5
  12. package/assets/checks/check-styles.mjs +5 -5
  13. package/assets/checks/lib-common.mjs +3 -3
  14. package/assets/checks/rt-kit-checks.config.mjs +85 -1
  15. package/assets/defaults/project.sh +50 -0
  16. package/assets/docs/GLOSSARY.md +17 -15
  17. package/assets/hooks/browser-guard-device-id.sh +9 -1
  18. package/assets/hooks/browser-guard-no-asking.sh +9 -1
  19. package/assets/hooks/browser-guard-no-listing.sh +9 -1
  20. package/assets/hooks/browser-guard-no-other-drivers.sh +7 -1
  21. package/assets/hooks/browser-guard-require-select.sh +11 -2
  22. package/assets/hooks/claim-guard.sh +113 -0
  23. package/assets/hooks/conscience-guard.sh +98 -0
  24. package/assets/hooks/deny-tail.sh +32 -0
  25. package/assets/hooks/dev-server-guard.sh +8 -2
  26. package/assets/hooks/docs-guard.sh +14 -2
  27. package/assets/hooks/exam-guard.sh +121 -0
  28. package/assets/hooks/git-guard-delivery-signature.sh +75 -0
  29. package/assets/hooks/git-guard-delivery.sh +154 -68
  30. package/assets/hooks/git-guard-main.sh +12 -0
  31. package/assets/hooks/git-guard-push-tests.sh +49 -2
  32. package/assets/hooks/grill-gate.sh +12 -0
  33. package/assets/hooks/handoff-entry-guard.sh +71 -0
  34. package/assets/hooks/postmortem-guard.sh +12 -0
  35. package/assets/hooks/proposal-guard.sh +12 -0
  36. package/assets/hooks/prose-style-guard.sh +73 -0
  37. package/assets/hooks/qa-dataid-guard.sh +12 -1
  38. package/assets/hooks/rerun-guard.sh +86 -0
  39. package/assets/hooks/reuse-first-guard.sh +12 -1
  40. package/assets/hooks/roles.sh +32 -0
  41. package/assets/hooks/skill-gate.sh +12 -0
  42. package/assets/hooks/sql-guard-write.sh +2 -1
  43. package/assets/hooks/sql-guard.sh +12 -1
  44. package/assets/hooks/task-flow-guard.sh +71 -7
  45. package/assets/hooks/turn-exit-guard.sh +189 -0
  46. package/assets/hooks/waiting-turn-guard.sh +12 -0
  47. package/assets/hooks/window-fill-guard.sh +12 -1
  48. package/assets/patterns/task-flow-close.md +8 -8
  49. package/assets/patterns/task-flow-handoff.md +1 -1
  50. package/assets/patterns/task-flow-resume.md +30 -4
  51. package/assets/patterns/task-flow-start.md +15 -6
  52. package/assets/rules/git-workflow.azure.md +36 -9
  53. package/assets/rules/git-workflow.github.md +74 -9
  54. package/assets/rules/git-workflow.gitlab.md +40 -12
  55. package/assets/rules/task-flow.md +123 -71
  56. package/assets/rules/testing.md +15 -1
  57. package/assets/samples/tasks/_template/plan.md +4 -1
  58. package/assets/samples/tasks/_template/progress.md +1 -0
  59. package/assets/skills/agent-kit.md +33 -0
  60. package/assets/templates/project.sh +16 -0
  61. package/lib/config.d.ts +8 -0
  62. package/lib/config.d.ts.map +1 -1
  63. package/lib/config.js +1 -0
  64. package/lib/config.js.map +1 -1
  65. package/package.json +1 -1
  66. package/rt-tools-agent-kit-0.10.0.tgz +0 -0
  67. package/rt-tools-agent-kit-0.9.1.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 с тем же условием, покажи пользователю, что попадает под удаление, и помечай запрос маркером destructive-ok с объяснением."
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
- jq -n --arg r "$1" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$r}}' 2>/dev/null \
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
- # (решения в законе `docs/constitution/work-conduct.md`).
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
- jq -n --arg r "$1" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$r}}' 2>/dev/null \
107
- || printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"%s"}}\n' "$1"
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/<фича>/ и укажи строкой '**Драфт:** \`путь\`'. Если правка поведения не меняет — поставь '**Поведение:** не меняется — <причина владельца>'. Правило — скил task-flow."
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