@rt-tools/agent-kit 0.5.0 → 0.5.2

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 (86) hide show
  1. package/README.md +73 -0
  2. package/assets/checks/check-doc-paths.mjs +200 -30
  3. package/assets/checks/check-specs.mjs +42 -7
  4. package/assets/checks/rt-kit-checks.config.mjs +12 -0
  5. package/assets/commands/agent-kit-digest.md +83 -0
  6. package/assets/commands/next-session.md +122 -0
  7. package/assets/commands/skill-curator.md +33 -1
  8. package/assets/defaults/gate-map.sh +23 -1
  9. package/assets/defaults/project.sh +37 -1
  10. package/assets/docs/GLOSSARY.md +77 -0
  11. package/assets/hooks/docs-guard.sh +18 -1
  12. package/assets/hooks/git-guard-delivery.sh +30 -3
  13. package/assets/hooks/git-guard-main.sh +8 -0
  14. package/assets/hooks/git-guard-push-tests.sh +8 -1
  15. package/assets/hooks/grill-gate.sh +38 -16
  16. package/assets/hooks/lint-after-edit.sh +10 -3
  17. package/assets/hooks/observe.sh +90 -0
  18. package/assets/hooks/postmortem-guard.sh +92 -0
  19. package/assets/hooks/profile-check.sh +43 -0
  20. package/assets/hooks/qa-dataid-guard.sh +9 -2
  21. package/assets/hooks/reuse-first-guard.sh +9 -2
  22. package/assets/hooks/skill-gate.sh +15 -0
  23. package/assets/hooks/skill-loaded.sh +7 -0
  24. package/assets/hooks/task-context-load.sh +16 -2
  25. package/assets/hooks/task-flow-guard.sh +9 -2
  26. package/assets/hooks/window-fill-guard.sh +157 -0
  27. package/assets/laws/code-structure.md +10 -0
  28. package/assets/laws/delivery.md +8 -1
  29. package/assets/laws/project-documentation.md +9 -0
  30. package/assets/laws/verifiability.md +9 -0
  31. package/assets/laws/work-conduct.md +47 -0
  32. package/assets/patterns/git-workflow-commit.azure.md +16 -12
  33. package/assets/patterns/git-workflow-commit.github.md +16 -12
  34. package/assets/patterns/git-workflow-commit.gitlab.md +16 -12
  35. package/assets/patterns/spec-driven-domain.md +19 -0
  36. package/assets/patterns/task-flow-close.md +4 -4
  37. package/assets/patterns/task-flow-handoff.md +115 -0
  38. package/assets/patterns/task-flow-resume.md +14 -2
  39. package/assets/patterns/task-flow-start.md +40 -2
  40. package/assets/rules/doc-style.md +39 -1
  41. package/assets/rules/git-workflow.azure.md +39 -0
  42. package/assets/rules/git-workflow.github.md +38 -0
  43. package/assets/rules/git-workflow.gitlab.md +38 -0
  44. package/assets/rules/spec-driven.md +14 -0
  45. package/assets/rules/task-flow.md +70 -3
  46. package/assets/skills/agent-kit.md +32 -0
  47. package/assets/templates/postmortem.md +32 -0
  48. package/assets/templates/proposal.md +39 -0
  49. package/bin/agent-kit.d.ts.map +1 -1
  50. package/bin/agent-kit.js +50 -1
  51. package/bin/agent-kit.js.map +1 -1
  52. package/lib/catalog.d.ts +33 -0
  53. package/lib/catalog.d.ts.map +1 -1
  54. package/lib/catalog.js +55 -1
  55. package/lib/catalog.js.map +1 -1
  56. package/lib/commands.d.ts +41 -0
  57. package/lib/commands.d.ts.map +1 -1
  58. package/lib/commands.js +315 -3
  59. package/lib/commands.js.map +1 -1
  60. package/lib/config.d.ts +11 -1
  61. package/lib/config.d.ts.map +1 -1
  62. package/lib/config.js +6 -0
  63. package/lib/config.js.map +1 -1
  64. package/lib/hooks-map.d.ts +15 -3
  65. package/lib/hooks-map.d.ts.map +1 -1
  66. package/lib/hooks-map.js +47 -11
  67. package/lib/hooks-map.js.map +1 -1
  68. package/lib/observations.d.ts +72 -0
  69. package/lib/observations.d.ts.map +1 -0
  70. package/lib/observations.js +126 -0
  71. package/lib/observations.js.map +1 -0
  72. package/lib/proposals.d.ts +48 -0
  73. package/lib/proposals.d.ts.map +1 -0
  74. package/lib/proposals.js +111 -0
  75. package/lib/proposals.js.map +1 -0
  76. package/lib/submit.d.ts +24 -0
  77. package/lib/submit.d.ts.map +1 -0
  78. package/lib/submit.js +26 -0
  79. package/lib/submit.js.map +1 -0
  80. package/lib/sync.d.ts +9 -1
  81. package/lib/sync.d.ts.map +1 -1
  82. package/lib/sync.js +4 -6
  83. package/lib/sync.js.map +1 -1
  84. package/package.json +1 -1
  85. package/rt-tools-agent-kit-0.5.2.tgz +0 -0
  86. package/rt-tools-agent-kit-0.5.0.tgz +0 -0
@@ -1,15 +1,18 @@
1
1
  #!/usr/bin/env bash
2
+ # rt-hook: PreToolUse AskUserQuestion
2
3
  # rt-hook: Stop
3
- # Гард разговора: ход, в котором владельцу задан вопрос, не заканчивается, пока за этот же ход
4
- # не читались законы и правила. Stop.
4
+ # Гард разговора: вопрос владельцу не задаётся, пока за этот же ход не читались законы и
5
+ # правила. Стоит на двух событиях, и это не дублирование.
5
6
  #
6
- # Зачем именно так. Требование «правила читаются до разговора» записано в правиле ведения
7
- # работы, а исполнения у него не было: все прочие гарды судят правку файла или команду, а
8
- # вопрос в чат ни тем, ни другим не является. Поймать его можно только на завершении хода —
9
- # событие получает путь к записи хода и видит его целиком.
7
+ # Зачем именно так. Требование «правила читаются до разговора» исполнимо ровно до отправки
8
+ # вопроса. Проверка на завершении хода отбивает задним числом: к моменту отказа вопрос уже у
9
+ # владельца, и владелец видит его вместе с отбитым ходом требование срабатывает, но работу не
10
+ # спасает. Поэтому ход с вопросом судится на инструменте вопроса, до отправки.
10
11
  #
11
- # Перехват инструмента меню вариантов эту дыру не закрывает: вопрос чаще задаётся прозой, и
12
- # ровно так был задан тот, из-за которого гард заведён.
12
+ # Одним этим перехватом дыра не закрывается: вопрос чаще задаётся прозой, и ровно так был задан
13
+ # тот, из-за которого гард заведён. Прозаический вопрос инструментом не является, и поймать его
14
+ # можно только на завершении хода — событие получает путь к записи хода и видит его целиком.
15
+ # Отсюда два события: меню ловится до отправки, проза — после.
13
16
  #
14
17
  # Чтением правил считается любой из трёх путей: загрузка правила, чтение файла законов или
15
18
  # правил, поиск по ним. Требовать именно загрузку значило бы гнать на неё там, где хватило
@@ -23,10 +26,13 @@ input="$(cat 2>/dev/null)"
23
26
 
24
27
  command -v jq >/dev/null 2>&1 || exit 0
25
28
 
29
+ # Какое событие пришло. У вызова инструмента есть его имя, у завершения хода — нет.
30
+ tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
31
+
26
32
  # Повторный заход по тому же ходу не судится: иначе ход не кончится никогда — гард сказал своё
27
- # один раз и отпускает.
33
+ # один раз и отпускает. К вызову инструмента это не относится: там судится сам вызов.
28
34
  active="$(printf '%s' "$input" | jq -r '.stop_hook_active // false' 2>/dev/null)"
29
- [ "$active" = "true" ] && exit 0
35
+ [ -z "$tool" ] && [ "$active" = "true" ] && exit 0
30
36
 
31
37
  transcript="$(printf '%s' "$input" | jq -r '.transcript_path // empty' 2>/dev/null)"
32
38
  [ -z "$transcript" ] && exit 0
@@ -61,7 +67,10 @@ read_re="$(printf '%s' "$laws_dir|$rules_dir|$specs_dir" | sed 's/^|*//; s/|*$//
61
67
  # бы.
62
68
  #
63
69
  # Хвост в 400 строк: запись хода растёт всю сессию, а судится только последний ход.
64
- verdict="$(tail -n 400 "$transcript" 2>/dev/null | jq -s -r --arg re "$read_re" '
70
+ # На событии вызова инструмента вопрос уже известен он и есть вызов; судится только то,
71
+ # читались ли за этот ход правила. На завершении хода вопрос ищется в тексте реплик: меню к
72
+ # этому моменту уже отбито раньше.
73
+ verdict="$(tail -n 400 "$transcript" 2>/dev/null | jq -s -r --arg re "$read_re" --arg tool "$tool" '
65
74
  def is_input:
66
75
  .type == "user"
67
76
  and (((.message.content // []) | if type == "array"
@@ -78,19 +87,32 @@ verdict="$(tail -n 400 "$transcript" 2>/dev/null | jq -s -r --arg re "$read_re"
78
87
  or ((.name == "Bash") and ((.input.command // "") | test($re)))
79
88
  ) | any) as $read
80
89
  | (($texts | join("\n")) | test("\\?[[:space:]]*$"; "m")) as $asked_prose
81
- | ($uses | map(.name == "AskUserQuestion") | any) as $asked_menu
82
- | if ($asked_prose or $asked_menu) and ($read | not) then "ask" else "pass" end
90
+ | ($tool != "") as $asking_now
91
+ | if ($asked_prose or $asking_now) and ($read | not) then "ask" else "pass" end
83
92
  ' 2>/dev/null)"
84
93
 
85
94
  [ "$verdict" = "ask" ] || exit 0
86
95
 
87
- reason="BLOCKED by grill-gate: в ответе есть вопрос владельцу, а законы и правила за этот ход не читались. Вопрос, ответ на который уже записан, владельцу не задаётся — правило ведения работы. Прогони поиск по словам темы и ответь по найденному; спрашивай только то, что документацией не покрыто:
96
+ if [ -n "$tool" ]; then
97
+ head="BLOCKED by grill-gate: вопрос владельцу ещё не ушёл, и это единственный момент, когда требование исполнимо."
98
+ else
99
+ head="BLOCKED by grill-gate: в ответе есть вопрос владельцу, а законы и правила за этот ход не читались."
100
+ fi
101
+
102
+ reason="$head Вопрос, ответ на который уже записан, владельцу не задаётся — правило ведения работы. Прогони поиск по словам темы и ответь по найденному; спрашивай только то, что документацией не покрыто:
88
103
 
89
104
  grep -rn -i \"<слово темы>\" $laws_dir $rules_dir $specs_dir
90
105
 
91
106
  Гард судит один ход: следующий заход не отбивается."
92
107
 
93
- jq -n --arg r "$reason" '{decision:"block",reason:$r}' 2>/dev/null \
94
- || printf '{"decision":"block","reason":"grill-gate: прочитай законы и правила по теме, прежде чем спрашивать владельца."}\n'
108
+ # Форма отказа у двух событий разная: вызов инструмента отбивается решением о доступе, а
109
+ # завершение хода решением о ходе. Одна форма на оба события молча не срабатывает.
110
+ if [ -n "$tool" ]; then
111
+ jq -n --arg r "$reason" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$r}}' 2>/dev/null \
112
+ || printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"grill-gate: прочитай законы и правила по теме, прежде чем спрашивать владельца."}}\n'
113
+ else
114
+ jq -n --arg r "$reason" '{decision:"block",reason:$r}' 2>/dev/null \
115
+ || printf '{"decision":"block","reason":"grill-gate: прочитай законы и правила по теме, прежде чем спрашивать владельца."}\n'
116
+ fi
95
117
 
96
118
  exit 0
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env bash
2
2
  # rt-hook: PostToolUse Edit|Write|MultiEdit|Bash|mcp__webstorm__create_new_file
3
+ # Требует: hooks/profile-check.sh
3
4
  # Линтер по следам правки. PostToolUse.
4
5
  #
5
6
  # Два входа. Правка файла — линтуется один изменённый файл. Перенос файла командой — линтуются
@@ -62,7 +63,13 @@ for profile in "$rt_hooks_dir/../rt-kit/defaults/project.sh" "$rt_hooks_dir/../d
62
63
  # shellcheck disable=SC1090
63
64
  [ -f "$profile" ] && . "$profile" 2>/dev/null
64
65
  done
65
- command -v rt_lint_for >/dev/null 2>&1 || exit 0
66
+
67
+ # Слово о нехватке функции профиля: хук, вышедший молча, неотличим от работающего. Файл может
68
+ # быть не разложен — тогда остаётся прежнее поведение, молчаливое.
69
+ # shellcheck disable=SC1090
70
+ [ -f "$rt_hooks_dir/profile-check.sh" ] && . "$rt_hooks_dir/profile-check.sh"
71
+ command -v rt_needs >/dev/null 2>&1 || rt_needs() { command -v "$1" >/dev/null 2>&1; }
72
+ rt_needs rt_lint_for lint-after-edit || exit 0
66
73
 
67
74
  # --- какие файлы проверяем -------------------------------------------------------------
68
75
 
@@ -133,7 +140,7 @@ fi
133
140
  # --- отсев того, что линтерами дерева не покрыто ----------------------------------------
134
141
 
135
142
  lintable() {
136
- if command -v rt_is_app_code >/dev/null 2>&1; then
143
+ if rt_needs rt_is_app_code lint-after-edit; then
137
144
  rt_is_app_code "$1" || return 1
138
145
  fi
139
146
  if [ -n "${RT_LINT_SKIP_RE:-}" ] && printf '%s' "$1" | grep -qE "$RT_LINT_SKIP_RE"; then
@@ -156,7 +163,7 @@ linter_name() {
156
163
 
157
164
  # --- прогон -----------------------------------------------------------------------------
158
165
 
159
- push_checks="$(command -v rt_push_checks >/dev/null 2>&1 && rt_push_checks 2>/dev/null)"
166
+ push_checks="$(rt_needs rt_push_checks lint-after-edit && rt_push_checks 2>/dev/null)"
160
167
 
161
168
  report=""
162
169
  linters=""
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env bash
2
+ # Запись наблюдений о слое правил. НЕ гард: объявления `rt-hook:` у него нет, к событиям агента
3
+ # он не подключается. Его источают гарды — тем же приёмом, каким гейт источает карту.
4
+ #
5
+ # Зачем он есть. Правки в тексты пакета шли из головы того, кто их пишет: чем из разложенного
6
+ # пользуются каждый день, чем не пользовались ни разу и обо что спотыкаются по второму кругу,
7
+ # пакету было неизвестно. Часть данных при этом уже собиралась и выбрасывалась — запись загрузок
8
+ # жила во временном каталоге и гибла со сжатием контекста.
9
+ #
10
+ # ЧТО ЗАПИСЫВАЕТСЯ. Только имена ресурсов пакета и счётчики: имя правила или гарда, род события,
11
+ # род файла, версия пакета и признак сессии. Ни путей дерева, ни имён его доменов, ни имени
12
+ # самого дерева — и держится это не памятью зовущего, а `rt_observe_clean`: значение со слэшем
13
+ # не пишется вовсе. Наблюдение уезжает потом в чужой репозиторий, и запрет называть чужое дерево
14
+ # обязан держаться конструкцией.
15
+ #
16
+ # ОТКАЗ В ПОЛЬЗУ РАБОТЫ. Наблюдение — побочная работа гарда, и любая её поломка молча пропускает
17
+ # действие: недоступный каталог, отсутствие `date`, полный диск. Гард, упавший на записи
18
+ # наблюдения, останавливал бы работу ради статистики.
19
+
20
+ # Значение, годное к записи. Путь не выносится ни при каких обстоятельствах, остальное чистится
21
+ # до имени и обрезается: длинное значение в наблюдении не значит ничего, кроме утечки.
22
+ rt_observe_clean() {
23
+ case "$1" in
24
+ */*) return 0 ;;
25
+ esac
26
+ printf '%s' "$1" | LC_ALL=C tr -cd 'A-Za-z0-9._:-' | cut -c1-48
27
+ }
28
+
29
+ # Признак сессии: считается по её имени и обратно не восстанавливается. Нужен, чтобы сводка
30
+ # знала число заходов, а не только число событий.
31
+ rt_observe_session() {
32
+ printf '%s' "$1" | cksum 2>/dev/null | cut -d' ' -f1
33
+ }
34
+
35
+ # Версия пакета, разложившего этот файл. Стоит в шапке, которую ставит раскладка; в исходниках
36
+ # пакета шапки нет, и там версия — `dev`.
37
+ rt_observe_version() {
38
+ local found
39
+ found="$(sed -n 's/^# rt-kit v\([^ ]*\) .*/\1/p' "${BASH_SOURCE[0]}" 2>/dev/null | head -1)"
40
+ printf '%s' "${found:-dev}"
41
+ }
42
+
43
+ # Каталог наблюдений этого дерева. Пустая строка — записи не будет.
44
+ rt_observe_dir() {
45
+ local root="${CLAUDE_PROJECT_DIR:-.}" config
46
+ config="$root/.claude/rt-kit.json"
47
+ # Выключатель дерева гасит запись целиком, а не частями: пакет стоит и у тех, о ком мы не
48
+ # знаем. Нечитаемая настройка выключателем не считается — иначе сломанный JSON тихо
49
+ # выключал бы наблюдения, и понять это было бы нечем.
50
+ if [ -f "$config" ] && command -v jq >/dev/null 2>&1; then
51
+ [ "$(jq -r 'if .observe == false then "off" else "on" end' "$config" 2>/dev/null)" = "off" ] && return 0
52
+ fi
53
+ printf '%s' "$root/.claude/rt-kit/observations"
54
+ }
55
+
56
+ # Одно наблюдение: род события и пары `ключ=значение`. Ключ `sid` хешируется, остальные
57
+ # чистятся. Пустое значение поля не заводит.
58
+ #
59
+ # rt_note gate-deny res=styling-bem kind=scss sid="$sid"
60
+ rt_note() {
61
+ local kind="$1" dir day stamp line pair key value
62
+ [ -n "$kind" ] || return 0
63
+ shift
64
+
65
+ dir="$(rt_observe_dir)"
66
+ [ -n "$dir" ] || return 0
67
+ mkdir -p "$dir" 2>/dev/null || return 0
68
+
69
+ day="$(date -u +%Y-%m-%d 2>/dev/null)" || return 0
70
+ stamp="$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null)" || return 0
71
+ [ -n "$day" ] || return 0
72
+
73
+ line="{\"t\":\"$stamp\",\"ev\":\"$(rt_observe_clean "$kind")\""
74
+ for pair in "$@"; do
75
+ key="$(rt_observe_clean "${pair%%=*}")"
76
+ value="${pair#*=}"
77
+ [ -n "$key" ] || continue
78
+ if [ "$key" = "sid" ]; then
79
+ value="$(rt_observe_session "$value")"
80
+ else
81
+ value="$(rt_observe_clean "$value")"
82
+ fi
83
+ [ -n "$value" ] || continue
84
+ line="$line,\"$key\":\"$value\""
85
+ done
86
+ line="$line,\"v\":\"$(rt_observe_version)\"}"
87
+
88
+ printf '%s\n' "$line" >> "$dir/$day.jsonl" 2>/dev/null || return 0
89
+ return 0
90
+ }
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env bash
2
+ # rt-hook: Stop
3
+ # Гард происшествия: ход, в котором исполнитель признал промах, не заканчивается, пока записи о
4
+ # происшествии нет. Stop.
5
+ #
6
+ # Зачем именно так. Происшествие — заход, в котором исполнитель сделал не то, а слой правил
7
+ # этого не отбил, — исправляемого кода за собой не оставляет. Такой заход кончается извинением
8
+ # в переписке: назавтра механизм промаха пересказывается уже приглаженно, остаются выводы, а из
9
+ # выводов правило не выводится. Разбор случался только тогда, когда владелец требовал его вслух.
10
+ #
11
+ # Ловится признание образцами, а не пониманием смысла: оценку «это был промах» назначал бы тот,
12
+ # кому она мешает, и порог плыл бы. Набор образцов виден, пополняется правкой и промахивается
13
+ # заметно — заход, признавший промах словами вне набора, гард пропускает, и это сказано вслух в
14
+ # договорённости, а не считается закрытым.
15
+ #
16
+ # Ловится на завершении хода, а не на отправке реплики: к моменту признания промах уже случился,
17
+ # и ловить раньше нечего. Этим он отличается от гарда разговора, у которого есть свой инструмент
18
+ # — вопрос владельцу.
19
+ #
20
+ # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: при любой ошибке, отсутствии записи хода и повторном заходе ход
21
+ # РАЗРЕШАЕТСЯ (exit 0). Сломанный гард не имеет права заклинить разговор.
22
+
23
+ input="$(cat 2>/dev/null)"
24
+ [ -z "$input" ] && exit 0
25
+
26
+ command -v jq >/dev/null 2>&1 || exit 0
27
+
28
+ # Повторный заход по тому же ходу не судится: гард сказал своё один раз и отпускает.
29
+ active="$(printf '%s' "$input" | jq -r '.stop_hook_active // false' 2>/dev/null)"
30
+ [ "$active" = "true" ] && exit 0
31
+
32
+ transcript="$(printf '%s' "$input" | jq -r '.transcript_path // empty' 2>/dev/null)"
33
+ [ -z "$transcript" ] && exit 0
34
+ [ -f "$transcript" ] || exit 0
35
+
36
+ root="${CLAUDE_PROJECT_DIR:-.}"
37
+
38
+ # Каталог записей у дерева свой. Заданный пустым — отказ дерева от требования: дереву, которое
39
+ # записей не ведёт, гард не навязывается.
40
+ notes_dir="${RT_POSTMORTEMS_DIR-docs/postmortems}"
41
+ [ -z "$notes_dir" ] && exit 0
42
+ [ -d "$root/$notes_dir" ] || exit 0
43
+
44
+ # Образцы признания промаха. Набор открыт и пополняется правкой: полнота его — открытый вопрос
45
+ # договорённости, а не обещание.
46
+ admitted_re='был неправ|был не прав|ошибс|моя ошибк|мой промах|промахнул|проглядел|не проверил|соврал|виноват|извин|прошу прощения|неверно утверждал|утверждение было ложн|принял на веру'
47
+
48
+ # Ход — это всё, что записано после последнего настоящего ввода владельца. Ответ инструмента
49
+ # приходит той же ролью, поэтому строки с `tool_result` вводом не считаются.
50
+ #
51
+ # Хвост в 400 строк: запись хода растёт всю сессию, а судится только последний ход.
52
+ verdict="$(tail -n 400 "$transcript" 2>/dev/null | jq -s -r --arg re "$admitted_re" '
53
+ def is_input:
54
+ .type == "user"
55
+ and (((.message.content // []) | if type == "array"
56
+ then ([.[] | select(.type == "tool_result")] | length)
57
+ else 0 end) == 0);
58
+
59
+ (map(is_input) | rindex(true)) as $i
60
+ | (if $i == null then . else .[$i + 1:] end) as $turn
61
+ | [$turn[] | select(.type == "assistant") | (.message.content // [])[] | select(.type == "text") | .text] as $texts
62
+ | [$turn[] | select(.type == "assistant") | (.message.content // [])[] | select(.type == "tool_use")] as $uses
63
+ # Признак нечувствителен к регистру флагом, а не приведением: приведение знает только
64
+ # латиницу, и «Был неправ» с большой буквы проходило бы мимо набора образцов молча.
65
+ | (($texts | join("\n")) | test($re; "i")) as $admitted
66
+ | ($uses | map(
67
+ ((.name // "") | test("^(Write|Edit|MultiEdit)$"))
68
+ and ((.input.file_path // "") | test("postmortem"))
69
+ ) | any) as $wrote
70
+ | if $admitted and ($wrote | not) then "admit" else "pass" end
71
+ ' 2>/dev/null)"
72
+
73
+ [ "$verdict" = "admit" ] || exit 0
74
+
75
+ # Запись, сделанная за эту сессию, снимает требование и без правки в этом же ходе: разбор мог
76
+ # лечь файлом ходом раньше — тем, в котором промах и признали.
77
+ if [ -n "$(find "$root/$notes_dir" -name '*.md' -newermt '-1 day' 2>/dev/null | head -1)" ]; then
78
+ exit 0
79
+ fi
80
+
81
+ reason="BLOCKED by postmortem-guard: в ответе признан промах, а записи о происшествии в \`$notes_dir/\` за сегодня нет. Происшествие — заход, в котором исполнитель сделал не то, а слой правил этого не отбил, — записывается в тот же заход: назавтра механизм промаха пересказывается уже приглаженно, и правило из него не выводится.
82
+
83
+ Запись называет: механизм промаха по шагам, что было доступно до него, чем ловилось и что из этого ушло в слой правил. Без последней строки это жалоба, а не разбор.
84
+
85
+ $notes_dir/<год>-<месяц>-<день>-<короткое имя>.md
86
+
87
+ Гард судит один ход: следующий заход не отбивается."
88
+
89
+ jq -n --arg r "$reason" '{decision:"block",reason:$r}' 2>/dev/null \
90
+ || printf '{"decision":"block","reason":"postmortem-guard: признан промах — запиши разбор происшествия."}\n'
91
+
92
+ exit 0
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env bash
2
+ # Нехватка функции профиля, сказанная вслух. НЕ гард: объявления `rt-hook:` у него нет, к
3
+ # событиям агента он не подключается. Его источают сами гарды — тем же приёмом, каким гейт
4
+ # источает карту, а гарды — запись наблюдений.
5
+ #
6
+ # Зачем он есть. Девять хуков пакета зовут функции профиля дерева, и у пяти проверка стоит до
7
+ # всякого поведения: нет функции — выход с нулём, не сделав ничего. Такой гард хуже
8
+ # отсутствующего: он лежит в дереве, стоит в настройке, виден в списке хуков и читается как
9
+ # работающий. Тишина при этом означает разом три разных вещи — «нечего проверять», «проверять
10
+ # нечем» и «всё в порядке», — и отличить их нечем.
11
+ #
12
+ # ЧТО ГОВОРИТСЯ. Имя функции и файл, в котором её определяют. Один раз за сессию: на каждый
13
+ # вызов та же строка повторялась бы десятки раз за заход и перестала бы читаться.
14
+ #
15
+ # ДЕЙСТВИЕ ВСЁ РАВНО ПРОПУСКАЕТСЯ. Хук сообщает о своей неполноте, а не судит правку: судить её
16
+ # нечем, и отбивать работу из-за ненастроенного дерева он не вправе.
17
+
18
+ # Куда кладётся отметка «об этом уже сказано». Каталог временных файлов, а не дерево: отметка
19
+ # живёт один заход и в историю не едет.
20
+ rt_needs_mark_dir() {
21
+ printf '%s' "${TMPDIR:-/tmp}"
22
+ }
23
+
24
+ # Есть ли функция профиля. Успех — есть, и хук работает дальше. Отказ — нет, и об этом сказано.
25
+ #
26
+ # rt_needs rt_is_app_code task-flow-guard "$sid"
27
+ #
28
+ # Третий параметр — признак сессии, если хук его знает. Не знает — отметка кладётся на день:
29
+ # без признака «один раз за сессию» превратилось бы в «один раз за всю жизнь машины».
30
+ rt_needs() {
31
+ command -v "$1" >/dev/null 2>&1 && return 0
32
+
33
+ rt_needs_key="${3:-$(date +%Y%m%d 2>/dev/null || printf 'nosession')}"
34
+ rt_needs_mark="$(rt_needs_mark_dir)/rt-kit-needs-$1-$rt_needs_key"
35
+ if [ ! -f "$rt_needs_mark" ]; then
36
+ printf '%s: нет функции %s — её определяют в .claude/rt-kit/project.sh, а умолчание везёт пакет в .claude/rt-kit/defaults/project.sh\n' \
37
+ "${2:-хук}" "$1" >&2
38
+ printf 'проверка не работает, действие пропущено\n' >&2
39
+ : >"$rt_needs_mark" 2>/dev/null || true
40
+ fi
41
+
42
+ return 1
43
+ }
@@ -1,5 +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
4
  # Гард якоря для спек. PreToolUse на правке разметки.
4
5
  #
5
6
  # Спеки адресуют элементы только через этот атрибут. Классы оформления меняются вместе с
@@ -56,10 +57,16 @@ for profile in "$rt_hooks_dir/../rt-kit/defaults/project.sh" "$rt_hooks_dir/../d
56
57
  [ -f "$profile" ] && . "$profile" 2>/dev/null
57
58
  done
58
59
 
60
+ # Слово о нехватке функции профиля: хук, вышедший молча, неотличим от работающего. Файл может
61
+ # быть не разложен — тогда остаётся прежнее поведение, молчаливое.
62
+ # shellcheck disable=SC1090
63
+ [ -f "$rt_hooks_dir/profile-check.sh" ] && . "$rt_hooks_dir/profile-check.sh"
64
+ command -v rt_needs >/dev/null 2>&1 || rt_needs() { command -v "$1" >/dev/null 2>&1; }
65
+
59
66
  # Разметка приложения живёт там же, где его код. Без этой положительной проверки гард
60
67
  # распространялся на любой файл разметки на диске — черновик вне дерева отклонялся требованием
61
68
  # проставить якоря.
62
- if command -v rt_is_app_code >/dev/null 2>&1; then
69
+ if rt_needs rt_is_app_code qa-dataid-guard; then
63
70
  rt_is_app_code "$path" || exit 0
64
71
  fi
65
72
 
@@ -75,7 +82,7 @@ added="$(printf '%s' "$input" | jq -r '
75
82
  ' 2>/dev/null)"
76
83
  [ -z "$added" ] && exit 0
77
84
 
78
- decorative="$(command -v rt_qa_decorative >/dev/null 2>&1 && rt_qa_decorative 2>/dev/null)"
85
+ decorative="$(rt_needs rt_qa_decorative qa-dataid-guard && rt_qa_decorative 2>/dev/null)"
79
86
  component_re="${RT_QA_COMPONENT_RE:--}"
80
87
 
81
88
  # Открывающие теги разбираются ЦЕЛИКОМ: тег занимает несколько строк, и якорь часто стоит не в
@@ -1,5 +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
4
  # Гард «ничего не пишется с нуля». PreToolUse на правке кода и разметки.
4
5
  #
5
6
  # Линтеры знают правила, но не знают ИНВЕНТАРЬ: линтер стилей поймает сырой цвет, линтер кода —
@@ -61,11 +62,17 @@ for profile in "$rt_hooks_dir/../rt-kit/defaults/project.sh" "$rt_hooks_dir/../d
61
62
  # shellcheck disable=SC1090
62
63
  [ -f "$profile" ] && . "$profile" 2>/dev/null
63
64
  done
64
- command -v rt_reinvented_in >/dev/null 2>&1 || exit 0
65
+
66
+ # Слово о нехватке функции профиля: хук, вышедший молча, неотличим от работающего. Файл может
67
+ # быть не разложен — тогда остаётся прежнее поведение, молчаливое.
68
+ # shellcheck disable=SC1090
69
+ [ -f "$rt_hooks_dir/profile-check.sh" ] && . "$rt_hooks_dir/profile-check.sh"
70
+ command -v rt_needs >/dev/null 2>&1 || rt_needs() { command -v "$1" >/dev/null 2>&1; }
71
+ rt_needs rt_reinvented_in reuse-first-guard || exit 0
65
72
 
66
73
  # Правила этого дерева действуют на код этого дерева: без положительной проверки гард требовал
67
74
  # бы собирать готовым и в черновике за пределами дерева.
68
- if command -v rt_is_app_code >/dev/null 2>&1; then
75
+ if rt_needs rt_is_app_code reuse-first-guard; then
69
76
  rt_is_app_code "$path" || exit 0
70
77
  fi
71
78
 
@@ -38,6 +38,7 @@ command -v skill_for >/dev/null 2>&1 || exit 0
38
38
 
39
39
  req=""
40
40
  target=""
41
+ kind=""
41
42
  case "$tool" in
42
43
  # Инструмент среды заводит файл теми же двумя данными, только называет их иначе — без этой
43
44
  # ветки файл заводился мимо гейта.
@@ -61,6 +62,12 @@ case "$tool" in
61
62
  # приходит в обычный сервис, а число-настройка в обычный класс.
62
63
  written="$(printf '%s' "$input" | jq -r '[.tool_input.content, .tool_input.text, .tool_input.new_string, (.tool_input.edits[]?.new_string)] | map(select(. != null)) | join("\n")' 2>/dev/null)"
63
64
  req="$(skill_for edit "$target" "$written" 2>/dev/null)"
65
+ # Род правки для наблюдения. Одно расширение, без пути и без имени файла: наблюдение
66
+ # уезжает наружу, и всё, кроме рода, там было бы адресом этого дерева.
67
+ case "${target##*/}" in
68
+ *.*) kind="${target##*.}" ;;
69
+ *) kind="none" ;;
70
+ esac
64
71
  ;;
65
72
  # Терминал среды исполняет ту же командную строку и кладёт её в то же поле: без этой ветки
66
73
  # коммит из него не требовал правила, тогда как тот же коммит из оболочки требовал.
@@ -77,9 +84,11 @@ case "$tool" in
77
84
  [ -n "$inner" ] && target="$inner"
78
85
  fi
79
86
  req="$(skill_for bash "$target" "" 2>/dev/null)"
87
+ kind="command"
80
88
  ;;
81
89
  mcp__claude-in-chrome__*)
82
90
  req="$(skill_for browser "$tool" "" 2>/dev/null)"
91
+ kind="browser"
83
92
  ;;
84
93
  *) exit 0 ;;
85
94
  esac
@@ -108,6 +117,12 @@ done
108
117
  [ -z "$want" ] && exit 0
109
118
  req="$want"
110
119
 
120
+ # Отбитие — наблюдение: правило, которое приходится требовать чаще прочего, и род правки, на
121
+ # котором это происходит, говорят о слое правил больше, чем список загруженного.
122
+ # shellcheck disable=SC1090
123
+ [ -f "$rt_hooks_dir/observe.sh" ] && . "$rt_hooks_dir/observe.sh" 2>/dev/null
124
+ command -v rt_note >/dev/null 2>&1 && rt_note gate-deny "res=$req" "kind=$kind" "sid=$sid"
125
+
111
126
  reason="Отбито гейтом правил: загрузи правило «${req}» инструментом Skill и повтори действие. Для этой области это происходит один раз за сессию."
112
127
 
113
128
  # Правило называет свой закон одним словом, а слоёв законов два: общий лежит в корне, закон
@@ -18,4 +18,11 @@ dir="${TMPDIR:-/tmp}/claude-skill-gate"
18
18
  mkdir -p "$dir" 2>/dev/null || exit 0
19
19
  printf '%s\n' "$skill" >> "$dir/${sid}.loaded" 2>/dev/null
20
20
 
21
+ # Та же загрузка вторым адресом — в наблюдения дерева. Запись выше живёт до сжатия контекста и
22
+ # гибнет вместе с ним: она отвечает гейту на вопрос «загружено ли», и больше ни на что.
23
+ rt_hooks_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
24
+ # shellcheck disable=SC1090
25
+ [ -f "$rt_hooks_dir/observe.sh" ] && . "$rt_hooks_dir/observe.sh" 2>/dev/null
26
+ command -v rt_note >/dev/null 2>&1 && rt_note skill-load "res=$skill" "sid=$sid"
27
+
21
28
  exit 0
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env bash
2
2
  # rt-hook: SessionStart startup|resume|compact|clear
3
+ # Требует: hooks/profile-check.sh
3
4
  # SessionStart: состояние незаконченной работы уезжает в контекст на каждом запуске сессии.
4
5
  #
5
6
  # Памятью это не держится по той же причине, что и словарь: замысел читают перед правкой
@@ -28,6 +29,12 @@ for profile in "$rt_hooks_dir/../rt-kit/defaults/project.sh" "$rt_hooks_dir/../d
28
29
  [ -f "$profile" ] && . "$profile" 2>/dev/null
29
30
  done
30
31
 
32
+ # Слово о нехватке функции профиля: хук, вышедший молча, неотличим от работающего. Файл может
33
+ # быть не разложен — тогда остаётся прежнее поведение, молчаливое.
34
+ # shellcheck disable=SC1090
35
+ [ -f "$rt_hooks_dir/profile-check.sh" ] && . "$rt_hooks_dir/profile-check.sh"
36
+ command -v rt_needs >/dev/null 2>&1 || rt_needs() { command -v "$1" >/dev/null 2>&1; }
37
+
31
38
  TASKS_DIR="${RT_TASKS_DIR:-docs/tasks}"
32
39
  [ -z "$TASKS_DIR" ] && exit 0
33
40
 
@@ -43,13 +50,20 @@ emit() {
43
50
  # Ветка под задачу без папки — работа идёт мимо. Сессию не рвём: SessionStart, отбивающий
44
51
  # запуск, оставляет владельца без агента вовсе, а правку кода поймает `task-flow-guard`.
45
52
  if [ ! -d "$DIR" ]; then
46
- if command -v rt_task_branch_ok >/dev/null 2>&1 && rt_task_branch_ok "$branch"; then
53
+ if rt_needs rt_task_branch_ok task-context-load && rt_task_branch_ok "$branch"; then
47
54
  {
48
55
  printf 'РАБОТА БЕЗ ПАПКИ ЗАДАЧИ.\n\n'
49
56
  printf 'Ветка `%s` названа задачей, а `%s/` нет: ход работы записывать некуда,\n' "$branch" "$DIR"
50
57
  printf 'и следующий заход начнёт с расспросов владельца.\n\n'
51
58
  printf 'Собрать с образца:\n\n cp -r %s/_template %s\n\n' "$TASKS_DIR" "$DIR"
52
- printf 'Правку кода приложения до этого отбивает гард. Правило скил `task-flow`.\n'
59
+ # О соседнем ресурсе условно и по имени: пакет не знает, разложен ли он здесь,
60
+ # а сказанное безусловно приходит в контекст каждой сессии и врёт про дерево тем
61
+ # увереннее, что печатает это сам инструмент.
62
+ if [ -f "$rt_hooks_dir/task-flow-guard.sh" ]; then
63
+ printf 'Правку кода приложения до этого отбивает гард `task-flow-guard`. Правило — скил `task-flow`.\n'
64
+ else
65
+ printf 'Правило — скил `task-flow`. Гарда `task-flow-guard` в дереве нет: правку кода до этого не отбивает ничто.\n'
66
+ fi
53
67
  } | emit
54
68
  fi
55
69
  exit 0
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env bash
2
2
  # rt-hook: PreToolUse Edit|Write|MultiEdit
3
+ # Требует: hooks/profile-check.sh
3
4
  # PreToolUse guard for Edit|Write|MultiEdit: код не пишется раньше замысла.
4
5
  #
5
6
  # Работа идёт много заходов, и между ними исполнитель не помнит ничего. Замысел, лежащий на
@@ -45,11 +46,17 @@ for profile in "$rt_hooks_dir/../rt-kit/defaults/project.sh" "$rt_hooks_dir/../d
45
46
  [ -f "$profile" ] && . "$profile" 2>/dev/null
46
47
  done
47
48
 
49
+ # Слово о нехватке функции профиля: хук, вышедший молча, неотличим от работающего. Файл может
50
+ # быть не разложен — тогда остаётся прежнее поведение, молчаливое.
51
+ # shellcheck disable=SC1090
52
+ [ -f "$rt_hooks_dir/profile-check.sh" ] && . "$rt_hooks_dir/profile-check.sh"
53
+ command -v rt_needs >/dev/null 2>&1 || rt_needs() { command -v "$1" >/dev/null 2>&1; }
54
+
48
55
  # Признак «правка меняет поведение» — путь, а не оценка на глаз: оценку назначает тот, кому
49
56
  # она мешает, и порог плывёт. Где живёт код приложения, знает профиль: правила, тексты, обвязка
50
57
  # и зависимости под требование не попадают — иначе разбор задачи нельзя было бы вести до
51
58
  # заведения ветки.
52
- command -v rt_is_app_code >/dev/null 2>&1 || exit 0
59
+ rt_needs rt_is_app_code task-flow-guard || exit 0
53
60
  rt_is_app_code "$path" || exit 0
54
61
 
55
62
  # Каталог папок задач: у дерева он свой, но имя обычно общее.
@@ -70,7 +77,7 @@ git rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0
70
77
  branch="$(git branch --show-current 2>/dev/null)"
71
78
  [ -z "$branch" ] && exit 0 # detached HEAD — не про наш случай
72
79
 
73
- if command -v rt_task_branch_ok >/dev/null 2>&1 && ! rt_task_branch_ok "$branch"; then
80
+ if rt_needs rt_task_branch_ok task-flow-guard && ! rt_task_branch_ok "$branch"; then
74
81
  deny "BLOCKED by task-flow: правка кода идёт в ветке под задачу, а текущая ветка — '${branch}'. Заведи задачу (npm run task:new -- --title '…' --slug <slug>) и ветку под её номером, затем повтори. Правило — скил task-flow."
75
82
  fi
76
83