@rt-tools/agent-kit 0.9.1 → 0.11.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 (119) 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-state-next.mjs +194 -0
  13. package/assets/checks/check-states.mjs +142 -0
  14. package/assets/checks/check-styles.mjs +5 -5
  15. package/assets/checks/check-turn-map.mjs +146 -0
  16. package/assets/checks/lib-common.mjs +3 -3
  17. package/assets/checks/rt-kit-checks.config.mjs +85 -1
  18. package/assets/commands/agent-kit-digest.md +6 -5
  19. package/assets/defaults/project.sh +108 -6
  20. package/assets/defaults/turn-map.md +46 -0
  21. package/assets/docs/GLOSSARY.md +17 -15
  22. package/assets/hooks/browser-device-id.sh +2 -0
  23. package/assets/hooks/browser-guard-device-id.sh +11 -1
  24. package/assets/hooks/browser-guard-no-asking.sh +11 -1
  25. package/assets/hooks/browser-guard-no-listing.sh +11 -1
  26. package/assets/hooks/browser-guard-no-other-drivers.sh +9 -1
  27. package/assets/hooks/browser-guard-require-select.sh +13 -2
  28. package/assets/hooks/claim-guard.sh +115 -0
  29. package/assets/hooks/commit-msg.sh +2 -0
  30. package/assets/hooks/conscience-guard.sh +100 -0
  31. package/assets/hooks/constitution-index.sh +2 -0
  32. package/assets/hooks/deny-tail.sh +32 -0
  33. package/assets/hooks/dev-server-guard.sh +10 -2
  34. package/assets/hooks/docs-guard.sh +16 -2
  35. package/assets/hooks/exam-guard.sh +123 -0
  36. package/assets/hooks/git-guard-delivery-signature.sh +77 -0
  37. package/assets/hooks/git-guard-delivery.sh +156 -68
  38. package/assets/hooks/git-guard-main.sh +14 -0
  39. package/assets/hooks/git-guard-push-tests.sh +51 -2
  40. package/assets/hooks/glossary-load.sh +2 -0
  41. package/assets/hooks/grill-gate.sh +14 -0
  42. package/assets/hooks/handoff-entry-guard.sh +73 -0
  43. package/assets/hooks/handoff-write.sh +103 -0
  44. package/assets/hooks/lint-after-edit.sh +2 -0
  45. package/assets/hooks/observe.sh +2 -0
  46. package/assets/hooks/postmortem-guard.sh +14 -0
  47. package/assets/hooks/proposal-guard.sh +14 -0
  48. package/assets/hooks/prose-style-guard.sh +75 -0
  49. package/assets/hooks/qa-dataid-guard.sh +14 -1
  50. package/assets/hooks/rerun-guard.sh +88 -0
  51. package/assets/hooks/reuse-first-guard.sh +14 -1
  52. package/assets/hooks/roles.sh +34 -0
  53. package/assets/hooks/skill-gate-layers.sh +2 -0
  54. package/assets/hooks/skill-gate-rearm.sh +2 -0
  55. package/assets/hooks/skill-gate.sh +14 -0
  56. package/assets/hooks/skill-loaded.sh +2 -0
  57. package/assets/hooks/sql-guard-parse.sh +2 -0
  58. package/assets/hooks/sql-guard-request.sh +2 -0
  59. package/assets/hooks/sql-guard-target.sh +2 -0
  60. package/assets/hooks/sql-guard-write.sh +4 -1
  61. package/assets/hooks/sql-guard.sh +14 -1
  62. package/assets/hooks/task-context-load.sh +2 -0
  63. package/assets/hooks/task-flow-guard.sh +73 -7
  64. package/assets/hooks/turn-entry-load.sh +62 -0
  65. package/assets/hooks/turn-exit-guard.sh +191 -0
  66. package/assets/hooks/utf8.sh +35 -0
  67. package/assets/hooks/waiting-turn-guard.sh +14 -0
  68. package/assets/hooks/window-fill-guard.sh +43 -2
  69. package/assets/laws/work-conduct.md +29 -0
  70. package/assets/patterns/cargo-triage-mark.md +119 -0
  71. package/assets/patterns/task-flow-close.md +29 -8
  72. package/assets/patterns/task-flow-handoff.md +19 -1
  73. package/assets/patterns/task-flow-resume.md +36 -4
  74. package/assets/patterns/task-flow-start.md +56 -6
  75. package/assets/patterns/turn-entry-map.md +81 -0
  76. package/assets/rules/cargo-triage.md +126 -0
  77. package/assets/rules/git-workflow.azure.md +36 -9
  78. package/assets/rules/git-workflow.github.md +74 -9
  79. package/assets/rules/git-workflow.gitlab.md +40 -12
  80. package/assets/rules/task-flow.md +136 -71
  81. package/assets/rules/testing.md +15 -1
  82. package/assets/rules/turn-entry.md +93 -0
  83. package/assets/samples/tasks/_template/plan.md +4 -1
  84. package/assets/samples/tasks/_template/progress.md +1 -0
  85. package/assets/skills/agent-kit.md +33 -0
  86. package/assets/templates/project.sh +16 -0
  87. package/bin/agent-kit.d.ts.map +1 -1
  88. package/bin/agent-kit.js +42 -1
  89. package/bin/agent-kit.js.map +1 -1
  90. package/lib/cargo-state.d.ts +62 -0
  91. package/lib/cargo-state.d.ts.map +1 -0
  92. package/lib/cargo-state.js +118 -0
  93. package/lib/cargo-state.js.map +1 -0
  94. package/lib/cargo.d.ts +42 -0
  95. package/lib/cargo.d.ts.map +1 -1
  96. package/lib/cargo.js +2 -0
  97. package/lib/cargo.js.map +1 -1
  98. package/lib/commands.d.ts.map +1 -1
  99. package/lib/commands.js +64 -4
  100. package/lib/commands.js.map +1 -1
  101. package/lib/config.d.ts +8 -0
  102. package/lib/config.d.ts.map +1 -1
  103. package/lib/config.js +1 -0
  104. package/lib/config.js.map +1 -1
  105. package/lib/observations.d.ts +35 -1
  106. package/lib/observations.d.ts.map +1 -1
  107. package/lib/observations.js +14 -2
  108. package/lib/observations.js.map +1 -1
  109. package/lib/ship.d.ts +3 -0
  110. package/lib/ship.d.ts.map +1 -1
  111. package/lib/ship.js +56 -0
  112. package/lib/ship.js.map +1 -1
  113. package/lib/thresholds.d.ts +49 -0
  114. package/lib/thresholds.d.ts.map +1 -0
  115. package/lib/thresholds.js +151 -0
  116. package/lib/thresholds.js.map +1 -0
  117. package/package.json +1 -1
  118. package/rt-tools-agent-kit-0.11.0.tgz +0 -0
  119. package/rt-tools-agent-kit-0.9.1.tgz +0 -0
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env bash
2
+ # rt-hook: PreCompact .*
3
+ # Требует: hooks/profile-check.sh
4
+ # Передача захода пишется перед сжатием контекста, а не рукой исполнителя.
5
+ #
6
+ # Заход кончается двумя способами: исполнитель доводит работу до точки и пишет передачу сам —
7
+ # либо контекст переполняется, и сжатие приходит от инструмента. Второй случай раньше терял
8
+ # передачу целиком: писать её в этот момент уже некому, а после сжатия пересказывать нечего.
9
+ # Ночью, когда напомнить некому, так кончается каждый заход.
10
+ #
11
+ # Хук собирает передачу из того, что лежит на диске: ход работы даёт состояние и следующий шаг,
12
+ # дерево — ветку, незакоммиченное и коммиты сверх главной. Ничего от себя он не добавляет:
13
+ # передача пересказывает записанное, а не заменяет его.
14
+ #
15
+ # Написанное руками не затирается молча: файл один, и последняя запись побеждает. Исполнитель,
16
+ # закрывающий заход по правилу, пишет поверх — его передача полнее, потому что он знает то,
17
+ # чего на диске нет.
18
+ #
19
+ # FAIL-OPEN: нет разборщика, пустой ввод, не репозиторий, нет каталога передачи — хук выходит
20
+ # нулём и молчит. Сжатие он не отбивает никогда: остановленное сжатие оставит заход без места.
21
+
22
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
23
+
24
+ input="$(cat 2>/dev/null)"
25
+ [ -z "$input" ] && exit 0
26
+
27
+ command -v jq >/dev/null 2>&1 || exit 0
28
+
29
+ rt_hooks_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
30
+ for profile in "$rt_hooks_dir/../rt-kit/defaults/project.sh" "$rt_hooks_dir/../defaults/project.sh" \
31
+ "${CLAUDE_PROJECT_DIR:-.}/.claude/rt-kit/defaults/project.sh" "${CLAUDE_PROJECT_DIR:-.}/.claude/rt-kit/project.sh"; do
32
+ # shellcheck disable=SC1090
33
+ [ -f "$profile" ] && . "$profile" 2>/dev/null
34
+ done
35
+
36
+ workdir="$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null)"
37
+ [ -z "$workdir" ] && workdir="${CLAUDE_PROJECT_DIR:-.}"
38
+ cd "$workdir" 2>/dev/null || exit 0
39
+ git rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0
40
+
41
+ branch="$(git branch --show-current 2>/dev/null)"
42
+ [ -z "$branch" ] && exit 0
43
+
44
+ root="$(git rev-parse --show-toplevel 2>/dev/null)"
45
+ [ -z "$root" ] && exit 0
46
+
47
+ handoff_dir="${RT_HANDOFF_DIR:-.claude/handoff}"
48
+ tasks_dir="${RT_TASKS_DIR:-docs/tasks}"
49
+
50
+ mkdir -p "$root/$handoff_dir" 2>/dev/null || exit 0
51
+ [ -d "$root/$handoff_dir" ] || exit 0
52
+
53
+ # Ручное сжатие от автоматического отличается одним: при ручном исполнитель у клавиатуры и
54
+ # может дописать передачу сам. Пишется она в обоих случаях — заход, сжатый руками, теряет
55
+ # контекст ровно так же.
56
+ trigger="$(printf '%s' "$input" | jq -r '.trigger // "auto"' 2>/dev/null)"
57
+ [ -z "$trigger" ] && trigger='auto'
58
+
59
+ progress="$root/$tasks_dir/$branch/progress.md"
60
+
61
+ # Строка раздела «Где стоим» по её названию. Пусто — работа идёт вне папки задачи, и выдумывать
62
+ # за неё состояние нельзя: в передаче тогда стоит то, что известно дереву.
63
+ line_of() {
64
+ [ -f "$progress" ] || return 0
65
+ sed -n "s/^[[:space:]]*[-*][[:space:]]*\*\*$1:\*\*[[:space:]]*\(.*\)/\1/p" "$progress" 2>/dev/null | head -1
66
+ }
67
+
68
+ state="$(line_of 'Состояние')"
69
+ stage="$(line_of 'Этап')"
70
+ next_step="$(line_of 'Следующий шаг')"
71
+ pull="$(line_of 'PR')"
72
+
73
+ uncommitted="$(git status --short 2>/dev/null | head -20)"
74
+ [ -z "$uncommitted" ] && uncommitted='нет'
75
+
76
+ ahead="$(git log --oneline origin/main..HEAD 2>/dev/null | head -20)"
77
+ [ -z "$ahead" ] && ahead='нет коммитов сверх главной'
78
+
79
+ target="$root/$handoff_dir/$branch.md"
80
+
81
+ {
82
+ printf '# Передача захода — сжатие контекста (%s)\n\n' "$trigger"
83
+ printf '**Рабочее дерево:** %s\n' "$root"
84
+ printf '**Ветка:** %s\n\n' "$branch"
85
+
86
+ if [ -n "$state" ]; then
87
+ printf '## Где стоим\n\n'
88
+ printf -- '- **Состояние:** %s\n' "$state"
89
+ [ -n "$stage" ] && printf -- '- **Этап:** %s\n' "$stage"
90
+ [ -n "$next_step" ] && printf -- '- **Следующий шаг:** %s\n' "$next_step"
91
+ [ -n "$pull" ] && printf -- '- **PR:** %s\n' "$pull"
92
+ printf '\nХод работы целиком — `%s/%s/progress.md`; замысел рядом с ним.\n\n' "$tasks_dir" "$branch"
93
+ else
94
+ printf '## Где стоим\n\nПапки задачи у этой ветки нет: состояние работы взять неоткуда.\n\n'
95
+ fi
96
+
97
+ printf '## Незакоммиченное\n\n```\n%s\n```\n\n' "$uncommitted"
98
+ printf '## Коммиты сверх главной\n\n```\n%s\n```\n\n' "$ahead"
99
+ printf 'Написана хуком перед сжатием контекста. Всё, что здесь стоит, проверяется деревом:\n'
100
+ printf 'передача пересказывает записанное и описывает минуту, когда её собрали.\n'
101
+ } > "$target" 2>/dev/null
102
+
103
+ exit 0
@@ -29,6 +29,8 @@
29
29
 
30
30
  # Сколько файлов линтуется за один перенос. Переезд либы трогает десятки файлов, и прогон по
31
31
  # каждому превратил бы хук в минутную паузу; на нарушение границы хватает первых.
32
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
33
+
32
34
  MAX_FILES=12
33
35
 
34
36
  input="$(cat 2>/dev/null)"
@@ -19,6 +19,8 @@
19
19
 
20
20
  # Значение, годное к записи. Путь не выносится ни при каких обстоятельствах, остальное чистится
21
21
  # до имени и обрезается: длинное значение в наблюдении не значит ничего, кроме утечки.
22
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
23
+
22
24
  rt_observe_clean() {
23
25
  case "$1" in
24
26
  */*) return 0 ;;
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env bash
2
2
  # rt-hook: Stop
3
+ # Требует: hooks/deny-tail.sh
3
4
  # Гард происшествия: ход, в котором исполнитель признал промах, не заканчивается, пока записи о
4
5
  # происшествии нет. Stop.
5
6
  #
@@ -20,6 +21,8 @@
20
21
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: при любой ошибке, отсутствии записи хода и повторном заходе ход
21
22
  # РАЗРЕШАЕТСЯ (exit 0). Сломанный гард не имеет права заклинить разговор.
22
23
 
24
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
25
+
23
26
  input="$(cat 2>/dev/null)"
24
27
  [ -z "$input" ] && exit 0
25
28
 
@@ -86,6 +89,17 @@ reason="BLOCKED by postmortem-guard: в ответе признан промах
86
89
 
87
90
  Гард судит один ход: следующий заход не отбивается."
88
91
 
92
+ # Общий хвост отказа: два законных хода. Файл может быть не разложен — тогда хвоста нет,
93
+ # а причина отказа остаётся прежней.
94
+ # shellcheck disable=SC1090
95
+ [ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" ] \
96
+ && . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" 2>/dev/null
97
+ command -v rt_deny_tail >/dev/null 2>&1 || rt_deny_tail() { :; }
98
+ deny_tail_text="$(rt_deny_tail "")"
99
+ [ -n "$deny_tail_text" ] && reason="${reason}
100
+
101
+ ${deny_tail_text}"
102
+
89
103
  jq -n --arg r "$reason" '{decision:"block",reason:$r}' 2>/dev/null \
90
104
  || printf '{"decision":"block","reason":"postmortem-guard: признан промах — запиши разбор происшествия."}\n'
91
105
 
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env bash
2
2
  # rt-hook: Stop
3
+ # Требует: hooks/deny-tail.sh
3
4
  # Гард предложения: ход, в котором владелец сказал завести или отправить предложение слою правил,
4
5
  # не заканчивается, пока отправки не было. Stop.
5
6
  #
@@ -19,6 +20,8 @@
19
20
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: при любой ошибке, нехватке `jq`, отсутствии записи хода и повторном
20
21
  # заходе ход РАЗРЕШАЕТСЯ (exit 0). Сломанный гард не имеет права заклинить разговор.
21
22
 
23
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
24
+
22
25
  input="$(cat 2>/dev/null)"
23
26
  [ -z "$input" ] && exit 0
24
27
 
@@ -87,6 +90,17 @@ reason="BLOCKED by proposal-guard: владелец сказал завести
87
90
 
88
91
  Гард судит один ход: следующий заход не отбивается."
89
92
 
93
+ # Общий хвост отказа: два законных хода. Файл может быть не разложен — тогда хвоста нет,
94
+ # а причина отказа остаётся прежней.
95
+ # shellcheck disable=SC1090
96
+ [ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" ] \
97
+ && . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" 2>/dev/null
98
+ command -v rt_deny_tail >/dev/null 2>&1 || rt_deny_tail() { :; }
99
+ deny_tail_text="$(rt_deny_tail "")"
100
+ [ -n "$deny_tail_text" ] && reason="${reason}
101
+
102
+ ${deny_tail_text}"
103
+
90
104
  jq -n --arg r "$reason" '{decision:"block",reason:$r}' 2>/dev/null \
91
105
  || printf '{"decision":"block","reason":"proposal-guard: владелец просил предложение — отправь его командой пакета."}\n'
92
106
 
@@ -0,0 +1,75 @@
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
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
16
+
17
+ input="$(cat 2>/dev/null)"
18
+ [ -z "$input" ] && exit 0
19
+ command -v jq >/dev/null 2>&1 || exit 0
20
+ command -v node >/dev/null 2>&1 || exit 0
21
+
22
+ tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
23
+ case "$tool" in
24
+ Edit | Write | MultiEdit) ;;
25
+ *) exit 0 ;;
26
+ esac
27
+
28
+ path="$(printf '%s' "$input" | jq -r '.tool_input.file_path // empty' 2>/dev/null)"
29
+ case "$path" in
30
+ *.md) ;;
31
+ *) exit 0 ;;
32
+ esac
33
+
34
+ # Описание прошлого и папки задач не судятся: архив не правится вовсе, а ход работы пишется
35
+ # наспех и живёт до слияния.
36
+ case "$path" in
37
+ */docs/archive/* | */docs/tasks/*) exit 0 ;;
38
+ esac
39
+
40
+ added="$(printf '%s' "$input" | jq -r '.tool_input.new_string // .tool_input.content // ([.tool_input.edits[]?.new_string] | join("\n")) // empty' 2>/dev/null)"
41
+ [ -z "$added" ] && exit 0
42
+
43
+ rt_hooks_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
44
+ check=""
45
+ 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
46
+ [ -f "$candidate" ] && check="$candidate" && break
47
+ done
48
+ [ -z "$check" ] && exit 0
49
+
50
+ tmp="$(mktemp -t prose)" || exit 0
51
+ printf '%s\n' "$added" > "$tmp"
52
+ found="$(node "$check" "$tmp" 2>&1 | grep -- '—' | sed 's|.*proba*[^:]*:| строка |' | head -8)"
53
+ rm -f "$tmp"
54
+ [ -z "$found" ] && exit 0
55
+
56
+ reason="BLOCKED by prose-style-guard: в новом тексте канцелярит или слово, которого в этом дереве не пишут.
57
+
58
+ ${found}
59
+
60
+ Правь текст, а не обходи находку: замена названа у каждой. Слог — правило о текстах, и проверка видит перечисленные признаки, а не стиль вообще: чистый по ней абзац может быть плохим, но грязный плохой точно."
61
+
62
+ # Общий хвост отказа: два законных хода и законная форма обхода, если она у отказа есть.
63
+ # Файл может быть не разложен — тогда хвоста нет, а причина отказа остаётся прежней.
64
+ # shellcheck disable=SC1090
65
+ [ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" ] \
66
+ && . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" 2>/dev/null
67
+ command -v rt_deny_tail >/dev/null 2>&1 || rt_deny_tail() { :; }
68
+ deny_tail_text="$(rt_deny_tail "")"
69
+ [ -n "$deny_tail_text" ] && reason="${reason}
70
+
71
+ ${deny_tail_text}"
72
+
73
+ jq -n --arg r "$reason" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$r}}' 2>/dev/null \
74
+ || printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"prose-style-guard: канцелярит в новом тексте."}}\n'
75
+ 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
  # Спеки адресуют элементы только через этот атрибут. Классы оформления меняются вместе с
@@ -25,6 +25,8 @@
25
25
  #
26
26
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: нет разборщика, битый ввод, чужой инструмент — пропуск.
27
27
 
28
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
29
+
28
30
  input="$(cat 2>/dev/null)"
29
31
  [ -z "$input" ] && exit 0
30
32
  command -v jq >/dev/null 2>&1 || exit 0
@@ -122,6 +124,17 @@ missing="$(printf '%s' "$added" | RT_QA_DECORATIVE="$decorative" RT_QA_COMPONENT
122
124
 
123
125
  reason="BLOCKED: интерактивные элементы без якоря для спек в ${path##*/}: ${missing}. Спеки адресуют элементы только через этот атрибут: классы оформления меняются вместе с вёрсткой, а поиск по роли и тексту ломается на переводах — оба вида селекторов делают сквозные спеки хрупкими. Проставь якорь через дефис по смыслу элемента; повторяющиеся элементы списка носят ОДИН якорь и различаются атрибутами данных. Если элемент чисто декоративный и спека его никогда не тронет — поставь атрибут пропуска НА САМ ЭТОТ ТЕГ, соседние элементы правки при этом продолжают проверяться."
124
126
 
127
+ # Общий хвост отказа: два законных хода и законная форма обхода, если она у отказа есть.
128
+ # Файл может быть не разложен — тогда хвоста нет, а причина отказа остаётся прежней.
129
+ # shellcheck disable=SC1090
130
+ [ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" ] \
131
+ && . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" 2>/dev/null
132
+ command -v rt_deny_tail >/dev/null 2>&1 || rt_deny_tail() { :; }
133
+ deny_tail_text="$(rt_deny_tail "атрибут пропуска на самом декоративном теге")"
134
+ [ -n "$deny_tail_text" ] && reason="${reason}
135
+
136
+ ${deny_tail_text}"
137
+
125
138
  jq -n --arg r "$reason" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$r}}' 2>/dev/null \
126
139
  || printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Интерактивный элемент без якоря для спек."}}\n'
127
140
 
@@ -0,0 +1,88 @@
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
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
24
+
25
+ input="$(cat 2>/dev/null)"
26
+ [ -z "$input" ] && exit 0
27
+ command -v jq >/dev/null 2>&1 || exit 0
28
+
29
+ tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
30
+ case "$tool" in
31
+ # Терминал среды и универсальный исполнитель кладут команду в то же поле.
32
+ Bash | mcp__webstorm__execute_terminal_command | mcp__webstorm__execute_tool) ;;
33
+ *) exit 0 ;;
34
+ esac
35
+
36
+ cmd="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null)"
37
+ [ -z "$cmd" ] && exit 0
38
+
39
+ # Профиль дерева: сперва умолчание пакета, поверх него — надстройка проекта, если она есть.
40
+ # Имя клиента хостинга дерево называет само: у каждого вида оно своё, а угаданное не совпадает
41
+ # ни с чем.
42
+ rt_hooks_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
43
+ 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
44
+ # shellcheck disable=SC1090
45
+ [ -f "$profile" ] && . "$profile" 2>/dev/null
46
+ done
47
+
48
+ # Вызов перезапуска: слово перезапуска отдельным словом рядом со словом прогона.
49
+ host_cli="${RT_HOST_CLI:-gh}"
50
+ printf '%s' "$cmd" | grep -qE "(^|[;&|(]|&&|\|\|)[[:space:]]*${host_cli}([[:space:]]|$)" || exit 0
51
+ printf '%s' "$cmd" | grep -qE '(^|[[:space:]])(run|workflow)([[:space:]]|$)' || exit 0
52
+ printf '%s' "$cmd" | grep -qE '(^|[[:space:]])rerun([[:space:]]|$)' || exit 0
53
+
54
+ # Номер задания. Не назван — гард молчит: судить не о чем.
55
+ run_id="$(printf '%s' "$cmd" | grep -oE '(^|[[:space:]])[0-9]{6,}([[:space:]]|$)' | tr -d ' ' | head -1)"
56
+ [ -z "$run_id" ] && exit 0
57
+
58
+ transcript="$(printf '%s' "$input" | jq -r '.transcript_path // empty' 2>/dev/null)"
59
+ [ -z "$transcript" ] && exit 0
60
+ [ -f "$transcript" ] || exit 0
61
+
62
+ # Читался ли журнал ЭТОГО задания за тот же ход. Ход — вызовы после последней реплики владельца:
63
+ # журнал, прочитанный вчера, о сегодняшнем состоянии прогона не говорит ничего.
64
+ seen="$(jq -s -r --arg id "$run_id" '
65
+ [.[] | select(.type == "assistant") | (.message.content // [])[] | select(.type == "tool_use")
66
+ | ((.input.command // "") | tostring)] as $used
67
+ | (($used | join("\n")) | test("run[[:space:]]+view[^\\n]*" + $id + "|" + $id + "[^\\n]*--log")) as $read
68
+ | if $read then "читал" else "не-читал" end
69
+ ' "$transcript" 2>/dev/null)"
70
+
71
+ [ "$seen" = "читал" ] && exit 0
72
+
73
+ reason="BLOCKED by rerun-guard: перезапуск задания ${run_id} без прочитанного журнала. Красное на прогоне бывает двух родов, и в списке они выглядят одинаково: отказ хостинга на шаге подготовки лечится перезапуском, дефект ветки — не лечится им вовсе, и круг повторяется, пока журнал не открыт. Прочитай журнал этого задания — ${host_cli} run view ${run_id} --log-failed — и повтори вызов. Гард судит порядок, а не причину падения: что в журнале написано, судишь ты."
74
+
75
+ # Общий хвост отказа: два законных хода и законная форма обхода, если она у отказа есть.
76
+ # Файл может быть не разложен — тогда хвоста нет, а причина отказа остаётся прежней.
77
+ # shellcheck disable=SC1090
78
+ [ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" ] \
79
+ && . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" 2>/dev/null
80
+ command -v rt_deny_tail >/dev/null 2>&1 || rt_deny_tail() { :; }
81
+ deny_tail_text="$(rt_deny_tail "")"
82
+ [ -n "$deny_tail_text" ] && reason="${reason}
83
+
84
+ ${deny_tail_text}"
85
+
86
+ jq -n --arg r "$reason" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$r}}' 2>/dev/null \
87
+ || printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"rerun-guard: журнал задания за этот ход не читался."}}\n'
88
+ 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
  # Линтеры знают правила, но не знают ИНВЕНТАРЬ: линтер стилей поймает сырой цвет, линтер кода —
@@ -32,6 +32,8 @@
32
32
  #
33
33
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: нет разборщика, битый ввод, чужой инструмент — пропуск.
34
34
 
35
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
36
+
35
37
  input="$(cat 2>/dev/null)"
36
38
  [ -z "$input" ] && exit 0
37
39
  command -v jq >/dev/null 2>&1 || exit 0
@@ -312,6 +314,17 @@ ${found}
312
314
  Порядок действий: 1) открой готовое — барель кита или базовый класс — и используй его; 2) найди в дереве экран, где этот случай уже собран, и повтори сборку; 3) если готового правда не хватает — расширяй его на месте, у готового, а не клонируй рядом: клон забирает правки на себя и расходится с оригиналом с первой же.
313
315
  Свой примитив, своя основа и свои инлайновые стили заводятся только с явного одобрения владельца, и спрашивается это до первого написанного файла. Разовое исключение помечается маркером отступления в той же строке, с объяснением, чего именно нет в готовом. Переименованием файла это не обходится."
314
316
 
317
+ # Общий хвост отказа: два законных хода и законная форма обхода, если она у отказа есть.
318
+ # Файл может быть не разложен — тогда хвоста нет, а причина отказа остаётся прежней.
319
+ # shellcheck disable=SC1090
320
+ [ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" ] \
321
+ && . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" 2>/dev/null
322
+ command -v rt_deny_tail >/dev/null 2>&1 || rt_deny_tail() { :; }
323
+ deny_tail_text="$(rt_deny_tail "маркер отступления в той же строке, с объяснением, чего именно нет в готовом")"
324
+ [ -n "$deny_tail_text" ] && reason="${reason}
325
+
326
+ ${deny_tail_text}"
327
+
315
328
  jq -n --arg r "$reason" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$r}}' 2>/dev/null \
316
329
  || printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Написано своё там, где готовое уже есть."}}\n'
317
330
 
@@ -0,0 +1,34 @@
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
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
23
+
24
+ rt_role_off() {
25
+ local role="$1" config
26
+ [ -n "$role" ] || return 1
27
+ command -v jq >/dev/null 2>&1 || return 1
28
+ config="${CLAUDE_PROJECT_DIR:-.}/.claude/rt-kit.json"
29
+ [ -f "$config" ] || return 1
30
+ [ "$(jq -r --arg role "$role" '
31
+ if ((.rolesOff // []) | type) == "array" and (((.rolesOff // []) | index($role)) != null)
32
+ then "off" else "on" end
33
+ ' "$config" 2>/dev/null)" = "off" ]
34
+ }
@@ -20,6 +20,8 @@
20
20
 
21
21
  # Уже названное вторым разом не требуется: отказ, перечисляющий одно правило дважды, читается
22
22
  # как два разных требования.
23
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
24
+
23
25
  rt_layer_add() {
24
26
  case " $req " in
25
27
  *" $1 "*) return 0 ;;
@@ -12,6 +12,8 @@
12
12
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: любая ошибка пропускает. Хук удаляет временный файл и ничего не
13
13
  # отбивает.
14
14
 
15
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
16
+
15
17
  input="$(cat 2>/dev/null)"
16
18
  [ -z "$input" ] && exit 0
17
19
 
@@ -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
  # Закон и правило, которых никто не открывает, не действуют. Напоминание в подсказке помогает
@@ -22,6 +23,8 @@
22
23
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: любая ошибка и любой неопознанный путь пропускают правку (exit 0).
23
24
  # Сломанный гейт не имеет права остановить работу совсем.
24
25
 
26
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
27
+
25
28
  input="$(cat 2>/dev/null)"
26
29
  [ -z "$input" ] && exit 0
27
30
 
@@ -172,6 +175,17 @@ if [ -n "$law" ]; then
172
175
  && reason="Отбито гейтом правил: загрузи правило «${req}» инструментом Skill — оно применяет закон ${law_path} к этому дереву — и повтори действие. ${fallback} Для этой области это происходит один раз за сессию."
173
176
  fi
174
177
 
178
+ # Общий хвост отказа: два законных хода и законная форма обхода, если она у отказа есть.
179
+ # Файл может быть не разложен — тогда хвоста нет, а причина отказа остаётся прежней.
180
+ # shellcheck disable=SC1090
181
+ [ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" ] \
182
+ && . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" 2>/dev/null
183
+ command -v rt_deny_tail >/dev/null 2>&1 || rt_deny_tail() { :; }
184
+ deny_tail_text="$(rt_deny_tail "")"
185
+ [ -n "$deny_tail_text" ] && reason="${reason}
186
+
187
+ ${deny_tail_text}"
188
+
175
189
  jq -n --arg r "$reason" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$r}}' 2>/dev/null \
176
190
  || printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Загрузи правило %s и повтори."}}\n' "$req"
177
191
 
@@ -7,6 +7,8 @@
7
7
  #
8
8
  # Хук только наблюдает: он всегда пропускает и ничего не отбивает.
9
9
 
10
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
11
+
10
12
  input="$(cat 2>/dev/null)"
11
13
  [ -z "$input" ] && exit 0
12
14
 
@@ -13,6 +13,8 @@
13
13
  # команда становилась одним сегментом, доказательство чтения из первой строки покрывало
14
14
  # неразобранный вызов из второй, и запись на боевую базу проходила молча — тот же обход,
15
15
  # что чинили для `&&`, только набранный с новой строки.
16
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
17
+
16
18
  normalize() {
17
19
  perl -0ne '
18
20
  # Продолжение длинной команды обратным слешем — это одна строка, а не две: без
@@ -10,6 +10,8 @@
10
10
 
11
11
  # Три вида ввода: запрос через подключение редактора, команда оболочки и та же команда,
12
12
  # завёрнутая в универсальный исполнитель. Инструмент, не названный здесь, гарда не касается.
13
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
14
+
13
15
  sql_read_request() {
14
16
  sql=""
15
17
  context=""
@@ -18,6 +18,8 @@
18
18
  # подключения, и по одному SQL отличить прод от локальной копии невозможно. Поэтому
19
19
  # подключения опознаются в лицо. Список сверяется вызовом list_database_connections;
20
20
  # добавили новое — допишите сюда, иначе оно попадёт в «неизвестные» ниже.
21
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
22
+
21
23
  sql_resolve_target() {
22
24
  is_prod=""
23
25
  PROD_CONNECTIONS="${RT_PROD_CONNECTIONS:-}"
@@ -7,6 +7,8 @@
7
7
 
8
8
  # Глаголы ищутся в сегментах ВЫЗОВА, а не по всей строке: иначе слово из шаблона поиска в
9
9
  # соседнем звене цепочки объявляло записью читающую команду.
10
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
11
+
10
12
  sql_detect_write() {
11
13
  is_write=""
12
14
  write_scope="$segments"
@@ -67,7 +69,8 @@ verdict() {
67
69
  if [ -n "$soft" ]; then
68
70
  ask "Запрос помечен маркером destructive-ok, но остаётся разрушительным: $1 Подтверди выполнение, если это осознанно."
69
71
  fi
70
- deny "BLOCKED: $1 Адресуй строки по первичному ключу — \`WHERE id IN ('…','…')\`: так затрагивается ровно столько строк, сколько перечислено, и промах виден до выполнения. Удаление по маске (email LIKE '%test%') однажды унесло вместе с тестовыми записями демонстрационные брони владельца. Если адресация по id действительно не подходит — сначала выполни SELECT с тем же условием, покажи пользователю, что попадает под удаление, и помечай запрос маркером destructive-ok с объяснением."
72
+ deny "BLOCKED: $1 Адресуй строки по первичному ключу — \`WHERE id IN ('…','…')\`: так затрагивается ровно столько строк, сколько перечислено, и промах виден до выполнения. Удаление по маске (email LIKE '%test%') однажды унесло вместе с тестовыми записями демонстрационные брони владельца. Если адресация по id действительно не подходит — сначала выполни SELECT с тем же условием и покажи пользователю, что попадает под удаление." \
73
+ "маркер destructive-ok при запросе, с объяснением"
71
74
  }
72
75
 
73
76
  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
  # Правка данных — единственное действие, которое нельзя откатить правкой кода. Удаление по
@@ -39,6 +40,8 @@
39
40
  # пропускаются целиком — иначе слова «drop» и «update» в описании коммита читались бы как
40
41
  # запрос. Доставки запроса внутри такой команды не бывает, так что цена послабления нулевая.
41
42
 
43
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
44
+
42
45
  input="$(cat 2>/dev/null)"
43
46
  [ -z "$input" ] && exit 0
44
47
  command -v jq >/dev/null 2>&1 || exit 0
@@ -69,8 +72,18 @@ done
69
72
 
70
73
  PROD_DSN="${RT_PROD_DSN:-}"
71
74
 
75
+ # Общий хвост отказа: два законных хода и законная форма обхода, если она у отказа есть. Файл
76
+ # может быть не разложен — тогда хвоста нет, а причина отказа остаётся прежней.
77
+ # shellcheck disable=SC1090
78
+ [ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" ] \
79
+ && . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" 2>/dev/null
80
+ command -v rt_deny_tail >/dev/null 2>&1 || rt_deny_tail() { :; }
81
+
72
82
  deny() {
73
- jq -n --arg r "$1" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$r}}' 2>/dev/null \
83
+ reason="$1"
84
+ tail_text="$(rt_deny_tail "$2")"
85
+ [ -n "$tail_text" ] && reason="$1 ${tail_text}"
86
+ jq -n --arg r "$reason" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$r}}' 2>/dev/null \
74
87
  || printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Destructive SQL blocked. Address rows by id."}}\n'
75
88
  exit 0
76
89
  }