@rt-tools/agent-kit 0.10.0 → 0.12.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 (139) hide show
  1. package/assets/checks/check-file-size.mjs +19 -4
  2. package/assets/checks/check-state-next.mjs +202 -0
  3. package/assets/checks/check-states.mjs +142 -0
  4. package/assets/checks/check-turn-map.mjs +146 -0
  5. package/assets/checks/rt-kit-checks.config.mjs +16 -2
  6. package/assets/defaults/project.sh +67 -7
  7. package/assets/defaults/turn-map.md +48 -0
  8. package/assets/hooks/browser-device-id.sh +2 -0
  9. package/assets/hooks/browser-guard-device-id.sh +5 -1
  10. package/assets/hooks/browser-guard-no-asking.sh +5 -1
  11. package/assets/hooks/browser-guard-no-listing.sh +2 -0
  12. package/assets/hooks/browser-guard-no-other-drivers.sh +7 -3
  13. package/assets/hooks/browser-guard-require-select.sh +6 -2
  14. package/assets/hooks/claim-guard.sh +5 -1
  15. package/assets/hooks/commit-msg.sh +2 -0
  16. package/assets/hooks/conscience-guard.sh +5 -1
  17. package/assets/hooks/constitution-index.sh +2 -0
  18. package/assets/hooks/dev-server-guard.sh +7 -3
  19. package/assets/hooks/dispatch.sh +69 -0
  20. package/assets/hooks/docs-guard.sh +8 -4
  21. package/assets/hooks/exam-guard.sh +7 -3
  22. package/assets/hooks/git-guard-delivery-signature.sh +2 -0
  23. package/assets/hooks/git-guard-delivery.sh +39 -5
  24. package/assets/hooks/git-guard-main.sh +8 -4
  25. package/assets/hooks/git-guard-push-tests.sh +8 -4
  26. package/assets/hooks/glossary-load.sh +2 -0
  27. package/assets/hooks/grill-gate.sh +6 -2
  28. package/assets/hooks/handoff-entry-guard.sh +6 -2
  29. package/assets/hooks/handoff-write.sh +124 -0
  30. package/assets/hooks/hook-input.sh +54 -0
  31. package/assets/hooks/lint-after-edit.sh +7 -3
  32. package/assets/hooks/observe.sh +2 -0
  33. package/assets/hooks/postmortem-guard.sh +5 -1
  34. package/assets/hooks/proposal-guard.sh +5 -1
  35. package/assets/hooks/prose-style-guard.sh +7 -3
  36. package/assets/hooks/qa-dataid-guard.sh +6 -2
  37. package/assets/hooks/rerun-guard.sh +7 -3
  38. package/assets/hooks/reuse-first-guard.sh +7 -3
  39. package/assets/hooks/roles.sh +2 -0
  40. package/assets/hooks/rule-article.sh +99 -0
  41. package/assets/hooks/skill-gate-layers.sh +2 -0
  42. package/assets/hooks/skill-gate-rearm.sh +5 -1
  43. package/assets/hooks/skill-gate.sh +25 -2
  44. package/assets/hooks/skill-loaded.sh +5 -1
  45. package/assets/hooks/sql-guard-parse.sh +2 -0
  46. package/assets/hooks/sql-guard-request.sh +4 -1
  47. package/assets/hooks/sql-guard-target.sh +2 -0
  48. package/assets/hooks/sql-guard-write.sh +2 -0
  49. package/assets/hooks/sql-guard.sh +6 -2
  50. package/assets/hooks/task-context-load.sh +2 -0
  51. package/assets/hooks/task-flow-guard.sh +8 -4
  52. package/assets/hooks/turn-entry-load.sh +62 -0
  53. package/assets/hooks/turn-exit-guard.sh +44 -17
  54. package/assets/hooks/utf8.sh +35 -0
  55. package/assets/hooks/waiting-turn-guard.sh +5 -1
  56. package/assets/hooks/window-fill-guard.sh +37 -5
  57. package/assets/laws/work-conduct.md +34 -9
  58. package/assets/patterns/dependencies-upgrade.md +1 -1
  59. package/assets/patterns/doc-style-write.md +3 -3
  60. package/assets/patterns/git-workflow-commit.azure.md +2 -202
  61. package/assets/patterns/git-workflow-commit.github.md +2 -258
  62. package/assets/patterns/git-workflow-commit.gitlab.md +1 -217
  63. package/assets/patterns/git-workflow-docker.md +3 -3
  64. package/assets/patterns/git-workflow-merge.md +3 -2
  65. package/assets/patterns/git-workflow-migration.md +3 -3
  66. package/assets/patterns/git-workflow-pr.azure.md +224 -0
  67. package/assets/patterns/git-workflow-pr.github.md +280 -0
  68. package/assets/patterns/git-workflow-pr.gitlab.md +240 -0
  69. package/assets/patterns/git-workflow-restart.md +3 -3
  70. package/assets/patterns/git-workflow-secrets.md +3 -3
  71. package/assets/patterns/task-flow-archive.md +193 -0
  72. package/assets/patterns/task-flow-close.md +11 -160
  73. package/assets/patterns/task-flow-handoff.md +22 -4
  74. package/assets/patterns/task-flow-resume.md +6 -0
  75. package/assets/patterns/task-flow-start.md +41 -0
  76. package/assets/patterns/turn-entry-map.md +81 -0
  77. package/assets/pitfalls/doc-style.md +80 -0
  78. package/assets/pitfalls/git-workflow.azure.md +50 -0
  79. package/assets/pitfalls/git-workflow.github.md +78 -0
  80. package/assets/pitfalls/git-workflow.gitlab.md +49 -0
  81. package/assets/pitfalls/spec-driven.md +36 -0
  82. package/assets/pitfalls/styling-bem.md +45 -0
  83. package/assets/pitfalls/task-flow.md +62 -0
  84. package/assets/pitfalls/testing.md +70 -0
  85. package/assets/rules/deploy-flow.azure.md +106 -0
  86. package/assets/rules/deploy-flow.github.md +113 -0
  87. package/assets/rules/deploy-flow.gitlab.md +108 -0
  88. package/assets/rules/doc-style.md +25 -76
  89. package/assets/rules/git-workflow.azure.md +6 -92
  90. package/assets/rules/git-workflow.github.md +14 -127
  91. package/assets/rules/git-workflow.gitlab.md +6 -93
  92. package/assets/rules/spec-driven.md +39 -30
  93. package/assets/rules/styling-bem.md +20 -39
  94. package/assets/rules/task-flow.md +17 -186
  95. package/assets/rules/testing.md +3 -64
  96. package/assets/rules/turn-conduct.md +206 -0
  97. package/assets/rules/turn-entry.md +93 -0
  98. package/assets/rules/typescript-conventions.md +15 -0
  99. package/assets/skills/agent-kit.md +35 -12
  100. package/assets/templates/pitfalls.md +10 -0
  101. package/assets/templates/rule.md +5 -3
  102. package/lib/assets.d.ts.map +1 -1
  103. package/lib/assets.js +6 -1
  104. package/lib/assets.js.map +1 -1
  105. package/lib/cargo.d.ts +42 -0
  106. package/lib/cargo.d.ts.map +1 -1
  107. package/lib/cargo.js +2 -0
  108. package/lib/cargo.js.map +1 -1
  109. package/lib/cascade.d.ts.map +1 -1
  110. package/lib/cascade.js +19 -1
  111. package/lib/cascade.js.map +1 -1
  112. package/lib/commands.d.ts.map +1 -1
  113. package/lib/commands.js +65 -4
  114. package/lib/commands.js.map +1 -1
  115. package/lib/config.d.ts +16 -1
  116. package/lib/config.d.ts.map +1 -1
  117. package/lib/config.js +8 -0
  118. package/lib/config.js.map +1 -1
  119. package/lib/hooks-map.d.ts +13 -0
  120. package/lib/hooks-map.d.ts.map +1 -1
  121. package/lib/hooks-map.js +33 -1
  122. package/lib/hooks-map.js.map +1 -1
  123. package/lib/observations.d.ts +35 -1
  124. package/lib/observations.d.ts.map +1 -1
  125. package/lib/observations.js +14 -2
  126. package/lib/observations.js.map +1 -1
  127. package/lib/ship.d.ts +2 -0
  128. package/lib/ship.d.ts.map +1 -1
  129. package/lib/ship.js +2 -0
  130. package/lib/ship.js.map +1 -1
  131. package/lib/thresholds.d.ts +49 -0
  132. package/lib/thresholds.d.ts.map +1 -0
  133. package/lib/thresholds.js +151 -0
  134. package/lib/thresholds.js.map +1 -0
  135. package/package.json +1 -1
  136. package/rt-tools-agent-kit-0.12.0.tgz +0 -0
  137. package/assets/commands/agent-kit-digest.md +0 -88
  138. package/assets/commands/rules-review.md +0 -98
  139. package/rt-tools-agent-kit-0.10.0.tgz +0 -0
@@ -25,17 +25,21 @@
25
25
  #
26
26
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: не репозиторий, битый ввод, нет профиля — пропуск.
27
27
 
28
- input="$(cat 2>/dev/null)"
28
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
29
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
30
+
31
+ rt_hook_read
32
+ input="$RT_HOOK_INPUT"
29
33
  [ -z "$input" ] && exit 0
30
34
 
31
- tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
35
+ tool="$(rt_hook_tool)"
32
36
  case "$tool" in
33
37
  # Терминал среды и универсальный исполнитель кладут команду в то же поле.
34
38
  Bash | mcp__webstorm__execute_terminal_command | mcp__webstorm__execute_tool) ;;
35
39
  *) exit 0 ;;
36
40
  esac
37
41
 
38
- cmd="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null)"
42
+ cmd="$(rt_hook_cmd)"
39
43
 
40
44
  # Вызов пуша узнаётся по двум признакам сразу — команда `git` в начале строки или за
41
45
  # разделителем и слово `push` отдельным словом. Тем же приёмом, что у гарда поставки: одной
@@ -86,7 +90,7 @@ ${deny_tail_text}"
86
90
  exit 0
87
91
  fi
88
92
 
89
- workdir="$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null)"
93
+ workdir="$(rt_hook_cwd)"
90
94
  [ -z "$workdir" ] && workdir="${CLAUDE_PROJECT_DIR:-.}"
91
95
  cd "$workdir" 2>/dev/null || exit 0
92
96
  git rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0
@@ -9,6 +9,8 @@
9
9
  #
10
10
  # FAIL-OPEN: нет файла или нет `jq` — выходим молча. Сессия важнее словаря.
11
11
 
12
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
13
+
12
14
  GLOSSARY="${CLAUDE_PROJECT_DIR:-.}/docs/GLOSSARY.md"
13
15
  [ -f "$GLOSSARY" ] || exit 0
14
16
  command -v jq >/dev/null 2>&1 || exit 0
@@ -22,13 +22,17 @@
22
22
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: при любой ошибке, отсутствии записи хода и повторном заходе ход
23
23
  # РАЗРЕШАЕТСЯ (exit 0). Сломанный гард не имеет права заклинить разговор.
24
24
 
25
- input="$(cat 2>/dev/null)"
25
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
26
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
27
+
28
+ rt_hook_read
29
+ input="$RT_HOOK_INPUT"
26
30
  [ -z "$input" ] && exit 0
27
31
 
28
32
  command -v jq >/dev/null 2>&1 || exit 0
29
33
 
30
34
  # Какое событие пришло. У вызова инструмента есть его имя, у завершения хода — нет.
31
- tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
35
+ tool="$(rt_hook_tool)"
32
36
 
33
37
  # Повторный заход по тому же ходу не судится: иначе ход не кончится никогда — гард сказал своё
34
38
  # один раз и отпускает. К вызову инструмента это не относится: там судится сам вызов.
@@ -14,11 +14,15 @@
14
14
  #
15
15
  # FAIL-OPEN: нет `jq`, нет записи хода, передачи в реплике нет → пропуск.
16
16
 
17
- input="$(cat 2>/dev/null)"
17
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
18
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
19
+
20
+ rt_hook_read
21
+ input="$RT_HOOK_INPUT"
18
22
  [ -z "$input" ] && exit 0
19
23
  command -v jq >/dev/null 2>&1 || exit 0
20
24
 
21
- tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
25
+ tool="$(rt_hook_tool)"
22
26
  case "$tool" in
23
27
  Edit | Write | MultiEdit) ;;
24
28
  *) exit 0 ;;
@@ -0,0 +1,124 @@
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
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
24
+
25
+ rt_hook_read
26
+ input="$RT_HOOK_INPUT"
27
+ [ -z "$input" ] && exit 0
28
+
29
+ command -v jq >/dev/null 2>&1 || exit 0
30
+
31
+ rt_hooks_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
32
+ for profile in "$rt_hooks_dir/../rt-kit/defaults/project.sh" "$rt_hooks_dir/../defaults/project.sh" \
33
+ "${CLAUDE_PROJECT_DIR:-.}/.claude/rt-kit/defaults/project.sh" "${CLAUDE_PROJECT_DIR:-.}/.claude/rt-kit/project.sh"; do
34
+ # shellcheck disable=SC1090
35
+ [ -f "$profile" ] && . "$profile" 2>/dev/null
36
+ done
37
+
38
+ workdir="$(rt_hook_cwd)"
39
+ [ -z "$workdir" ] && workdir="${CLAUDE_PROJECT_DIR:-.}"
40
+ cd "$workdir" 2>/dev/null || exit 0
41
+ git rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0
42
+
43
+ # Пустое имя ветки хук не останавливает. На отсоединённой голове имени нет, а всё, что едет в
44
+ # передачу, лежит в дереве и доступно целиком: имя нужно только файлу. Прежний выход нулём был
45
+ # полным молчанием — сжатие приходило без передачи, и заход после него начинал с пустого места.
46
+ branch="$(git branch --show-current 2>/dev/null)"
47
+ head_name=""
48
+ if [ -z "$branch" ]; then
49
+ head_name="$(git rev-parse --short HEAD 2>/dev/null)"
50
+ [ -z "$head_name" ] && head_name='detached'
51
+ head_name="detached-$head_name"
52
+ fi
53
+
54
+ root="$(git rev-parse --show-toplevel 2>/dev/null)"
55
+ [ -z "$root" ] && exit 0
56
+
57
+ handoff_dir="${RT_HANDOFF_DIR:-.claude/handoff}"
58
+ tasks_dir="${RT_TASKS_DIR:-docs/tasks}"
59
+
60
+ mkdir -p "$root/$handoff_dir" 2>/dev/null || exit 0
61
+ [ -d "$root/$handoff_dir" ] || exit 0
62
+
63
+ # Ручное сжатие от автоматического отличается одним: при ручном исполнитель у клавиатуры и
64
+ # может дописать передачу сам. Пишется она в обоих случаях — заход, сжатый руками, теряет
65
+ # контекст ровно так же.
66
+ trigger="$(printf '%s' "$input" | jq -r '.trigger // "auto"' 2>/dev/null)"
67
+ [ -z "$trigger" ] && trigger='auto'
68
+
69
+ # Файл передачи ищут по имени ветки; голове без имени он называется её коротким снимком.
70
+ handoff_name="$branch"
71
+ [ -z "$handoff_name" ] && handoff_name="$head_name"
72
+
73
+ progress=""
74
+ [ -n "$branch" ] && progress="$root/$tasks_dir/$branch/progress.md"
75
+
76
+ # Строка раздела «Где стоим» по её названию. Пусто — работа идёт вне папки задачи, и выдумывать
77
+ # за неё состояние нельзя: в передаче тогда стоит то, что известно дереву.
78
+ line_of() {
79
+ [ -f "$progress" ] || return 0
80
+ sed -n "s/^[[:space:]]*[-*][[:space:]]*\*\*$1:\*\*[[:space:]]*\(.*\)/\1/p" "$progress" 2>/dev/null | head -1
81
+ }
82
+
83
+ state="$(line_of 'Состояние')"
84
+ stage="$(line_of 'Этап')"
85
+ next_step="$(line_of 'Следующий шаг')"
86
+ pull="$(line_of 'PR')"
87
+
88
+ uncommitted="$(git status --short 2>/dev/null | head -20)"
89
+ [ -z "$uncommitted" ] && uncommitted='нет'
90
+
91
+ ahead="$(git log --oneline origin/main..HEAD 2>/dev/null | head -20)"
92
+ [ -z "$ahead" ] && ahead='нет коммитов сверх главной'
93
+
94
+ target="$root/$handoff_dir/$handoff_name.md"
95
+
96
+ {
97
+ printf '# Передача захода — сжатие контекста (%s)\n\n' "$trigger"
98
+ printf '**Рабочее дерево:** %s\n' "$root"
99
+ if [ -n "$branch" ]; then
100
+ printf '**Ветка:** %s\n\n' "$branch"
101
+ else
102
+ printf '**Ветка:** имени нет — отсоединённая голова %s\n\n' "$head_name"
103
+ fi
104
+
105
+ if [ -n "$state" ]; then
106
+ printf '## Где стоим\n\n'
107
+ printf -- '- **Состояние:** %s\n' "$state"
108
+ [ -n "$stage" ] && printf -- '- **Этап:** %s\n' "$stage"
109
+ [ -n "$next_step" ] && printf -- '- **Следующий шаг:** %s\n' "$next_step"
110
+ [ -n "$pull" ] && printf -- '- **PR:** %s\n' "$pull"
111
+ printf '\nХод работы целиком — `%s/%s/progress.md`; замысел рядом с ним.\n\n' "$tasks_dir" "$branch"
112
+ elif [ -z "$branch" ]; then
113
+ printf '## Где стоим\n\nИмени у головы нет, папки задачи при ней тоже: состояние работы взять неоткуда. Читающая сторона ищет передачу по имени ветки, а не найдя его — по последней записи каталога.\n\n'
114
+ else
115
+ printf '## Где стоим\n\nПапки задачи у этой ветки нет: состояние работы взять неоткуда.\n\n'
116
+ fi
117
+
118
+ printf '## Незакоммиченное\n\n```\n%s\n```\n\n' "$uncommitted"
119
+ printf '## Коммиты сверх главной\n\n```\n%s\n```\n\n' "$ahead"
120
+ printf 'Написана хуком перед сжатием контекста. Всё, что здесь стоит, проверяется деревом:\n'
121
+ printf 'передача пересказывает записанное и описывает минуту, когда её собрали.\n'
122
+ } > "$target" 2>/dev/null
123
+
124
+ exit 0
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env bash
2
+ # Общее чтение ввода хука. НЕ гард: объявления `rt-hook:` у него нет, к событиям агента он не
3
+ # подключается. Его источают сами гарды — тем же приёмом, каким они источают общий хвост отказа.
4
+ #
5
+ # Зачем он есть. Ввод приходит одним объектом, и каждому гарду из него нужны те же поля: имя
6
+ # инструмента, командная строка, путь правки, рабочий каталог. Пока каждый доставал их сам, один
7
+ # вызов агента стоил больше сотни разборов одного и того же текста — по шесть-семь на гард,
8
+ # восемнадцать гардов на событие. Разбор стоит шесть миллисекунд, запуск оболочки — пять.
9
+ #
10
+ # Как это работает. Диспетчер разбирает ввод однажды и кладёт поля в окружение. Гард спрашивает
11
+ # их отсюда: есть в окружении — берёт готовое, нет — разбирает сам. Второе — это прямой вызов:
12
+ # так гарды зовут наборы сценариев, и работать они обязаны и без диспетчера.
13
+ #
14
+ # ПОЧЕМУ ЧТЕНИЕ ПОТОКА — ОТДЕЛЬНАЯ КОМАНДА, А НЕ ЗНАЧЕНИЕ. Подстановка команд исполняется в
15
+ # подоболочке, и всё, что она запомнила, умирает вместе с ней: первый вызов вычитал бы поток, а
16
+ # второй получил бы пустоту — и гард пропустил бы вызов, который обязан был отбить. Поэтому поток
17
+ # читается командой `rt_hook_read`, кладущей ввод в переменную текущей оболочки, а не функцией,
18
+ # возвращающей его через подстановку.
19
+ #
20
+ # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: нет разборщика, битый ввод, пустое поле — пустая строка. Сломанное
21
+ # чтение не имеет права заклинить работу: гард на пустом поле выходит нулём.
22
+
23
+ # Читает ввод в `RT_HOOK_INPUT`, если его там ещё нет. Зовётся командой, а не подстановкой.
24
+ rt_hook_read() {
25
+ [ -n "${RT_HOOK_INPUT:-}" ] && return 0
26
+
27
+ RT_HOOK_INPUT="$(cat 2>/dev/null)"
28
+ export RT_HOOK_INPUT
29
+ return 0
30
+ }
31
+
32
+ # Поле ввода по имени переменной и пути в объекте: `rt_hook_field RT_HOOK_TOOL '.tool_name'`.
33
+ # Переменная окружения главнее разбора — её кладёт диспетчер, разобравший ввод один раз.
34
+ rt_hook_field() {
35
+ local name="$1" path="$2"
36
+
37
+ # Готовому верят только по признаку разбора, а не по тому, что переменная объявлена. Пустая
38
+ # переменная в окружении означает «поля нет», и без признака она неотличима от «поле есть и
39
+ # оно пустое»: гард переставал разбирать ввод и пропускал вызовы, которые обязан был отбить.
40
+ # Наборы поймали это шестнадцатью провалами — окружение прогона несло пустые значения.
41
+ if [ "${RT_HOOK_PARSED:-}" = '1' ] && [ -n "${!name+x}" ]; then
42
+ printf '%s' "${!name}"
43
+ return 0
44
+ fi
45
+
46
+ rt_hook_read
47
+ printf '%s' "$RT_HOOK_INPUT" | jq -r "${path} // empty" 2>/dev/null
48
+ }
49
+
50
+ # Поля, которые спрашивают все гарды.
51
+ rt_hook_tool() { rt_hook_field RT_HOOK_TOOL '.tool_name'; }
52
+ rt_hook_cmd() { rt_hook_field RT_HOOK_CMD '.tool_input.command'; }
53
+ rt_hook_file() { rt_hook_field RT_HOOK_FILE '.tool_input.file_path'; }
54
+ rt_hook_cwd() { rt_hook_field RT_HOOK_CWD '.cwd'; }
@@ -29,13 +29,17 @@
29
29
 
30
30
  # Сколько файлов линтуется за один перенос. Переезд либы трогает десятки файлов, и прогон по
31
31
  # каждому превратил бы хук в минутную паузу; на нарушение границы хватает первых.
32
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
33
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
34
+
32
35
  MAX_FILES=12
33
36
 
34
- input="$(cat 2>/dev/null)"
37
+ rt_hook_read
38
+ input="$RT_HOOK_INPUT"
35
39
  [ -z "$input" ] && exit 0
36
40
  command -v jq >/dev/null 2>&1 || exit 0
37
41
 
38
- tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
42
+ tool="$(rt_hook_tool)"
39
43
  case "$tool" in
40
44
  Edit | Write | MultiEdit | mcp__webstorm__create_new_file) mode="edit" ;;
41
45
  Bash) mode="move" ;;
@@ -52,7 +56,7 @@ if [ "$mode" = "move" ]; then
52
56
  esac
53
57
  fi
54
58
 
55
- workdir="$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null)"
59
+ workdir="$(rt_hook_cwd)"
56
60
  [ -z "$workdir" ] && workdir="${CLAUDE_PROJECT_DIR:-.}"
57
61
  cd "$workdir" 2>/dev/null || exit 0
58
62
  [ -f package.json ] || exit 0
@@ -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 ;;
@@ -21,7 +21,11 @@
21
21
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: при любой ошибке, отсутствии записи хода и повторном заходе ход
22
22
  # РАЗРЕШАЕТСЯ (exit 0). Сломанный гард не имеет права заклинить разговор.
23
23
 
24
- input="$(cat 2>/dev/null)"
24
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
25
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
26
+
27
+ rt_hook_read
28
+ input="$RT_HOOK_INPUT"
25
29
  [ -z "$input" ] && exit 0
26
30
 
27
31
  command -v jq >/dev/null 2>&1 || exit 0
@@ -20,7 +20,11 @@
20
20
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: при любой ошибке, нехватке `jq`, отсутствии записи хода и повторном
21
21
  # заходе ход РАЗРЕШАЕТСЯ (exit 0). Сломанный гард не имеет права заклинить разговор.
22
22
 
23
- input="$(cat 2>/dev/null)"
23
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
24
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
25
+
26
+ rt_hook_read
27
+ input="$RT_HOOK_INPUT"
24
28
  [ -z "$input" ] && exit 0
25
29
 
26
30
  command -v jq >/dev/null 2>&1 || exit 0
@@ -12,18 +12,22 @@
12
12
  #
13
13
  # FAIL-OPEN: нет узла, нет проверки, чужой инструмент, не `.md` → пропуск.
14
14
 
15
- input="$(cat 2>/dev/null)"
15
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
16
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
17
+
18
+ rt_hook_read
19
+ input="$RT_HOOK_INPUT"
16
20
  [ -z "$input" ] && exit 0
17
21
  command -v jq >/dev/null 2>&1 || exit 0
18
22
  command -v node >/dev/null 2>&1 || exit 0
19
23
 
20
- tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
24
+ tool="$(rt_hook_tool)"
21
25
  case "$tool" in
22
26
  Edit | Write | MultiEdit) ;;
23
27
  *) exit 0 ;;
24
28
  esac
25
29
 
26
- path="$(printf '%s' "$input" | jq -r '.tool_input.file_path // empty' 2>/dev/null)"
30
+ path="$(rt_hook_file)"
27
31
  case "$path" in
28
32
  *.md) ;;
29
33
  *) exit 0 ;;
@@ -25,12 +25,16 @@
25
25
  #
26
26
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: нет разборщика, битый ввод, чужой инструмент — пропуск.
27
27
 
28
- input="$(cat 2>/dev/null)"
28
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
29
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
30
+
31
+ rt_hook_read
32
+ input="$RT_HOOK_INPUT"
29
33
  [ -z "$input" ] && exit 0
30
34
  command -v jq >/dev/null 2>&1 || exit 0
31
35
  command -v perl >/dev/null 2>&1 || exit 0
32
36
 
33
- tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
37
+ tool="$(rt_hook_tool)"
34
38
  case "$tool" in
35
39
  # Инструмент среды заводит файл теми же двумя данными, только называет их иначе — без этой
36
40
  # ветки разметка заводилась мимо всех проверок.
@@ -20,18 +20,22 @@
20
20
  # похож на перезапуск — пропуск. Гард без номера судить не берётся: перезапуск последнего
21
21
  # упавшего прогона зовут и без него, а угадывать, о каком задании речь, значит отбивать наугад.
22
22
 
23
- input="$(cat 2>/dev/null)"
23
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
24
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
25
+
26
+ rt_hook_read
27
+ input="$RT_HOOK_INPUT"
24
28
  [ -z "$input" ] && exit 0
25
29
  command -v jq >/dev/null 2>&1 || exit 0
26
30
 
27
- tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
31
+ tool="$(rt_hook_tool)"
28
32
  case "$tool" in
29
33
  # Терминал среды и универсальный исполнитель кладут команду в то же поле.
30
34
  Bash | mcp__webstorm__execute_terminal_command | mcp__webstorm__execute_tool) ;;
31
35
  *) exit 0 ;;
32
36
  esac
33
37
 
34
- cmd="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null)"
38
+ cmd="$(rt_hook_cmd)"
35
39
  [ -z "$cmd" ] && exit 0
36
40
 
37
41
  # Профиль дерева: сперва умолчание пакета, поверх него — надстройка проекта, если она есть.
@@ -32,12 +32,16 @@
32
32
  #
33
33
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: нет разборщика, битый ввод, чужой инструмент — пропуск.
34
34
 
35
- input="$(cat 2>/dev/null)"
35
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
36
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
37
+
38
+ rt_hook_read
39
+ input="$RT_HOOK_INPUT"
36
40
  [ -z "$input" ] && exit 0
37
41
  command -v jq >/dev/null 2>&1 || exit 0
38
42
  command -v perl >/dev/null 2>&1 || exit 0
39
43
 
40
- tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
44
+ tool="$(rt_hook_tool)"
41
45
  shell_cmd=""
42
46
  case "$tool" in
43
47
  # Инструмент среды заводит файл теми же двумя данными, только называет их иначе — без этой
@@ -52,7 +56,7 @@ case "$tool" in
52
56
  # имён гард стоял бы объявленным на них и молча пропускал — состояние хуже необъявленного,
53
57
  # потому что снаружи выглядит закрытым.
54
58
  Bash | mcp__webstorm__execute_terminal_command | mcp__webstorm__execute_tool)
55
- shell_cmd="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null)"
59
+ shell_cmd="$(rt_hook_cmd)"
56
60
  [ -z "$shell_cmd" ] && exit 0
57
61
  # Универсальный исполнитель прячет настоящую команду во вложенной строке: без её разбора
58
62
  # путь стоит за кавычкой, и до него не дотягивается ни один образец.
@@ -19,6 +19,8 @@
19
19
  # Выключена ли роль. Успех — выключена.
20
20
  #
21
21
  # rt_role_off strict-teacher && exit 0
22
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
23
+
22
24
  rt_role_off() {
23
25
  local role="$1" config
24
26
  [ -n "$role" ] || return 1
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env bash
2
+ # Разбор признака применимости у статьи правила. Помощник: своего события не объявляет, его
3
+ # подключает тот, кому нужен текст статьи, — гейт правил в своём отказе.
4
+ #
5
+ # Правило весит от двадцати до шестидесяти килобайт, а под конкретную правку подпадает одна его
6
+ # статья. Отказ, зовущий правило целиком, платит за решение полной ценой правила — и учит не
7
+ # тому: дешевле не читать лишнего, то есть работать хуже разведанным.
8
+ #
9
+ # Признак стоит при самой статье: карта гейта знает путь и правило, но не знает, какая из двух
10
+ # десятков статей про этот путь. Статья знает о себе всё.
11
+ #
12
+ # - **Заголовок статьи.** Текст статьи, как обычно.
13
+ # <!-- rt-when: *.scss *.css -->
14
+ #
15
+ # Комментарий не виден в собранной разметке и не мешает читать статью; образцы разделены
16
+ # пробелом и сверяются с путём правки самой оболочкой, а не поиском по словам: поиск называет не
17
+ # ту статью и молчит об этом.
18
+ #
19
+ # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: статья без признака законна, правило без единого признака — тоже. Не
20
+ # нашлось ни одной подошедшей статьи — печатается пусто, и зовущий остаётся при прежнем отказе.
21
+
22
+ # Статьи правила, чей признак совпал с путём правки. Доводы: файл правила, путь правки.
23
+ # Печатает текст подошедших статей целиком, по статье, разделяя их пустой строкой.
24
+ rt_rule_articles() {
25
+ rule_file="$1"
26
+ edited="$2"
27
+ [ -f "$rule_file" ] || return 0
28
+ [ -n "$edited" ] || return 0
29
+
30
+ # Статьи выбираются в два прохода: сперва номера строк, где стоят признаки, и сами образцы,
31
+ # затем — текст той статьи, чей образец совпал. Сопоставление делает оболочка: `case` знает
32
+ # правила путей, а разборщик текста — нет.
33
+ while IFS=' ' read -r line patterns; do
34
+ [ -n "$line" ] || continue
35
+ hit=''
36
+ # Образцы разбираются под выключенным раскрытием имён: `*.scss` в цикле развернулось бы
37
+ # в имена файлов рабочего каталога, и статья выбиралась бы по тому, где стоял вызов.
38
+ # Разделитель задаётся здесь же — внешний мог быть переопределён тем, кто нас позвал.
39
+ set -f
40
+ old_ifs="$IFS"
41
+ IFS=' '
42
+ # shellcheck disable=SC2086
43
+ set -- $patterns
44
+ IFS="$old_ifs"
45
+ set +f
46
+ for pattern in "$@"; do
47
+ [ -n "$pattern" ] || continue
48
+ # shellcheck disable=SC2254
49
+ case "$edited" in
50
+ $pattern) hit=1; break ;;
51
+ esac
52
+ # Образец без каталога сверяется и с именем файла: статья говорит «про такие файлы»,
53
+ # а правка приходит полным путём.
54
+ # shellcheck disable=SC2254
55
+ case "${edited##*/}" in
56
+ $pattern) hit=1; break ;;
57
+ esac
58
+ done
59
+ [ -n "$hit" ] || continue
60
+ rt_rule_article_at "$rule_file" "$line"
61
+ done <<EOF
62
+ $(rt_rule_article_marks "$rule_file")
63
+ EOF
64
+ }
65
+
66
+ # Признаки правила: строка «номер строки — образцы». Отдельной функцией, потому что её же зовёт
67
+ # сверка полноты текстов: правило, у которого признак записан не той формой, тихо остаётся без
68
+ # статьи в отказе, и заметить это нечем.
69
+ rt_rule_article_marks() {
70
+ [ -f "$1" ] || return 0
71
+ grep -n 'rt-when:' "$1" 2>/dev/null \
72
+ | sed -e 's/^\([0-9]*\):.*rt-when:[[:space:]]*/\1 /' -e 's/[[:space:]]*-->.*$//'
73
+ }
74
+
75
+ # Текст статьи, внутри которой стоит строка с признаком. Статья начинается ближайшим сверху
76
+ # пунктом списка верхнего уровня и кончается перед следующим таким пунктом или перед строкой без
77
+ # отступа: продолжение статьи всегда идёт с отступом.
78
+ rt_rule_article_at() {
79
+ awk -v mark="$2" '
80
+ NR <= mark && /^- / { start = NR }
81
+ { line[NR] = $0 }
82
+ END {
83
+ if (!start) { exit }
84
+ for (i = start + 1; i <= NR; i++) {
85
+ if (line[i] ~ /^- / || (line[i] !~ /^[ \t]/ && line[i] != "")) { break }
86
+ stop = i
87
+ }
88
+ if (!stop) { stop = start }
89
+ last = start
90
+ for (i = start; i <= stop; i++) {
91
+ if (line[i] ~ /rt-when:/) { continue }
92
+ if (line[i] == "" && i == stop) { continue }
93
+ print line[i]
94
+ last = i
95
+ }
96
+ print ""
97
+ }
98
+ ' "$1" 2>/dev/null
99
+ }
@@ -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,7 +12,11 @@
12
12
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: любая ошибка пропускает. Хук удаляет временный файл и ничего не
13
13
  # отбивает.
14
14
 
15
- input="$(cat 2>/dev/null)"
15
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
16
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
17
+
18
+ rt_hook_read
19
+ input="$RT_HOOK_INPUT"
16
20
  [ -z "$input" ] && exit 0
17
21
 
18
22
  sid="$(printf '%s' "$input" | jq -r '.session_id // empty' 2>/dev/null)"
@@ -23,11 +23,15 @@
23
23
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: любая ошибка и любой неопознанный путь пропускают правку (exit 0).
24
24
  # Сломанный гейт не имеет права остановить работу совсем.
25
25
 
26
- input="$(cat 2>/dev/null)"
26
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
27
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
28
+
29
+ rt_hook_read
30
+ input="$RT_HOOK_INPUT"
27
31
  [ -z "$input" ] && exit 0
28
32
 
29
33
  sid="$(printf '%s' "$input" | jq -r '.session_id // "nosession"' 2>/dev/null)"
30
- tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
34
+ tool="$(rt_hook_tool)"
31
35
 
32
36
  # Умолчание карты ищется и рядом с самим хуком: уезжают они вместе.
33
37
  rt_hooks_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@@ -173,6 +177,25 @@ if [ -n "$law" ]; then
173
177
  && reason="Отбито гейтом правил: загрузи правило «${req}» инструментом Skill — оно применяет закон ${law_path} к этому дереву — и повтори действие. ${fallback} Для этой области это происходит один раз за сессию."
174
178
  fi
175
179
 
180
+ # Статья, под которую подпадает эта правка. Правило весит от двадцати до шестидесяти килобайт,
181
+ # и отказ, зовущий его целиком, платит за решение полной ценой правила. Статья говорит о своей
182
+ # применимости сама — разбирает её помощник рядом; нет помощника или нет размеченных статей,
183
+ # отказ остаётся прежним, и это тот же отказ в пользу работы, что и везде.
184
+ #
185
+ # Загрузку правила статья не отменяет: она снимает чтение правила целиком, а не сам отказ.
186
+ # Правило целиком остаётся вторым ходом — для того, кому статьи мало.
187
+ # shellcheck disable=SC1090
188
+ [ -f "$rt_hooks_dir/rule-article.sh" ] && . "$rt_hooks_dir/rule-article.sh" 2>/dev/null
189
+ if [ "$kind" != "command" ] && [ "$kind" != "browser" ] && command -v rt_rule_articles >/dev/null 2>&1; then
190
+ article="$(rt_rule_articles "$root/$rules_dir/${req}/SKILL.md" "$target" 2>/dev/null)"
191
+ if [ -n "$article" ]; then
192
+ reason="Отбито гейтом правил. Под эту правку подпадает статья правила «${req}»:
193
+
194
+ ${article}
195
+ Статья снимает чтение правила целиком, а не отказ: загрузи правило «${req}» инструментом Skill и повтори действие. ${fallback} Для этой области это происходит один раз за сессию."
196
+ fi
197
+ fi
198
+
176
199
  # Общий хвост отказа: два законных хода и законная форма обхода, если она у отказа есть.
177
200
  # Файл может быть не разложен — тогда хвоста нет, а причина отказа остаётся прежней.
178
201
  # shellcheck disable=SC1090
@@ -7,7 +7,11 @@
7
7
  #
8
8
  # Хук только наблюдает: он всегда пропускает и ничего не отбивает.
9
9
 
10
- input="$(cat 2>/dev/null)"
10
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
11
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
12
+
13
+ rt_hook_read
14
+ input="$RT_HOOK_INPUT"
11
15
  [ -z "$input" ] && exit 0
12
16
 
13
17
  sid="$(printf '%s' "$input" | jq -r '.session_id // "nosession"' 2>/dev/null)"