@rt-tools/agent-kit 0.16.1 → 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.
Files changed (136) hide show
  1. package/assets/checks/board-epics.github.mjs +123 -0
  2. package/assets/checks/board-gh.github.mjs +121 -0
  3. package/assets/checks/board-paths.github.mjs +88 -0
  4. package/assets/checks/board-runs.github.mjs +8 -0
  5. package/assets/checks/board-titles.github.mjs +66 -0
  6. package/assets/checks/board.github.mjs +77 -73
  7. package/assets/checks/check-board.github.mjs +56 -7
  8. package/assets/checks/check-file-size.mjs +10 -2
  9. package/assets/checks/check-glossary.mjs +170 -0
  10. package/assets/checks/check-hook-scope.mjs +126 -0
  11. package/assets/checks/check-profile-drift.mjs +195 -0
  12. package/assets/checks/check-push-gate.mjs +59 -1
  13. package/assets/checks/check-schema-drift.mjs +12 -5
  14. package/assets/checks/check-specs.mjs +1 -1
  15. package/assets/checks/rt-kit-checks.config.mjs +13 -0
  16. package/assets/checks/spec-anchors.mjs +2 -2
  17. package/assets/checks/spec-contract.mjs +9 -0
  18. package/assets/defaults/gate-map.sh +13 -4
  19. package/assets/defaults/project.sh +31 -108
  20. package/assets/defaults/shell.sh +139 -0
  21. package/assets/docs/GLOSSARY.md +1 -1
  22. package/assets/hooks/browser-device-id.sh +20 -4
  23. package/assets/hooks/browser-guard-device-id.sh +5 -2
  24. package/assets/hooks/browser-guard-no-other-drivers.sh +41 -2
  25. package/assets/hooks/claim-guard.sh +22 -1
  26. package/assets/hooks/docs-guard.sh +1 -1
  27. package/assets/hooks/exam-guard.sh +85 -12
  28. package/assets/hooks/git-guard-delivery-conflict.sh +85 -0
  29. package/assets/hooks/git-guard-delivery-folder.sh +19 -0
  30. package/assets/hooks/git-guard-delivery-signature.sh +14 -5
  31. package/assets/hooks/git-guard-delivery.sh +54 -2
  32. package/assets/hooks/git-guard-push-tests.sh +21 -1
  33. package/assets/hooks/grill-gate-ask.sh +25 -0
  34. package/assets/hooks/grill-gate.sh +75 -11
  35. package/assets/hooks/lint-after-edit.sh +44 -16
  36. package/assets/hooks/proposal-guard.sh +11 -4
  37. package/assets/hooks/rule-source-guard.sh +8 -13
  38. package/assets/hooks/skill-gate-layers.sh +7 -0
  39. package/assets/hooks/skill-gate.sh +29 -0
  40. package/assets/hooks/task-context-load.sh +32 -0
  41. package/assets/hooks/task-flow-context.sh +194 -0
  42. package/assets/hooks/task-flow-draft-guard.sh +109 -0
  43. package/assets/hooks/task-flow-guard.sh +27 -159
  44. package/assets/hooks/turn-exit-guard.sh +53 -88
  45. package/assets/hooks/waiting-turn-guard.sh +65 -2
  46. package/assets/hooks/window-fill-guard.sh +5 -1
  47. package/assets/hooks/work-start-guard.sh +166 -0
  48. package/assets/hooks/write-targets.sh +24 -0
  49. package/assets/laws/delivery.md +19 -0
  50. package/assets/laws/frontend-application.md +4 -0
  51. package/assets/laws/verifiability.md +44 -0
  52. package/assets/laws/work-conduct.md +15 -0
  53. package/assets/patterns/browser-verification-measure.md +3 -1
  54. package/assets/patterns/browser-verification-stand.md +17 -4
  55. package/assets/patterns/doc-style-write.md +62 -2
  56. package/assets/patterns/git-workflow-commit.github.md +33 -5
  57. package/assets/patterns/git-workflow-docker.md +22 -0
  58. package/assets/patterns/git-workflow-freshness.md +87 -0
  59. package/assets/patterns/git-workflow-merge.md +41 -0
  60. package/assets/patterns/git-workflow-migration.md +11 -0
  61. package/assets/patterns/git-workflow-pr.github.md +12 -1
  62. package/assets/patterns/git-workflow-restart.md +18 -0
  63. package/assets/patterns/git-workflow-secrets.md +14 -0
  64. package/assets/patterns/git-workflow-stack.md +93 -1
  65. package/assets/patterns/lib-layers-move.md +4 -0
  66. package/assets/patterns/spec-driven-domain.md +47 -2
  67. package/assets/patterns/spec-driven-rule.md +16 -4
  68. package/assets/patterns/spec-driven-sweep.md +57 -0
  69. package/assets/patterns/status-report-table.github.md +1 -1
  70. package/assets/patterns/task-flow-archive.md +58 -16
  71. package/assets/patterns/task-flow-close.md +106 -23
  72. package/assets/patterns/task-flow-handoff.md +14 -2
  73. package/assets/patterns/task-flow-resume.md +35 -8
  74. package/assets/patterns/task-flow-start.md +60 -22
  75. package/assets/patterns/testing-e2e.md +23 -0
  76. package/assets/patterns/turn-entry-map.md +1 -1
  77. package/assets/pitfalls/agent-kit.md +71 -3
  78. package/assets/pitfalls/doc-style.md +15 -0
  79. package/assets/pitfalls/git-workflow.github.md +88 -0
  80. package/assets/pitfalls/spec-driven.md +22 -0
  81. package/assets/pitfalls/task-flow.md +113 -0
  82. package/assets/pitfalls/testing.md +6 -0
  83. package/assets/pitfalls/turn-conduct.md +49 -0
  84. package/assets/rules/browser-verification.md +58 -0
  85. package/assets/rules/deploy-flow.azure.md +8 -0
  86. package/assets/rules/deploy-flow.github.md +27 -0
  87. package/assets/rules/deploy-flow.gitlab.md +8 -0
  88. package/assets/rules/doc-style.md +20 -0
  89. package/assets/rules/git-workflow.azure.md +22 -2
  90. package/assets/rules/git-workflow.github.md +127 -118
  91. package/assets/rules/git-workflow.gitlab.md +17 -4
  92. package/assets/rules/lists.md +5 -0
  93. package/assets/rules/observability.needs-app.md +4 -0
  94. package/assets/rules/reuse-first.md +14 -0
  95. package/assets/rules/shared-code.md +5 -0
  96. package/assets/rules/spec-driven.md +33 -15
  97. package/assets/rules/task-flow.md +132 -126
  98. package/assets/rules/testing.md +41 -2
  99. package/assets/rules/turn-conduct.md +73 -57
  100. package/assets/rules/turn-entry.md +6 -0
  101. package/assets/samples/tasks/_template/grill.md +5 -0
  102. package/assets/samples/tasks/_template/plan.md +3 -0
  103. package/assets/skills/agent-kit.md +108 -76
  104. package/assets/templates/postmortem.md +5 -1
  105. package/bin/agent-kit.d.ts.map +1 -1
  106. package/bin/agent-kit.js +30 -6
  107. package/bin/agent-kit.js.map +1 -1
  108. package/lib/catalog.d.ts.map +1 -1
  109. package/lib/catalog.js +2 -1
  110. package/lib/catalog.js.map +1 -1
  111. package/lib/commands.d.ts.map +1 -1
  112. package/lib/commands.js +96 -7
  113. package/lib/commands.js.map +1 -1
  114. package/lib/hooks-map.d.ts +26 -0
  115. package/lib/hooks-map.d.ts.map +1 -1
  116. package/lib/hooks-map.js +58 -2
  117. package/lib/hooks-map.js.map +1 -1
  118. package/lib/sections.d.ts +6 -0
  119. package/lib/sections.d.ts.map +1 -1
  120. package/lib/sections.js +19 -0
  121. package/lib/sections.js.map +1 -1
  122. package/lib/shipment.d.ts +2 -0
  123. package/lib/shipment.d.ts.map +1 -1
  124. package/lib/shipment.fixture.d.ts +5 -0
  125. package/lib/shipment.fixture.d.ts.map +1 -1
  126. package/lib/shipment.fixture.js +7 -0
  127. package/lib/shipment.fixture.js.map +1 -1
  128. package/lib/shipment.js +13 -1
  129. package/lib/shipment.js.map +1 -1
  130. package/lib/sync.d.ts +35 -3
  131. package/lib/sync.d.ts.map +1 -1
  132. package/lib/sync.js +59 -8
  133. package/lib/sync.js.map +1 -1
  134. package/package.json +1 -1
  135. package/rt-tools-agent-kit-0.18.0.tgz +0 -0
  136. package/rt-tools-agent-kit-0.16.1.tgz +0 -0
@@ -17,13 +17,16 @@
17
17
  rt_hook_read
18
18
  input="$RT_HOOK_INPUT"
19
19
 
20
- device_id="$("${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/browser-device-id.sh" 2>/dev/null)"
20
+ # Признак сессии помощнику передаётся: без него слово о ненастроенном дереве метится днём и
21
+ # приходит один раз в сутки, а не один раз за заход.
22
+ sid="$(printf '%s' "$input" | jq -r '.session_id // "nosession"' 2>/dev/null)"
23
+
24
+ device_id="$("${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/browser-device-id.sh" "$sid")"
21
25
  [ -z "$device_id" ] && exit 0
22
26
 
23
27
  requested="$(printf '%s' "$input" | jq -r '.tool_input.deviceId // empty' 2>/dev/null)"
24
28
 
25
29
  if [ "$requested" = "$device_id" ]; then
26
- sid="$(printf '%s' "$input" | jq -r '.session_id // "nosession"' 2>/dev/null)"
27
30
  marker_dir="${TMPDIR:-/tmp}/claude-browser-guard"
28
31
  mkdir -p "$marker_dir" 2>/dev/null && : >"$marker_dir/${sid}" 2>/dev/null
29
32
  exit 0
@@ -1,12 +1,18 @@
1
1
  #!/usr/bin/env bash
2
- # rt-hook: PreToolUse mcp__playwright__.*|mcp__chrome-devtools__.*|Bash
2
+ # rt-hook: PreToolUse mcp__playwright__.*|mcp__chrome-devtools__.*|Bash|mcp__webstorm__execute_terminal_command|mcp__webstorm__execute_tool
3
3
  # Требует: hooks/deny-tail.sh
4
4
  # Гард обходных путей к браузеру. PreToolUse.
5
5
  #
6
6
  # Закрепление профиля чего-то стоит только тогда, когда дверь одна. Здесь перечислены двери,
7
7
  # которые обходят её целиком и закреплённый профиль не спрашивают вовсе: второй драйвер,
8
8
  # третий драйвер, открытие ссылки средствами системы, управление браузером через сценарий
9
- # автоматизации, отдельная утилита и прямой запуск бинарника.
9
+ # автоматизации, отдельная утилита, прямой запуск бинарника и браузер, поднятый библиотекой
10
+ # изнутри скрипта.
11
+ #
12
+ # Последняя дверь в строке команды не видна вовсе: там стоят имя интерпретатора и путь файла, а
13
+ # вождение лежит внутри самого файла. Поэтому судится и содержимое запускаемого файла, и код,
14
+ # переданный доводом вместо файла, — на известные точки входа браузерных библиотек. Так
15
+ # закреплённый профиль обошли дважды за один заход, и не остановило ничто.
10
16
  #
11
17
  # Написание сквозных спек при этом остаётся законным: прогон спеки не выдаёт агенту
12
18
  # интерактивный браузер. Отбиваются только глаголы вождения.
@@ -86,4 +92,37 @@ printf '%s' "$cmd" | grep -qE "${RT_CMD_BOUND}(/[^[:space:]]*/)?(google-chrome|c
86
92
  printf '%s' "$cmd" | grep -qF 'Google Chrome.app/Contents/MacOS' \
87
93
  && deny "Прямой запуск бинарника браузера обходит закреплённый профиль."
88
94
 
95
+ # Точки входа браузерных библиотек — то, чем браузер поднимают из кода. Перечислены и глагол
96
+ # запуска, и глагол присоединения к уже поднятому: второй обходит профиль ровно так же.
97
+ launch='(chromium|firefox|webkit|browserType|puppeteer|chromeLauncher)[[:space:]]*\.[[:space:]]*(launch|connect)|launchPersistentContext|webdriver\.(Chrome|Firefox)|chrome-launcher'
98
+
99
+ # Код, переданный доводом вместо файла. Судится только вместе с именем интерпретатора и его
100
+ # флагом кода: голый образец отбивал бы и поиск по дереву, в котором такое слово просто ищут.
101
+ if printf '%s' "$cmd" | grep -qE "${RT_CMD_BOUND}(node|bun|deno|python3?)([[:space:]]+-[^[:space:]]+)*[[:space:]]+(-e|--eval|-c|-p|--print)[[:space:]]" \
102
+ && printf '%s' "$cmd" | grep -qE "$launch"; then
103
+ deny "Браузер, поднятый библиотекой из кода довода, закреплённый профиль не спрашивает."
104
+ fi
105
+
106
+ # Файлы, которые команда запускает интерпретатором. Обёртки менеджеров пакетов снимаются: под
107
+ # ними стоит тот же интерпретатор, а видеть его надо в позиции команды.
108
+ launched="$(printf '%s' "$cmd" | sed -E '
109
+ s/(^|[[:space:]])(npx|bunx)[[:space:]]+/\1/g
110
+ s/(^|[[:space:]])(pnpm|npm|yarn)[[:space:]]+(exec|dlx)[[:space:]]+/\1/g
111
+ s/(^|[[:space:]])deno[[:space:]]+run[[:space:]]+/\1deno /g
112
+ ' | tr '|&;' '\n\n\n' | sed -nE '
113
+ s/.*(^|[[:space:]])(node|bun|deno|tsx|ts-node|python3?)[[:space:]]+((-[^[:space:]]+|[A-Za-z_][A-Za-z0-9_]*=[^[:space:]]+)[[:space:]]+)*([^[:space:]]+).*/\5/p
114
+ ' | tr -d "\"'\`")"
115
+
116
+ cwd="$(rt_hook_cwd)"
117
+ for file in $launched; do
118
+ for base in '' "${cwd:-.}/" "${CLAUDE_PROJECT_DIR:-.}/"; do
119
+ path="${base}${file}"
120
+ [ -f "$path" ] || continue
121
+ if head -c 200000 "$path" 2>/dev/null | grep -qE "$launch"; then
122
+ deny "Браузер, поднятый библиотекой изнутри ${file}, закреплённый профиль не спрашивает."
123
+ fi
124
+ break
125
+ done
126
+ done
127
+
89
128
  exit 0
@@ -22,6 +22,12 @@
22
22
  # сверять. Эти случаи держит правило ведения работы, и это известная граница гарда, а не
23
23
  # обещание.
24
24
  #
25
+ # Не судится и чужое слово: цитата в кавычках, вопрос экзаменатора, пересказ чужого текста,
26
+ # строка кода и предложение в условном наклонении утверждением о дереве не являются. Отбитые
27
+ # за них ходы обходятся дороже пропущенных: гард, отбивающий цитату, учит не проверять дерево,
28
+ # а не писать кавычек. Поэтому перед разбором из сказанного вынимаются кавычки, код, строки
29
+ # цитирования и предложения со словом условия.
30
+ #
25
31
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: при любой ошибке, нехватке `jq`, отсутствии записи хода или пустом
26
32
  # тексте ход РАЗРЕШАЕТСЯ (exit 0). Сломанный гард не имеет права заклинить разговор.
27
33
 
@@ -95,6 +101,19 @@ said="$(printf '%s' "$turn" | jq -r '.said // ""' 2>/dev/null)"
95
101
  ran="$(printf '%s' "$turn" | jq -r '.ran // ""' 2>/dev/null)"
96
102
  [ -z "$said" ] && exit 0
97
103
 
104
+ # Чужое слово вынимается до разбора. Строка цитирования и блок кода уходят целиком, кавычки и
105
+ # код в строке гасятся пробелом, а предложения со словом условия отбрасываются: «прогнал бы» и
106
+ # «если бы линт был зелёным» о состоянии дерева не говорят ничего.
107
+ judged="$(printf '%s\n' "$said" \
108
+ | awk 'BEGIN { code = 0 }
109
+ /^[[:space:]]*```/ { code = !code; next }
110
+ !code && !/^[[:space:]]*>/' \
111
+ | sed -E 's/`[^`]*`/ /g; s/«[^»]*»/ /g; s/"[^"]*"/ /g' \
112
+ | sed -E 's/([.!?])[[:space:]]+/\1\'$'\n''/g' \
113
+ | grep -viE '(^|[[:space:]])(бы|если|разве|неужели|ли)([[:space:]]|[,.]|$)' 2>/dev/null)"
114
+ [ -z "$judged" ] && exit 0
115
+ said="$judged"
116
+
98
117
  # Карта утверждений: что сказано о дереве | чем это показывают | что назвать в отказе.
99
118
  #
100
119
  # Слово берётся в той форме, в какой его говорят владельцу. Будущее время сюда не идёт:
@@ -128,7 +147,9 @@ for row in "${claims[@]}"; do
128
147
 
129
148
  Утверждение о дереве стоит ровно столько, сколько команда, его показавшая: сказанное без команды владелец читает как проверенный факт и узнаёт о расхождении последним.
130
149
 
131
- Запусти ${name} этим же ходом и назови её выводлибо убери утверждение из ответа.
150
+ Первый выход убрать утверждение из ответа и сказать о сделанном без него. Он же и верный там, где владелец команды не просил: запуск ради снятия отказа бывает опаснее того, что гард стережёт, так ход, которому нечего было поставлять, дошёл до пуша.
151
+
152
+ Второй — запустить ${name} этим же ходом и назвать её вывод.
132
153
 
133
154
  Гард судит один ход: следующий заход не отбивается."
134
155
 
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # rt-hook: PreToolUse Edit|Write|MultiEdit|Bash|mcp__webstorm__create_new_file|mcp__webstorm__execute_terminal_command|mcp__webstorm__execute_tool
2
+ # rt-hook: PreToolUse Edit|Write|MultiEdit|NotebookEdit|Bash|mcp__webstorm__create_new_file|mcp__webstorm__execute_terminal_command|mcp__webstorm__execute_tool
3
3
  # Требует: hooks/profile-check.sh, hooks/deny-tail.sh
4
4
  # Гард пары «правка и её документ». PreToolUse.
5
5
  #
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bash
2
- # rt-hook: PreToolUse Edit|Write|MultiEdit|mcp__webstorm__create_new_file|Bash
3
- # Требует: agents/strict-teacher.md, hooks/roles.sh, hooks/deny-tail.sh
2
+ # rt-hook: PreToolUse Edit|Write|MultiEdit|mcp__webstorm__create_new_file|Bash|mcp__webstorm__execute_terminal_command
3
+ # Требует: agents/strict-teacher.md, hooks/roles.sh, hooks/deny-tail.sh, hooks/write-targets.sh
4
4
  # Гард экзамена: правка не идёт, пока за сессию не сдан экзамен по загруженным правилам.
5
5
  #
6
6
  # Зачем именно так. Гейт правил требует загрузить правило перед правкой и на этом кончается:
@@ -15,6 +15,21 @@
15
15
  # между вопросами и ответами не должно быть чтения тех же правил. Признак грубый и своей границы
16
16
  # не скрывает: чтение соседнего правила он засчитает списыванием тоже.
17
17
  #
18
+ # Вердикт ищется во всех формах записи хода, а не в одной. Форму доставки выбирает хост: роль,
19
+ # работающая фоном, отдаёт результат уведомлением о завершении, и записи вида «ответ инструмента»
20
+ # у неё нет вовсе. Дерево, где роль так и работает, гард запер целиком — пять кругов экзамена с
21
+ # полным вердиктом не отпустили ни одной правки; разбор — в описаниях происшествий.
22
+ #
23
+ # Отброшены при этом две формы, и обе намеренно. Свой текст помощника вердиктом не бывает:
24
+ # написать нужную строку в ответе стоит одного движения. Ответы инструментов чтения и записи —
25
+ # тоже: печать той же строки эхом или чтение файла с нею проходили бы гард, то есть единственным
26
+ # достижимым способом стала бы подделка. Засчитывается ответ инструмента, который читать и
27
+ # писать файлы не умеет, — им роль и запускают.
28
+ #
29
+ # Из-под гарда выведены настройка дерева, её надстройки и передача захода: настройка, которой
30
+ # гард выключается, этим гардом не запирается — иначе выхода из отказа нет вовсе. Отправка груза
31
+ # в приём заперта была той же дырой: адрес приёма живёт в той же настройке.
32
+ #
18
33
  # FAIL-OPEN: нет jq, нет записи хода, чужой инструмент → пропуск. Сломанный гард не должен
19
34
  # мешать работать.
20
35
 
@@ -34,16 +49,46 @@ rt_hooks_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
34
49
  [ -f "$rt_hooks_dir/roles.sh" ] && . "$rt_hooks_dir/roles.sh" 2>/dev/null
35
50
  command -v rt_role_off >/dev/null 2>&1 && rt_role_off strict-teacher && exit 0
36
51
 
52
+ # Цели записи разбирает общий помощник — тот же, которым их разбирает гард места правки.
53
+ # shellcheck disable=SC1090
54
+ [ -f "$rt_hooks_dir/write-targets.sh" ] && . "$rt_hooks_dir/write-targets.sh" 2>/dev/null
55
+ command -v rt_write_targets >/dev/null 2>&1 || rt_write_targets() { cat >/dev/null; }
56
+
57
+ # Пути, которые гард не судит: настройка дерева, её надстройки и каталог передачи захода.
58
+ # Печатает «да», если все названные цели выведены из-под гарда.
59
+ rt_exam_free_paths() {
60
+ free=1
61
+ while IFS= read -r target; do
62
+ [ -z "$target" ] && continue
63
+ case "$target" in
64
+ *.claude/rt-kit.json | *.claude/rt-kit/* | *.claude/handoff/*) ;;
65
+ *) free=0 ;;
66
+ esac
67
+ done
68
+ [ "$free" = "1" ] && printf 'да'
69
+ }
70
+
37
71
  tool="$(rt_hook_tool)"
38
72
  # Второй экзамен спрашивается на снятии черновика: работа кончилась, и правила поставки к этому
39
73
  # моменту читались давно — между их чтением и этой минутой прошёл весь заход.
40
74
  ready=0
41
75
  case "$tool" in
42
- Edit | Write | MultiEdit | mcp__webstorm__create_new_file) ;;
43
- Bash)
76
+ Edit | Write | MultiEdit | mcp__webstorm__create_new_file)
77
+ target="$(printf '%s' "$input" | jq -r '.tool_input.file_path // .tool_input.pathInProject // empty' 2>/dev/null)"
78
+ [ -n "$target" ] && [ "$(printf '%s\n' "$target" | rt_exam_free_paths)" = "да" ] && exit 0
79
+ ;;
80
+ Bash | mcp__webstorm__execute_terminal_command)
44
81
  cmd="$(rt_hook_cmd)"
45
- printf '%s' "$cmd" | grep -qE 'pr[[:space:]]+ready|mr[[:space:]]+update[^|;&]*--ready' || exit 0
46
- ready=1
82
+ if printf '%s' "$cmd" | grep -qE 'pr[[:space:]]+ready|mr[[:space:]]+update[^|;&]*--ready'; then
83
+ ready=1
84
+ else
85
+ # Запись файла вызовом оболочки судится наравне с правкой: закрытый честный путь при
86
+ # открытом обходном означает, что гард держит того, кто правилам следует, и пропускает
87
+ # того, кто их обходит.
88
+ targets="$(printf '%s' "$cmd" | rt_write_targets)"
89
+ [ -z "$targets" ] && exit 0
90
+ [ "$(printf '%s\n' "$targets" | rt_exam_free_paths)" = "да" ] && exit 0
91
+ fi
47
92
  ;;
48
93
  *) exit 0 ;;
49
94
  esac
@@ -70,11 +115,39 @@ deny() {
70
115
 
71
116
  # Судится вся сессия, а не последний ход: экзамен сдаётся один раз на старте и держится до конца.
72
117
  verdict="$(jq -s -r '
73
- [.[] | select(.type == "user") | .message.content // [] | select(type == "array") | .[]
74
- | select(.type == "tool_result") | .content
75
- | if type == "string" then .
76
- elif type == "array" then (map(if type == "object" then (.text // "") else tostring end) | join("\n"))
77
- else tostring end] | join("\n")
118
+ def textof:
119
+ if type == "string" then .
120
+ elif type == "array" then (map(if type == "object" then (.text // "") else tostring end) | join("\n"))
121
+ else tostring end;
122
+
123
+ # Инструменты, читающие и пишущие файлы: их ответ вердиктом не считается — иначе печать той
124
+ # же строки эхом и чтение файла с нею проходят гард, а настоящий вердикт не проходит.
125
+ ["Bash", "Read", "Grep", "Glob", "Edit", "Write", "MultiEdit", "NotebookEdit"] as $mute
126
+ | [.[] | select(.type == "assistant") | (.message.content // [])[]
127
+ | select(.type == "tool_use") | select(.name as $n | $mute | index($n) != null) | (.id // "")] as $muted
128
+
129
+ | [ .[]
130
+ # Свой текст помощника вердиктом не бывает: написать нужную строку в ответе стоит одного
131
+ # движения.
132
+ | if .type == "assistant" then ""
133
+ elif .type == "user" then
134
+ ([ ((.message.content // []) | if type == "array" then .[] else empty end
135
+ | select(.type == "tool_result")
136
+ | select(((.tool_use_id // "") as $i | $muted | index($i)) == null)
137
+ | .content | textof),
138
+ ((.message.content // "") | if type == "string" then . else "" end),
139
+ # Поле результата вызова: та же запись, другая форма. Отбрасывается, только
140
+ # если этот результат принадлежит инструменту чтения или записи.
141
+ (. as $rec
142
+ | if ($rec.toolUseResult // null) == null then ""
143
+ elif ([($rec.message.content // []) | if type == "array" then .[] else empty end
144
+ | select(.type == "tool_result") | (.tool_use_id // "")]
145
+ | map($muted | index(.)) | any(. != null)) then ""
146
+ else ($rec.toolUseResult | textof) end)
147
+ ] | join("\n"))
148
+ # Записи хоста — уведомление о завершении роли и вложение: форму их выбирает хост, и
149
+ # засчитываются они целиком.
150
+ else tostring end ] | join("\n")
78
151
  | [scan("ЭКЗАМЕН:[[:space:]]*сдано[[:space:]]*([0-9]+)[[:space:]]*из[[:space:]]*([0-9]+)")]
79
152
  | if length == 0 then "нет"
80
153
  else (.[-1] | if .[0] == .[1] then "сдан" else "провален" end)
@@ -120,6 +193,6 @@ case "$verdict" in
120
193
  deny "BLOCKED by exam-guard: экзамен по загруженным правилам провален. Перечитай правило целиком — не тот кусок, о котором спрашивали, — и позови роль strict-teacher снова. Показанный ответ даёт знание одной строки, а не правила."
121
194
  ;;
122
195
  *)
123
- deny "BLOCKED by exam-guard: за эту сессию экзамена по загруженным правилам не было. Позови роль strict-teacher, передай ей список загруженных правил, ответь на её вопросы по памяти и верни ей ответы — вердикт она отдаёт строкой «ЭКЗАМЕН: сдано N из 5». Загруженное правило и прочитанное правило — разные вещи, и цену этой разницы платит владелец."
196
+ deny "BLOCKED by exam-guard: за эту сессию экзамена по загруженным правилам не было. Позови роль strict-teacher, передай ей список загруженных правил, ответь на её вопросы по памяти и верни ей ответы — вердикт она отдаёт строкой «ЭКЗАМЕН: сдано N из 5». Засчитывается он из ответа роли в любой форме, какой его доставил хост, но не из вывода оболочки и не из твоего же текста: печать этой строки эхом гард не отпускает. Роль уже звали и вердикт получен — значит, он пришёл формой, которой гард не видит: это дефект гарда, и правка `.claude/rt-kit.json` из-под него выведена. Загруженное правило и прочитанное правило — разные вещи, и цену этой разницы платит владелец."
124
197
  ;;
125
198
  esac
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env bash
2
+ # Конфликтующая своя заявка для гарда поставки: пока хоть одна отданная работа помечена
3
+ # конфликтующей, новая не берётся.
4
+ #
5
+ # Строки `# rt-hook:` здесь нет намеренно: событие и образец вызова объявляет сам гард, а
6
+ # помощник рядом хуком не регистрируется и в одиночку ничего не решает. Он зовётся из гарда,
7
+ # пользуется его переменными и его отказом — тем же приёмом, что помощники папки и подписи.
8
+ #
9
+ # Зачем именно так. Гард поставки стоит в трёх точках и в каждой требует того, что в эту минуту
10
+ # исправимо, а состояние уже отданного не спрашивает нигде. Конфликт приезжает в отданную заявку
11
+ # чужим слиянием, без единого действия её автора: влить её человек больше не может, и заявка
12
+ # стоит, пока автор не заметит. Заметить её нечем — требование перечитывать свои открытые заявки
13
+ # записано текстом и держится памятью: две заявки простояли конфликтующими весь заход, и заметил
14
+ # это владелец.
15
+ #
16
+ # Исправимо это ровно в минуту, когда берётся следующая работа: после неё конфликтов становится
17
+ # больше, а не меньше — каждая новая заявка растёт из той же главной ветки и приезжает в ту же
18
+ # очередь, которую придётся догонять.
19
+ #
20
+ # Работой здесь считаются четыре команды: заведение задачи, заведение ветки под задачу, перевод
21
+ # колонки в работу и открытие заявки. Всё, чем конфликт чинится, — подтягивание, вливание,
22
+ # коммит, отправка, переход между ветками — идёт как прежде: гард, отбивающий починку, запирал
23
+ # бы дерево наглухо.
24
+ #
25
+ # Судится только прямое «конфликтует». Пока хостинг считает сливаемость, он отвечает
26
+ # неопределённостью — и считает он её заново после каждой правки главной ветки, — а читать
27
+ # неопределённость как конфликт значило бы отбивать работу на каждой свежей вершине.
28
+ #
29
+ # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: нет функции профиля, нет сети, нет машинной записи — ярус пропускается.
30
+
31
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
32
+ # Начало вызова помощник берёт оттуда же, откуда все гарды. Источается он и здесь, хотя зовущий
33
+ # гард это уже сделал: пустой образец превратил бы признак в поиск слова по всей строке.
34
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
35
+
36
+ rt_delivery_conflict() {
37
+ # Признак взятия работы читается из текста команды и стоит первым: опрос ходит в сеть, и
38
+ # платить за него на каждой команде оболочки нельзя.
39
+ taking=''
40
+ case "$cmd" in
41
+ *"${task_new}"*) taking='заведение задачи' ;;
42
+ esac
43
+ # Перевод колонки судится вместе с именем колонки работы: перевод в разбор и в закрытые —
44
+ # это конец работы, а не её начало.
45
+ if [ -z "$taking" ] && [ -n "$task_move" ]; then
46
+ case "$cmd" in
47
+ *"${task_move}"*)
48
+ case "$cmd" in
49
+ *"${RT_BOARD_INPROGRESS_KEY:-in-progress}"*) taking='перевод колонки в работу' ;;
50
+ esac
51
+ ;;
52
+ esac
53
+ fi
54
+ if [ -z "$taking" ] \
55
+ && printf '%s' "$cmd" | grep -qE '(^|[;&|[:space:]])git[[:space:]]+(checkout[[:space:]]+-b|switch[[:space:]]+-c)[[:space:]]+[A-Za-z]+-[0-9]+'; then
56
+ taking='заведение ветки под задачу'
57
+ fi
58
+ if [ -z "$taking" ] \
59
+ && printf '%s' "$cmd" | grep -qE "${RT_CMD_BOUND}(gh[[:space:]]+pr[[:space:]]+create|glab[[:space:]]+mr[[:space:]]+create)([[:space:]]|\$)"; then
60
+ taking='открытие заявки'
61
+ fi
62
+ [ -z "$taking" ] && return 0
63
+
64
+ rt_needs rt_conflicting_pulls git-guard-delivery || return 0
65
+ stuck="$(cd "$root" && rt_conflicting_pulls 2>/dev/null)" || return 0
66
+ [ -z "$stuck" ] && return 0
67
+
68
+ listed=''
69
+ while IFS= read -r line; do
70
+ [ -z "$line" ] && continue
71
+ listed="${listed}${listed:+
72
+ }— ${line}"
73
+ done <<EOF
74
+ ${stuck}
75
+ EOF
76
+ [ -z "$listed" ] && return 0
77
+
78
+ deny "BLOCKED: ${taking} при своей конфликтующей заявке. Отданное конфликтует с главной веткой, и влить его человек не может:
79
+
80
+ ${listed}
81
+
82
+ Конфликт приезжает чужим слиянием, без единого действия автора заявки: она стоит, пока её не догонят. Новая работа его не чинит — она прибавляет к очереди ещё одну ветку, которая растёт из той же главной и отстанет так же.
83
+
84
+ Ход отсюда один: подтянуть главную, влить её в названную ветку, разобрать конфликт и отправить — и только потом брать новую работу. Что чем чинится — паттерн git-workflow-freshness."
85
+ }
@@ -61,6 +61,25 @@ rt_delivery_open_folder() {
61
61
  && fault "папку задачи удалили, но в «${archive_dir}» ветка ничего не добавила. Удалить проще, чем разобрать, — и вместе с папкой пропадает разбор просьбы, единственная запись слов владельца. Перенеси то, что объясняет принятые решения, одним файлом с понятным именем и повтори."
62
62
  }
63
63
 
64
+ # Условие снятия черновика: тот же предмет между открытием и слиянием. Снятый черновик читается
65
+ # владельцем как приглашение влить, и кнопку он нажимает, не дожидаясь коммита уборки: гард
66
+ # слияния сюда не достаёт вовсе — нажимает человек на хостинге. Три работы подряд уехали в
67
+ # главную ветку именно так, и в теле каждой заявки стояло обещание убрать папку после одобрения.
68
+ #
69
+ # Ветку функция спрашивает сама: на этом ходе гард её ещё не посчитал.
70
+ rt_delivery_ready_folder() {
71
+ [ -n "$tasks_dir" ] || return 0
72
+ printf '%s' "$cmd" | grep -qiE "$folder_skip_re" && return 0
73
+
74
+ _branch="$(git branch --show-current 2>/dev/null)"
75
+ [ -z "$_branch" ] && return 0
76
+ rt_task_branch_ok "$_branch" || return 0 # за беззадачной веткой папки не стоит
77
+
78
+ _lying="$(rt_folder_in_branch "$tasks_dir/$_branch")"
79
+ [ -n "$_lying" ] \
80
+ && fault "в ветке лежит папка задачи «${_lying}» — снятый черновик читается как «можно вливать», а вливать её туда нельзя. Кнопку нажимает человек на хостинге, и до его руки папка уедет в главную. Перенеси в «${archive_dir:-архив}» то, что объясняет принятые решения, остальное удали, закоммить и повтори. Если работа вливается частями, поставь в команду комментарий «# Task-folder-skip: <причина>»."
81
+ }
82
+
64
83
  # Условие слияния: тот же предмет вторым рубежом. Он ловит слияние, идущее командой, — то, до
65
84
  # которого гард достаёт. Отсюда выходят только отказом или молчанием: слияние решается целиком.
66
85
  rt_delivery_merge_folder() {
@@ -24,9 +24,15 @@ rt_delivery_signature() {
24
24
  # изнутри это выглядит верным — имя учётной записи в истории то самое. Так одиннадцать коммитов
25
25
  # и уехали в главную ветку за чужой подписью; нашёл это владелец, читая историю глазами.
26
26
  #
27
- # Точкапуш: до него подпись переписывается на месте всей веткой сразу, после только силовой
28
- # отправкой. На коммите гард не стоит: почта задаётся переменными самой команды, и разбор её из
29
- # текста ловил бы ту же строку, которая и так перед глазами у набравшего её.
27
+ # Точек две коммит и пуш, и судят они одно: подпись коммитов, уже лежащих во вкладе ветки.
28
+ # Текст самой команды при этом не разбирается ни там, ни там: почта задаётся её переменными, и
29
+ # разбор ловил бы ту же строку, которая и так перед глазами у набравшего её.
30
+ #
31
+ # Коммит добавлен потому, что промах делается на нём и до пуша успевает лечь в несколько коммитов
32
+ # подряд: каждый следующий берёт адрес у предыдущего. Отбитый на коммите промах чинится
33
+ # переписыванием последнего коммита тем же ходом; отбитый на пуше — переписыванием истории, а
34
+ # права на него у исполнителя может не быть вовсе. Первый коммит ветки при этом проходит: судить
35
+ # нечего, вклад пуст.
30
36
  #
31
37
  # Судится коммит, НАЗВАВШИЙСЯ машинной записью: её логин стоит именем автора либо левой частью
32
38
  # почты. Опознавать по самой почте нельзя — она в таком коммите как раз и неверна, а требовать
@@ -45,9 +51,12 @@ rt_delivery_signature() {
45
51
  # Пробный пуш не отправляет ничего, и подпись у него не спрашивают. Отложенная правка — тоже:
46
52
  # `git stash push` кладёт правку в тайник этой же машины, поэтому тайник вырезается из строки, а
47
53
  # признак считается по остатку.
54
+ # Коммит узнаётся тем же приёмом: слово `commit` отдельным словом при вызове git. Пробный
55
+ # коммит здесь не отличают — своего `--dry-run` он не знает.
48
56
  if printf '%s' "$cmd" | grep -qE "${RT_CMD_BOUND}git([[:space:]]|\$)" \
49
- && printf '%s' "$cmd" | sed -E 's/git[[:space:]]+stash[[:space:]]+push/git stash/g' | grep -qE '(^|[[:space:]])push([[:space:]]|$)' \
50
- && ! printf '%s' "$cmd" | grep -q -- '--dry-run'; then
57
+ && { { printf '%s' "$cmd" | sed -E 's/git[[:space:]]+stash[[:space:]]+push/git stash/g' | grep -qE '(^|[[:space:]])push([[:space:]]|$)' \
58
+ && ! printf '%s' "$cmd" | grep -q -- '--dry-run'; } \
59
+ || printf '%s' "$cmd" | grep -qE '(^|[[:space:]])commit([[:space:]]|$)'; }; then
51
60
  # Два условия молчания. Дерево, не назвавшее почты машинной записи, требования не получает:
52
61
  # своей машинной записи у пакета нет, а выдуманная отбивала бы работу в чужом дереве.
53
62
  # Вершины главной ветки нет — вклад считать не от чего.
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bash
2
2
  # rt-hook: PreToolUse Bash|mcp__webstorm__execute_terminal_command|mcp__webstorm__execute_tool
3
- # Требует: hooks/git-guard-delivery-folder.sh, hooks/profile-check.sh, hooks/deny-tail.sh
3
+ # Требует: hooks/git-guard-delivery-folder.sh, hooks/git-guard-delivery-conflict.sh, hooks/profile-check.sh, hooks/deny-tail.sh
4
4
  # Гард поставки. PreToolUse на заведении ветки, пуше и открытии заявки на слияние.
5
5
  #
6
6
  # Закон о поставке требует трёх вещей, которых обычно не проверяет ничто: правка начинается с
@@ -107,6 +107,11 @@ task_bot="${RT_TASK_BOT:-}"
107
107
  # у него может не быть отдельной машинной записи.
108
108
  pull_token_var="${RT_PULL_TOKEN_VAR:-}"
109
109
  pull_token_hint="${RT_PULL_TOKEN_HINT:-}"
110
+ # Раздел, который тело заявки обязано нести с минуты открытия. Кнопку слияния нажимает человек
111
+ # на хостинге, куда гард не достаёт: всё, чем требование там держится, — то, что владелец увидел
112
+ # на странице. Заголовок раздела пишется языком заявки, поэтому образец называет дерево, а не
113
+ # пакет: чужих слов пакет не знает, и выдуманное умолчание не совпало бы ни с чем.
114
+ pull_body_section="${RT_PULL_BODY_SECTION:-}"
110
115
  commit_email="${RT_COMMIT_EMAIL:-}"
111
116
  tasks_dir="${RT_TASKS_DIR:-}"
112
117
  archive_dir="${RT_ARCHIVE_DIR:-}"
@@ -158,6 +163,14 @@ fault() {
158
163
  # shellcheck disable=SC1090
159
164
  [ -f "$rt_hooks_dir/git-guard-delivery-folder.sh" ] && . "$rt_hooks_dir/git-guard-delivery-folder.sh" 2>/dev/null
160
165
 
166
+ # Конфликтующая своя заявка: тот же приём, что у папки и подписи, — свой предмет живёт
167
+ # помощником рядом. Зовётся он до всех ярусов ниже и судит не готовность этой работы, а право
168
+ # брать следующую: пока отданное конфликтует, его чинят первым действием хода. Нет помощника —
169
+ # ярус не судится, а работа идёт дальше.
170
+ # shellcheck disable=SC1090
171
+ [ -f "$rt_hooks_dir/git-guard-delivery-conflict.sh" ] && . "$rt_hooks_dir/git-guard-delivery-conflict.sh" 2>/dev/null
172
+ command -v rt_delivery_conflict >/dev/null 2>&1 && rt_delivery_conflict
173
+
161
174
  # Отказ по накопленному. Пусто — вызывающая сторона идёт дальше.
162
175
  deny_faults() {
163
176
  [ -z "$faults" ] && return 0
@@ -327,6 +340,11 @@ if printf '%s' "$cmd" | grep -qE "${RT_CMD_BOUND}(gh[[:space:]]+pr[[:space:]]+re
327
340
  fi
328
341
  fi
329
342
  fi
343
+
344
+ # Папка задачи: тот же предмет, что на открытии и на слиянии, третьим рубежом. Условие
345
+ # местное — оно читает ветку, а не хостинг, — и потому стоит вне сетевого яруса выше.
346
+ command -v rt_delivery_ready_folder >/dev/null 2>&1 && rt_delivery_ready_folder
347
+
330
348
  deny_faults
331
349
  fi
332
350
 
@@ -370,7 +388,12 @@ fi
370
388
  if [ -n "$title" ]; then
371
389
  printf '%s' "$title" | grep -qE "$title_re" \
372
390
  || fault "заголовок заявки не начинается с номера задачи. В списке заявок тела не видно, а строка связи живёт именно там — без номера в заголовке PR с задачей не сопоставить."
373
- title_number="$(printf '%s' "$title" | sed -nE 's/^\[[A-Za-z]+-([0-9]+)\].*/\1/p')"
391
+ # Номер вынимается из той части заголовка, которую признала сама форма, а не второй
392
+ # регуляркой рядом. Своя регулярка знает только пакетную форму: дерево, замостившее форму
393
+ # своей, получало пустой номер — и сверка номера заголовка с номером ветки молча не
394
+ # выполнялась вовсе, выглядя при этом сошедшейся.
395
+ title_matched="$(printf '%s' "$title" | grep -oE "$title_re" | head -1)"
396
+ title_number="$(printf '%s' "$title_matched" | grep -oE '[A-Za-z]+-[0-9]+' | head -1 | sed -E 's/^[A-Za-z]+-//')"
374
397
  if [ -n "$number" ] && [ -n "$title_number" ]; then
375
398
  [ "$title_number" = "$number" ] \
376
399
  || fault "в заголовке заявки номер ${title_number}, у ветки — ${number}. Задача, ветка и PR несут один и тот же номер."
@@ -424,6 +447,35 @@ if [ -n "$pull_token_var" ] \
424
447
  fault "заявка открывается без токена машинной записи: в команде нет подстановки «${pull_token_var}». Открытая залогиненной записью, она выйдет от владельца — ревьювером его тогда не назначить, и чинится это только переоткрытием.${pull_token_hint:+ Подставь токен: ${pull_token_hint} …}"
425
448
  fi
426
449
 
450
+ # Тело заявки несёт раздел об оставшемся шаге, и несёт его с минуты открытия. Требование
451
+ # записано было и прежде — прозой в паттерне закрытия работы, ниже того места, где описано само
452
+ # открытие: читающий доходил до открытия, открывал и раздела ещё не прочитал. Так заявка и
453
+ # уехала без него, а владелец влил её кнопкой, пока шёл прогон.
454
+ #
455
+ # Судится текст команды: тело приходит доводом либо файлом, и оба вида читаются здесь же. Файл
456
+ # читается с диска — к моменту разбора он уже написан. Ни того ни другого в команде нет —
457
+ # требования нет: заявка без тела вовсе судится строкой выше, а не этой.
458
+ if [ -n "$pull_body_section" ]; then
459
+ body=''
460
+ if command -v perl >/dev/null 2>&1; then
461
+ body="$(printf '%s' "$cmd" | perl -0ne '
462
+ if (/(?:--body|-b)(?:=|\s+)(?:"((?:[^"\\]|\\.)*)"|\x27([^\x27]*)\x27|(\S+))/s) {
463
+ print defined $1 ? $1 : (defined $2 ? $2 : $3);
464
+ }
465
+ ' 2>/dev/null)"
466
+ body_file="$(printf '%s' "$cmd" | perl -0ne '
467
+ if (/(?:--body-file|-F)(?:=|\s+)(?:"((?:[^"\\]|\\.)*)"|\x27([^\x27]*)\x27|(\S+))/s) {
468
+ print defined $1 ? $1 : (defined $2 ? $2 : $3);
469
+ }
470
+ ' 2>/dev/null)"
471
+ [ -z "$body" ] && [ -n "$body_file" ] && [ -f "$body_file" ] && body="$(cat "$body_file" 2>/dev/null)"
472
+ fi
473
+
474
+ if [ -n "$body" ] && ! printf '%s' "$body" | grep -qE "$pull_body_section"; then
475
+ fault "в теле заявки нет раздела об оставшемся шаге. Кнопку слияния нажимает человек на хостинге, где гардов нет, и вливает он, как только видит зелёное: всё, чем требование там держится, — то, что владелец прочитал на странице. Раздел стоит последним и говорит ровно одно — осталось ли что-то до слияния; переписывается он тем же вызовом, которым правится тело."
476
+ fi
477
+ fi
478
+
427
479
  check_task "$number" "заявка с ветки «${branch}»" да
428
480
 
429
481
  # Папка задачи разбирается до открытия заявки, а не после одобрения. Прежде здесь стояло
@@ -119,11 +119,25 @@ if git fetch --quiet origin "$main_branch" 2>/dev/null && git rev-parse --verify
119
119
  base="origin/$main_branch"
120
120
  fi
121
121
 
122
+ # Код, которым проверка объявляет, что смотреть было не на что: ни «сошлось», ни «расхождение».
123
+ # Прежде такая проверка говорила о себе строкой вывода и выходила нулём — в наборе этот ноль
124
+ # стоял рядом с пройденными и ничем от них не отличался, а сводка читалась как проверенная
125
+ # целиком. Пуш он не отбивает: проверка, которой нечего смотреть, поломкой не является.
126
+ rt_skip_code="${RT_SKIP_CODE:-7}"
127
+
122
128
  failed=""
123
129
  output=""
130
+ skipped=""
124
131
  while IFS= read -r check; do
125
132
  [ -z "$check" ] && continue
126
- out="$(eval "$check" 2>&1)" && continue
133
+ out="$(eval "$check" 2>&1)"
134
+ status=$?
135
+ [ "$status" -eq 0 ] && continue
136
+ if [ "$status" -eq "$rt_skip_code" ]; then
137
+ skipped="${skipped}${skipped:+
138
+ }${check}"
139
+ continue
140
+ fi
127
141
  failed="$check"
128
142
  output="$out"
129
143
  break
@@ -131,6 +145,12 @@ done <<EOF
131
145
  $(rt_push_checks "$base")
132
146
  EOF
133
147
 
148
+ # Пропущенное называется вслух и тогда, когда набор прошёл: молчание о нём и есть та самая
149
+ # неотличимость, ради которой код заведён. Пуш при этом идёт — отказа здесь нет.
150
+ if [ -z "$failed" ] && [ -n "$skipped" ]; then
151
+ printf 'гейт пуша: набор прошёл, но эти проверки смотреть было не на что:\n%s\n' "$skipped" >&2
152
+ fi
153
+
134
154
  [ -z "$failed" ] && exit 0
135
155
 
136
156
  # Хвост вывода, а не весь: у прогонщика он длинный, а нужна причина отказа.
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env bash
2
+ # rt-hook: PreToolUse AskUserQuestion
3
+ # Требует: hooks/grill-gate.sh
4
+ # Половина гарда разговора, стоящая на инструменте вопроса: меню судится до отправки.
5
+ #
6
+ # Своим ресурсом, а не строкой в шапке соседа, потому что инструмент вопроса — единственный
7
+ # способ спросить владельца, и у дерева на нём уже может стоять свой гард со своей причиной
8
+ # отказа. Второй отказ на том же инструменте делает вопрос недоступным вовсе, и дерево
9
+ # отказывается от гарда целиком вместо того, чтобы взять его половину. Разведённые по двум
10
+ # ресурсам, половины отменяются порознь: строка отказа снимает одну и оставляет другую.
11
+ #
12
+ # Судит вызов то же тело, что и завершение хода: требование одно, и расходиться двум его
13
+ # редакциям нельзя. Здесь только объявление события и передача ввода.
14
+ #
15
+ # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: нет соседнего файла — ход РАЗРЕШАЕТСЯ. Половина гарда, потерявшая
16
+ # тело, не имеет права заклинить разговор.
17
+
18
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
19
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
20
+
21
+ rt_hooks_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
22
+ [ -f "$rt_hooks_dir/grill-gate.sh" ] || exit 0
23
+
24
+ rt_hook_read
25
+ printf '%s' "$RT_HOOK_INPUT" | "$rt_hooks_dir/grill-gate.sh"