@rt-tools/agent-kit 0.16.1 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/assets/checks/board-epics.github.mjs +123 -0
  2. package/assets/checks/board-gh.github.mjs +121 -0
  3. package/assets/checks/board-paths.github.mjs +88 -0
  4. package/assets/checks/board-runs.github.mjs +8 -0
  5. package/assets/checks/board-titles.github.mjs +66 -0
  6. package/assets/checks/board.github.mjs +77 -73
  7. package/assets/checks/check-board.github.mjs +56 -7
  8. package/assets/checks/check-file-size.mjs +10 -2
  9. package/assets/checks/check-glossary.mjs +170 -0
  10. package/assets/checks/check-hook-scope.mjs +126 -0
  11. package/assets/checks/check-profile-drift.mjs +195 -0
  12. package/assets/checks/check-push-gate.mjs +59 -1
  13. package/assets/checks/check-schema-drift.mjs +12 -5
  14. package/assets/checks/check-specs.mjs +1 -1
  15. package/assets/checks/rt-kit-checks.config.mjs +13 -0
  16. package/assets/checks/spec-anchors.mjs +2 -2
  17. package/assets/checks/spec-contract.mjs +9 -0
  18. package/assets/defaults/gate-map.sh +13 -4
  19. package/assets/defaults/project.sh +31 -108
  20. package/assets/defaults/shell.sh +139 -0
  21. package/assets/docs/GLOSSARY.md +1 -1
  22. package/assets/hooks/browser-device-id.sh +20 -4
  23. package/assets/hooks/browser-guard-device-id.sh +5 -2
  24. package/assets/hooks/browser-guard-no-other-drivers.sh +41 -2
  25. package/assets/hooks/claim-guard.sh +22 -1
  26. package/assets/hooks/docs-guard.sh +1 -1
  27. package/assets/hooks/exam-guard.sh +85 -12
  28. package/assets/hooks/git-guard-delivery-conflict.sh +85 -0
  29. package/assets/hooks/git-guard-delivery-folder.sh +19 -0
  30. package/assets/hooks/git-guard-delivery-signature.sh +14 -5
  31. package/assets/hooks/git-guard-delivery.sh +54 -2
  32. package/assets/hooks/git-guard-push-tests.sh +21 -1
  33. package/assets/hooks/grill-gate-ask.sh +25 -0
  34. package/assets/hooks/grill-gate.sh +75 -11
  35. package/assets/hooks/lint-after-edit.sh +44 -16
  36. package/assets/hooks/proposal-guard.sh +11 -4
  37. package/assets/hooks/rule-source-guard.sh +8 -13
  38. package/assets/hooks/skill-gate-layers.sh +7 -0
  39. package/assets/hooks/skill-gate.sh +29 -0
  40. package/assets/hooks/task-context-load.sh +32 -0
  41. package/assets/hooks/task-flow-context.sh +194 -0
  42. package/assets/hooks/task-flow-draft-guard.sh +109 -0
  43. package/assets/hooks/task-flow-guard.sh +27 -159
  44. package/assets/hooks/turn-exit-guard.sh +53 -88
  45. package/assets/hooks/waiting-turn-guard.sh +65 -2
  46. package/assets/hooks/window-fill-guard.sh +5 -1
  47. package/assets/hooks/work-start-guard.sh +166 -0
  48. package/assets/hooks/write-targets.sh +24 -0
  49. package/assets/laws/delivery.md +19 -0
  50. package/assets/laws/frontend-application.md +4 -0
  51. package/assets/laws/verifiability.md +44 -0
  52. package/assets/laws/work-conduct.md +15 -0
  53. package/assets/patterns/browser-verification-measure.md +3 -1
  54. package/assets/patterns/browser-verification-stand.md +17 -4
  55. package/assets/patterns/doc-style-write.md +62 -2
  56. package/assets/patterns/git-workflow-commit.github.md +33 -5
  57. package/assets/patterns/git-workflow-docker.md +22 -0
  58. package/assets/patterns/git-workflow-freshness.md +87 -0
  59. package/assets/patterns/git-workflow-merge.md +41 -0
  60. package/assets/patterns/git-workflow-migration.md +11 -0
  61. package/assets/patterns/git-workflow-pr.github.md +12 -1
  62. package/assets/patterns/git-workflow-restart.md +18 -0
  63. package/assets/patterns/git-workflow-secrets.md +14 -0
  64. package/assets/patterns/git-workflow-stack.md +93 -1
  65. package/assets/patterns/lib-layers-move.md +4 -0
  66. package/assets/patterns/spec-driven-domain.md +47 -2
  67. package/assets/patterns/spec-driven-rule.md +16 -4
  68. package/assets/patterns/spec-driven-sweep.md +57 -0
  69. package/assets/patterns/status-report-table.github.md +1 -1
  70. package/assets/patterns/task-flow-archive.md +58 -16
  71. package/assets/patterns/task-flow-close.md +106 -23
  72. package/assets/patterns/task-flow-handoff.md +14 -2
  73. package/assets/patterns/task-flow-resume.md +35 -8
  74. package/assets/patterns/task-flow-start.md +60 -22
  75. package/assets/patterns/testing-e2e.md +23 -0
  76. package/assets/patterns/turn-entry-map.md +1 -1
  77. package/assets/pitfalls/agent-kit.md +71 -3
  78. package/assets/pitfalls/doc-style.md +15 -0
  79. package/assets/pitfalls/git-workflow.github.md +88 -0
  80. package/assets/pitfalls/spec-driven.md +22 -0
  81. package/assets/pitfalls/task-flow.md +113 -0
  82. package/assets/pitfalls/testing.md +6 -0
  83. package/assets/pitfalls/turn-conduct.md +49 -0
  84. package/assets/rules/browser-verification.md +58 -0
  85. package/assets/rules/deploy-flow.azure.md +8 -0
  86. package/assets/rules/deploy-flow.github.md +27 -0
  87. package/assets/rules/deploy-flow.gitlab.md +8 -0
  88. package/assets/rules/doc-style.md +20 -0
  89. package/assets/rules/git-workflow.azure.md +22 -2
  90. package/assets/rules/git-workflow.github.md +127 -118
  91. package/assets/rules/git-workflow.gitlab.md +17 -4
  92. package/assets/rules/lists.md +5 -0
  93. package/assets/rules/observability.needs-app.md +4 -0
  94. package/assets/rules/reuse-first.md +14 -0
  95. package/assets/rules/shared-code.md +5 -0
  96. package/assets/rules/spec-driven.md +33 -15
  97. package/assets/rules/task-flow.md +132 -126
  98. package/assets/rules/testing.md +41 -2
  99. package/assets/rules/turn-conduct.md +73 -57
  100. package/assets/rules/turn-entry.md +6 -0
  101. package/assets/samples/tasks/_template/grill.md +5 -0
  102. package/assets/samples/tasks/_template/plan.md +3 -0
  103. package/assets/skills/agent-kit.md +108 -76
  104. package/assets/templates/postmortem.md +5 -1
  105. package/bin/agent-kit.d.ts.map +1 -1
  106. package/bin/agent-kit.js +30 -6
  107. package/bin/agent-kit.js.map +1 -1
  108. package/lib/catalog.d.ts.map +1 -1
  109. package/lib/catalog.js +2 -1
  110. package/lib/catalog.js.map +1 -1
  111. package/lib/commands.d.ts.map +1 -1
  112. package/lib/commands.js +96 -7
  113. package/lib/commands.js.map +1 -1
  114. package/lib/hooks-map.d.ts +26 -0
  115. package/lib/hooks-map.d.ts.map +1 -1
  116. package/lib/hooks-map.js +58 -2
  117. package/lib/hooks-map.js.map +1 -1
  118. package/lib/sections.d.ts +6 -0
  119. package/lib/sections.d.ts.map +1 -1
  120. package/lib/sections.js +19 -0
  121. package/lib/sections.js.map +1 -1
  122. package/lib/shipment.d.ts +2 -0
  123. package/lib/shipment.d.ts.map +1 -1
  124. package/lib/shipment.fixture.d.ts +5 -0
  125. package/lib/shipment.fixture.d.ts.map +1 -1
  126. package/lib/shipment.fixture.js +7 -0
  127. package/lib/shipment.fixture.js.map +1 -1
  128. package/lib/shipment.js +13 -1
  129. package/lib/shipment.js.map +1 -1
  130. package/lib/sync.d.ts +35 -3
  131. package/lib/sync.d.ts.map +1 -1
  132. package/lib/sync.js +59 -8
  133. package/lib/sync.js.map +1 -1
  134. package/package.json +1 -1
  135. package/rt-tools-agent-kit-0.18.0.tgz +0 -0
  136. package/rt-tools-agent-kit-0.16.1.tgz +0 -0
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env bash
2
- # rt-hook: PreToolUse AskUserQuestion
3
2
  # Требует: hooks/deny-tail.sh
4
3
  # rt-hook: Stop
5
4
  # Гард разговора: вопрос владельцу не задаётся, пока за этот же ход не читались законы и
6
- # правила. Стоит на двух событиях, и это не дублирование.
5
+ # правила. Судит два события, и это не дублирование; второе объявлено соседним ресурсом
6
+ # `hooks/grill-gate-ask.sh`, который отдаёт вызов сюда.
7
7
  #
8
8
  # Зачем именно так. Требование «правила читаются до разговора» исполнимо ровно до отправки
9
9
  # вопроса. Проверка на завершении хода отбивает задним числом: к моменту отказа вопрос уже у
@@ -13,12 +13,23 @@
13
13
  # Одним этим перехватом дыра не закрывается: вопрос чаще задаётся прозой, и ровно так был задан
14
14
  # тот, из-за которого гард заведён. Прозаический вопрос инструментом не является, и поймать его
15
15
  # можно только на завершении хода — событие получает путь к записи хода и видит его целиком.
16
- # Отсюда два события: меню ловится до отправки, проза — после.
16
+ # Отсюда два события: меню ловится до отправки, проза — после. Объявлены они разными ресурсами
17
+ # затем, чтобы дерево, у которого инструмент вопроса занят своим гардом, могло взять половину, а
18
+ # не отказаться от требования целиком.
17
19
  #
18
20
  # Чтением правил считается любой из трёх путей: загрузка правила, чтение файла законов или
19
21
  # правил, поиск по ним. Требовать именно загрузку значило бы гнать на неё там, где хватило
20
22
  # одного поиска, — гард мешал бы работе вместо того, чтобы её выправлять.
21
23
  #
24
+ # Но чтение чего угодно из слоя правил вопроса не закрывает: прочитанный разбор чужого промаха
25
+ # и поиск по каталогу засчитывались наравне с правилом, которое этой работе и требуется, — а
26
+ # ответ на заданный владельцу вопрос лежал ровно в нём. Поэтому, когда область работы этого хода
27
+ # известна, чтением считается только правило этой области. Область берётся оттуда же, откуда её
28
+ # берёт гейт правил: по путям правок хода.
29
+ #
30
+ # ОТКАЗ В ПОЛЬЗУ РАБОТЫ и здесь: правок в ходу нет, карты гейта нет, область не определилась —
31
+ # засчитывается любое чтение, как прежде.
32
+ #
22
33
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: при любой ошибке, отсутствии записи хода и повторном заходе ход
23
34
  # РАЗРЕШАЕТСЯ (exit 0). Сломанный гард не имеет права заклинить разговор.
24
35
 
@@ -57,15 +68,56 @@ done
57
68
  laws_dir="${RT_LAWS_DIR-docs/constitution}"
58
69
  rules_dir="${RT_RULES_DIR-.claude/skills}"
59
70
  specs_dir="${RT_SPECS_DIR-docs/specs}"
71
+ # Замысел эпика и описание прошлого читаются наравне с законами: решение, связывающее задачи
72
+ # эпика, лежит именно там. Прочитавший замысел получал отказ наравне с не читавшим ничего, и
73
+ # снимался тот отказ поиском по трём каталогам, среди которых нужного не было.
74
+ plans_dir="${RT_PLANS_DIR-docs/plans}"
75
+ archive_dir="${RT_ARCHIVE_DIR-docs/archive}"
60
76
 
61
77
  # Дерево, у которого нет ни законов, ни правил, требования не получает: читать нечего.
62
78
  [ -z "$laws_dir" ] && [ -z "$rules_dir" ] && exit 0
63
79
 
80
+ # Карта гейта: по ней имя правила достаётся из пути правки. Нет её — область не определяется, и
81
+ # всё остаётся прежним.
82
+ rt_hooks_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
83
+ for map in "$rt_hooks_dir/../rt-kit/defaults/gate-map.sh" "$rt_hooks_dir/../defaults/gate-map.sh" "${CLAUDE_PROJECT_DIR:-.}/.claude/rt-kit/defaults/gate-map.sh" "${CLAUDE_PROJECT_DIR:-.}/.claude/rt-kit/gate-map.sh"; do
84
+ # shellcheck disable=SC1090
85
+ [ -f "$map" ] && . "$map" 2>/dev/null
86
+ done
87
+
64
88
  # Образец, по которому вызов инструмента считается чтением правил. Каталоги идут в него как
65
89
  # есть: точка в `.claude` совпадает с любым знаком и лишнего сюда не приводит.
66
- read_re="$(printf '%s' "$laws_dir|$rules_dir|$specs_dir" | sed 's/^|*//; s/|*$//; s/||*/|/g')"
90
+ read_re="$(printf '%s' "$laws_dir|$rules_dir|$specs_dir|$plans_dir|$archive_dir" | sed 's/^|*//; s/|*$//; s/||*/|/g')"
67
91
  [ -z "$read_re" ] && exit 0
68
92
 
93
+ # Область работы этого хода: правила, которых требует гейт от путей, правленных в ходу. Пути
94
+ # достаются тем же разбором хода, что и ниже, — своим вызовом, чтобы образец чтения был готов
95
+ # к главному разбору.
96
+ need_re=''
97
+ if command -v skill_for >/dev/null 2>&1 && [ -n "$rules_dir" ]; then
98
+ edited="$(tail -n 400 "$transcript" 2>/dev/null | jq -s -r '
99
+ def is_input:
100
+ .type == "user"
101
+ and (((.message.content // []) | if type == "array"
102
+ then ([.[] | select(.type == "tool_result")] | length)
103
+ else 0 end) == 0);
104
+ (map(is_input) | rindex(true)) as $i
105
+ | (if $i == null then . else .[$i + 1:] end)
106
+ | [.[] | select(.type == "assistant") | (.message.content // [])[]
107
+ | select(.type == "tool_use") | select(.name == "Edit" or .name == "Write" or .name == "NotebookEdit")
108
+ | (.input.file_path // .input.notebook_path // "")]
109
+ | map(select(. != "")) | unique | .[]
110
+ ' 2>/dev/null)"
111
+ for path in $edited; do
112
+ for rule in $(skill_for edit "$path" '' 2>/dev/null); do
113
+ case "|$need_re|" in
114
+ *"|$rule|"*) ;;
115
+ *) need_re="${need_re}${need_re:+|}${rule}" ;;
116
+ esac
117
+ done
118
+ done
119
+ fi
120
+
69
121
  # Ход — это всё, что записано после последнего настоящего ввода владельца. Ответ инструмента
70
122
  # приходит той же ролью `user`, поэтому строки с `tool_result` вводом не считаются: иначе ходом
71
123
  # оказался бы кусок после последнего вызова инструмента, и чтение правил в его начале потерялось
@@ -75,7 +127,7 @@ read_re="$(printf '%s' "$laws_dir|$rules_dir|$specs_dir" | sed 's/^|*//; s/|*$//
75
127
  # На событии вызова инструмента вопрос уже известен — он и есть вызов; судится только то,
76
128
  # читались ли за этот ход правила. На завершении хода вопрос ищется в тексте реплик: меню к
77
129
  # этому моменту уже отбито раньше.
78
- verdict="$(tail -n 400 "$transcript" 2>/dev/null | jq -s -r --arg re "$read_re" --arg tool "$tool" '
130
+ verdict="$(tail -n 400 "$transcript" 2>/dev/null | jq -s -r --arg re "$read_re" --arg tool "$tool" --arg need "$need_re" --arg rules "$rules_dir" '
79
131
  def is_input:
80
132
  .type == "user"
81
133
  and (((.message.content // []) | if type == "array"
@@ -86,11 +138,23 @@ verdict="$(tail -n 400 "$transcript" 2>/dev/null | jq -s -r --arg re "$read_re"
86
138
  | (if $i == null then . else .[$i + 1:] end) as $turn
87
139
  | [$turn[] | select(.type == "assistant") | (.message.content // [])[] | select(.type == "text") | .text] as $texts
88
140
  | [$turn[] | select(.type == "assistant") | (.message.content // [])[] | select(.type == "tool_use")] as $uses
89
- | ($uses | map(
90
- (.name == "Skill")
91
- or ((.name // "") | test("^(Read|Grep|Glob)$")) and ((.input | tostring) | test($re))
92
- or ((.name == "Bash") and ((.input.command // "") | test($re)))
93
- ) | any) as $read
141
+ | (if $need == "" then
142
+ $uses | map(
143
+ (.name == "Skill")
144
+ or ((.name // "") | test("^(Read|Grep|Glob)$")) and ((.input | tostring) | test($re))
145
+ or ((.name == "Bash") and ((.input.command // "") | test($re)))
146
+ ) | any
147
+ else
148
+ # Область работы известна — засчитывается только правило этой области: прочитанный
149
+ # разбор чужого промаха на заданный вопрос не отвечает.
150
+ ($need | split("|")) as $rules_needed
151
+ | $uses | map(
152
+ ((.name == "Skill") and (((.input.skill // "") | tostring) as $s | $rules_needed | index($s) != null))
153
+ or (((.name // "") | test("^(Read|Grep|Glob|Bash)$"))
154
+ and ((.input | tostring) as $text
155
+ | $rules_needed | map(. as $rule | $text | test($rules + "/" + $rule + "(/|\\b)")) | any))
156
+ ) | any
157
+ end) as $read
94
158
  | (($texts | join("\n")) | test("\\?[[:space:]]*$"; "m")) as $asked_prose
95
159
  | ($tool != "") as $asking_now
96
160
  | if ($asked_prose or $asking_now) and ($read | not) then "ask" else "pass" end
@@ -106,7 +170,7 @@ fi
106
170
 
107
171
  reason="$head Вопрос, ответ на который уже записан, владельцу не задаётся — правило ведения работы. Прогони поиск по словам темы и ответь по найденному; спрашивай только то, что документацией не покрыто:
108
172
 
109
- grep -rn -i \"<слово темы>\" $laws_dir $rules_dir $specs_dir
173
+ grep -rn -i \"<слово темы>\" $laws_dir $rules_dir $specs_dir $plans_dir $archive_dir
110
174
 
111
175
  Гард судит один ход: следующий заход не отбивается."
112
176
 
@@ -3,11 +3,15 @@
3
3
  # Требует: hooks/profile-check.sh
4
4
  # Линтер по следам правки. PostToolUse.
5
5
  #
6
- # Два входа. Правка файла — линтуется один изменённый файл. Перенос файла командой линтуются
7
- # файлы по адресу назначения: перенос меняет либу, а вместе с ней и границы, и импорт, законный
8
- # на прежнем месте, на новом уже запрещён. Ровно так запрещённый импорт уезжает в общую либу
9
- # молча: файлы перекладываются командой, хук на неё не смотрит, и правило границ сработало бы
10
- # но его никто не запустил.
6
+ # Два входа. Правка файла — линтуется один изменённый файл. Команда оболочкилинтуется то,
7
+ # что она записала: перенос меняет либу, а вместе с ней и границы, и импорт, законный на прежнем
8
+ # месте, на новом уже запрещён; запись перенаправлением, дозаписью или интерпретатором меняет
9
+ # сам файл, и линтера он прежде не получал вовсе. Ровно так запрещённый импорт уезжает в общую
10
+ # либу молча, а правленая проверка выходит из-под форматтера: правило сработало бы — но его
11
+ # никто не запустил.
12
+ #
13
+ # Что команда пишет и куда, знает профиль дерева, и знает он это одним способом для всех: тот
14
+ # же признак читает гейт правил.
11
15
  #
12
16
  # Полный набор гоняет гард на пуше, но это конец работы: к моменту, когда правило срабатывает,
13
17
  # поверх нарушения лежит десяток правок, и разбор превращается в археологию. Здесь тот же
@@ -19,6 +23,8 @@
19
23
  #
20
24
  # Что здесь чем зовётся, знает профиль дерева:
21
25
  # rt_lint_for — чем линтуется этот файл;
26
+ # rt_shell_writes — пишет ли эта команда;
27
+ # rt_shell_paths — какие пути она записала;
22
28
  # rt_is_app_code — где лежит код, к которому линтеры вообще относятся;
23
29
  # RT_LINT_SKIP_RE — что из него исключено;
24
30
  # rt_push_checks — что гоняет гейт пуша. По нему же решается, догонит ли замечание позже:
@@ -46,14 +52,12 @@ case "$tool" in
46
52
  *) exit 0 ;;
47
53
  esac
48
54
 
49
- # Отсев до всякой работы: хук висит на каждой команде оболочки, а перенос среди них — редкость.
55
+ # Отсев до всякой работы: хук висит на каждой команде оболочки, а пишет из них меньшинство.
56
+ # Пустая команда отсеивается здесь же — признак записи у неё спрашивать не о чем.
50
57
  command_text=""
51
58
  if [ "$mode" = "move" ]; then
52
59
  command_text="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null)"
53
- case "$command_text" in
54
- *"git mv "*) ;;
55
- *) exit 0 ;;
56
- esac
60
+ [ -z "$command_text" ] && exit 0
57
61
  fi
58
62
 
59
63
  workdir="$(rt_hook_cwd)"
@@ -75,6 +79,15 @@ done
75
79
  command -v rt_needs >/dev/null 2>&1 || rt_needs() { command -v "$1" >/dev/null 2>&1; }
76
80
  rt_needs rt_lint_for lint-after-edit || exit 0
77
81
 
82
+ # Признак записи спрашивается после профиля: до него функций ещё нет. Дерево, которое признака
83
+ # не объявило, остаётся при прежнем поведении — разбирается один перенос.
84
+ if [ "$mode" = "move" ] && rt_needs rt_shell_writes lint-after-edit; then
85
+ case "$command_text" in
86
+ *"git mv "*) ;;
87
+ *) rt_shell_writes "$command_text" || exit 0 ;;
88
+ esac
89
+ fi
90
+
78
91
  # --- какие файлы проверяем -------------------------------------------------------------
79
92
 
80
93
  # Пути назначения всех переносов в команде. Команда бывает составной, поэтому режется по
@@ -136,7 +149,17 @@ if [ "$mode" = "edit" ]; then
136
149
  esac
137
150
  candidates="$(expand "$path")"
138
151
  else
139
- candidates="$(collect_moved "$command_text" | while IFS= read -r moved; do expand "$moved"; done)"
152
+ # Перенос разворачивает каталог: он двигает целые слои. Запись нет: пишут всегда в файл,
153
+ # а каталог в команде записи — это рабочий каталог, и развёрнутый он отдаёт линтеру половину
154
+ # дерева. Поэтому у записанных путей берутся только существующие файлы.
155
+ written=""
156
+ if rt_needs rt_shell_paths lint-after-edit; then
157
+ written="$(rt_shell_paths "$command_text" 2>/dev/null | while IFS= read -r path; do
158
+ [ -f "$path" ] && printf '%s\n' "$path"
159
+ done)"
160
+ fi
161
+ candidates="$( { collect_moved "$command_text" | while IFS= read -r moved; do expand "$moved"; done
162
+ printf '%s\n' "$written"; } | sort -u)"
140
163
  fi
141
164
 
142
165
  [ -z "$candidates" ] && exit 0
@@ -214,11 +237,16 @@ else
214
237
  tail_line="Почини их сейчас: этот линтер в гейт пуша не входит, и отложенное замечание уедет в главную ветку молча."
215
238
  fi
216
239
 
217
- if [ "$mode" = "move" ]; then
218
- head_line="ЛИНТЕР (${linters}) НАШЁЛ ЗАМЕЧАНИЯ ПОСЛЕ ПЕРЕНОСА. Перенос меняет либу, а вместе с ней границы: импорт, законный на прежнем месте, на новом может быть запрещён."
219
- else
220
- head_line="ЛИНТЕР (${linters}) НАШЁЛ ЗАМЕЧАНИЯ:"
221
- fi
240
+ case "$mode:$command_text" in
241
+ # Перенос назван отдельно: замечание после него объясняется не правкой файла, а сменой его
242
+ # места, и без этой строки читатель ищет промах в тексте, которого никто не менял.
243
+ move:*"git mv "*)
244
+ head_line="ЛИНТЕР (${linters}) НАШЁЛ ЗАМЕЧАНИЯ ПОСЛЕ ПЕРЕНОСА. Перенос меняет либу, а вместе с ней границы: импорт, законный на прежнем месте, на новом может быть запрещён." ;;
245
+ move:*)
246
+ head_line="ЛИНТЕР (${linters}) НАШЁЛ ЗАМЕЧАНИЯ ПОСЛЕ ЗАПИСИ КОМАНДОЙ:" ;;
247
+ *)
248
+ head_line="ЛИНТЕР (${linters}) НАШЁЛ ЗАМЕЧАНИЯ:" ;;
249
+ esac
222
250
 
223
251
  ctx="${head_line}
224
252
  ${report}
@@ -38,16 +38,24 @@ transcript="$(printf '%s' "$input" | jq -r '.transcript_path // empty' 2>/dev/nu
38
38
  [ -f "$transcript" ] || exit 0
39
39
 
40
40
  # Каталог предложений у дерева свой. Заданный пустым — отказ дерева от требования: дереву, не
41
- # берущему переносимый слой правил, гард не навязывается.
41
+ # берущему переносимый слой правил, гард не навязывается. Каталога нет на диске — то же самое:
42
+ # дерево механизмом не пользуется, и стража ему не навязывают. Судить по одной переменной значило
43
+ # бы требовать отправки там, где отправлять нечем и некуда.
44
+ root="${CLAUDE_PROJECT_DIR:-.}"
42
45
  proposals_dir="${RT_PROPOSALS_DIR-.claude/rt-kit/proposals}"
43
46
  [ -z "$proposals_dir" ] && exit 0
47
+ [ -d "$root/$proposals_dir" ] || exit 0
44
48
 
45
49
  # Просьба владельца. Набор открыт и пополняется правкой: полнота его — открытый вопрос
46
50
  # договорённости, а не обещание.
47
- asked_re='(завед|напиш|отправ|пошл|зашл|отошл|выгруз)[а-яё]*[^.!?]{0,40}(пропозал|предложени)|(пропозал|предложени)[а-яё]*[^.!?]{0,40}(завед|напиш|отправ|пошл|зашл|отошл|выгруз)'
51
+ #
52
+ # Латиница читается наравне: заход, который владелец ведёт по-английски, отличается от русского
53
+ # словами, а требование в нём то же. Своей ветки ей не заводится — обе пары стоят в одном
54
+ # образце, иначе одну из них правили бы, забывая про вторую.
55
+ asked_re='(завед|напиш|отправ|пошл|зашл|отошл|выгруз|send|file|open|submit|raise|report)[а-яёa-z]*[^.!?]{0,40}(пропозал|предложени|proposal)|(пропозал|предложени|proposal)[а-яёa-z]*[^.!?]{0,40}(завед|напиш|отправ|пошл|зашл|отошл|выгруз|send|file|open|submit|raise|report)'
48
56
 
49
57
  # Сосед, без которого слово «предложение» не считается просьбой о слое правил.
50
- context_re='пропозал|слою правил|слоя правил|слой правил|пакет|agent-kit|наверх'
58
+ context_re='пропозал|слою правил|слоя правил|слой правил|пакет|agent-kit|наверх|proposal|rule layer|upstream|package'
51
59
 
52
60
  # Ход — это всё, что записано после последнего настоящего ввода владельца. Ответ инструмента
53
61
  # приходит той же ролью, поэтому строки с `tool_result` вводом не считаются.
@@ -86,7 +94,6 @@ verdict="$(tail -n 400 "$transcript" 2>/dev/null | jq -s -r --arg asked "$asked_
86
94
  # зависимостью, зовёт бинарь из зависимостей; дерево, где пакет живёт исходниками, бинаря не
87
95
  # имеет вовсе — там зовут собранный bin. Названная наугад команда стоит исполнителю хода: отказ
88
96
  # читается как указание, и вызов `npx agent-kit` отвечает в таком дереве отказом установки.
89
- root="${CLAUDE_PROJECT_DIR:-.}"
90
97
  if [ -x "$root/node_modules/.bin/agent-kit" ]; then
91
98
  propose_cmd="npx agent-kit propose"
92
99
  else
@@ -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, hooks/deny-tail.sh
3
+ # Требует: hooks/profile-check.sh, hooks/deny-tail.sh, hooks/write-targets.sh
4
4
  # Гард места правки: слой правил чинится там, где сломано, а не там, где виден.
5
5
  #
6
6
  # Слой правил правит тот же исполнитель, которым слой правил управляет, и разницы между
@@ -53,18 +53,13 @@ deny() {
53
53
  exit 0
54
54
  }
55
55
 
56
- # Цели записи, названные командой прямо. Печатает по одной в строке.
57
- rt_write_targets() {
58
- tr "\"'\`" ' ' \
59
- | sed -E 's/>>?/\n>/g' \
60
- | sed -nE '
61
- s/^>[[:space:]]*([^[:space:]|&;]+).*/\1/p
62
- s/(^|.*[[:space:]])tee[[:space:]]+(-a[[:space:]]+)?([^[:space:]|&;]+).*/\3/p
63
- s/(^|.*[[:space:]])sed[[:space:]]+-i[[:space:]]+([^[:space:]]+[[:space:]]+)*([^[:space:]|&;]+)$/\3/p
64
- s/(^|.*[[:space:]])(cp|mv|install)[[:space:]]+([^[:space:]]+[[:space:]]+)+([^[:space:]|&;]+).*/\4/p
65
- ' \
66
- | sort -u
67
- }
56
+ # Цели записи разбирает общий помощник: тот же признак нужен гарду экзамена, и разойдясь, две
57
+ # копии пропустили бы разные формы записи. Файла нет — остаётся молчаливое умолчание, чтобы гард
58
+ # не сломался на неполной раскладке.
59
+ # shellcheck disable=SC1090
60
+ [ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/write-targets.sh" ] \
61
+ && . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/write-targets.sh" 2>/dev/null
62
+ command -v rt_write_targets >/dev/null 2>&1 || rt_write_targets() { cat >/dev/null; }
68
63
 
69
64
  tool="$(rt_hook_tool)"
70
65
  candidates=""
@@ -22,6 +22,13 @@
22
22
  # как два разных требования.
23
23
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
24
24
 
25
+ # Собранное дерево кодом не бывает: путь к артефакту приходит из команды, которая его запускает.
26
+ # Карта гейта такой путь уже пропускает, и слои обязаны молчать вместе с ней — иначе выход из
27
+ # карты снимает одно требование и оставляет полтора десятка других.
28
+ case "$target" in
29
+ */node_modules/* | */dist/* | */build/* | */.nx/* | */coverage/*) return 0 ;;
30
+ esac
31
+
25
32
  rt_layer_add() {
26
33
  case " $req " in
27
34
  *" $1 "*) return 0 ;;
@@ -151,6 +151,20 @@ for name in $req; do
151
151
  break
152
152
  done
153
153
  [ -z "$want" ] && exit 0
154
+
155
+ # Что потребуется дальше по этой же команде. Требуется по-прежнему одно правило за раз, но
156
+ # длину пути заход видит с первого отказа: на сквозной правке отказы идут по одному на вызов и
157
+ # читаются как разные требования — тринадцать подряд за одну задачу, и каждый стоил хода.
158
+ ahead=""
159
+ for name in $req; do
160
+ [ "$name" = "$want" ] && continue
161
+ [ -f "$root/$rules_dir/${name}/SKILL.md" ] || continue
162
+ if [ -f "$loaded" ] && grep -qE "^([^:]*:)?$(printf '%s' "$name" | sed 's/[][\.*^$/]/\\&/g')$" "$loaded" 2>/dev/null; then
163
+ continue
164
+ fi
165
+ ahead="${ahead}${ahead:+, }${name}"
166
+ done
167
+
154
168
  req="$want"
155
169
 
156
170
  # Отбитие — наблюдение: правило, которое приходится требовать чаще прочего, и род правки, на
@@ -197,6 +211,21 @@ ${article}
197
211
  fi
198
212
  fi
199
213
 
214
+ # Что потребуется дальше по этой же команде. Строка стоит после всех веток текста: их три, и
215
+ # каждая переписывает причину целиком.
216
+ [ -n "$ahead" ] && reason="${reason} Дальше по этой команде потребуются: ${ahead}."
217
+
218
+ # Спутник зовётся отдельным предложением и ко всем видам отказа разом. Прежде он был назван
219
+ # только запасным ходом — «если инструмент такого имени не знает», — и читатель, у которого имя
220
+ # известно, до него не доходил вовсе. Цена этого: правило называет способ, а спутник рядом
221
+ # объявляет его здесь невозможным; работа, сделанная способом из правила, кончилась результатом,
222
+ # которого для её заказчика не существовало, а разделы спутника за тот заход не открывались ни
223
+ # разу.
224
+ companion="$rules_dir/${req}/implementation.md"
225
+ [ -f "$root/$companion" ] && reason="${reason}
226
+
227
+ Спутник правила — ${companion} — читается вместе с ним: правило говорит, что должно быть верно, а спутник — чем это верно здесь и что здесь названо невозможным. Инструментом он не грузится, его читают файлом."
228
+
200
229
  # Общий хвост отказа: два законных хода и законная форма обхода, если она у отказа есть.
201
230
  # Файл может быть не разложен — тогда хвоста нет, а причина отказа остаётся прежней.
202
231
  # shellcheck disable=SC1090
@@ -52,12 +52,44 @@ emit() {
52
52
  # Ветка под задачу без папки — работа идёт мимо. Сессию не рвём: SessionStart, отбивающий
53
53
  # запуск, оставляет владельца без агента вовсе, а правку кода поймает `task-flow-guard`.
54
54
  if [ ! -d "$DIR" ]; then
55
+ # Папки нет по двум разным причинам, и говорить о них надо разное. Первая — работа шла мимо
56
+ # правила. Вторая — папку разобрала сама ветка последним коммитом перед заявкой: это законный
57
+ # исход, и указание собрать её заново уводит заход с хвоста работы обратно в её начало.
58
+ #
59
+ # Различает их история ветки: снос папки её же коммитом после общего предка с главной. Ход
60
+ # работы к этой минуте уехал вместе с папкой, и состояние держат заявка и передача захода.
61
+ main_branch="${RT_MAIN_BRANCH:-main}"
62
+ base="$(git merge-base "origin/${main_branch}" HEAD 2>/dev/null || git merge-base "$main_branch" HEAD 2>/dev/null)"
63
+ dropped=''
64
+ [ -n "$base" ] && dropped="$(git log "${base}..HEAD" --diff-filter=D --name-only --pretty=format: -- "$DIR" 2>/dev/null | head -1)"
65
+
66
+ if [ -n "$dropped" ]; then
67
+ {
68
+ printf 'РАБОТА ЗАКРЫВАЕТСЯ — папка задачи разобрана этой веткой.\n\n'
69
+ printf 'Ход работы удалён вместе с папкой: состояние держат заявка и передача захода.\n'
70
+ printf 'Папка заново не собирается. Правка кода после разбора требует восстановить её\n'
71
+ printf 'на время правки и повторить разбор тем же коммитом. Порядок — скил `task-flow`,\n'
72
+ printf 'паттерны `task-flow-close` и `task-flow-archive`.\n'
73
+ } | emit
74
+ exit 0
75
+ fi
76
+
55
77
  if rt_needs rt_task_branch_ok task-context-load && rt_task_branch_ok "$branch"; then
56
78
  {
57
79
  printf 'РАБОТА БЕЗ ПАПКИ ЗАДАЧИ.\n\n'
58
80
  printf 'Ветка `%s` названа задачей, а `%s/` нет: ход работы записывать некуда,\n' "$branch" "$DIR"
59
81
  printf 'и следующий заход начнёт с расспросов владельца.\n\n'
60
82
  printf 'Собрать с образца:\n\n cp -r %s/_template %s\n\n' "$TASKS_DIR" "$DIR"
83
+ # Папка бывает названа не именем ветки: этапы одной большой задачи идут отдельными
84
+ # ветками при одной общей папке. Названная поимённо папка — единственное, по чему
85
+ # заход её найдёт; иначе он читает отказ как «записей нет» и отвечает владельцу из
86
+ # кода, минуя всё, что в этих записях решено.
87
+ others="$(find "$TASKS_DIR" -mindepth 1 -maxdepth 1 -type d ! -name '_template' 2>/dev/null | sort)"
88
+ if [ -n "$others" ]; then
89
+ printf 'В каталоге задач при этом лежит:\n\n%s\n\n' "$others"
90
+ printf 'Этапы одной задачи идут отдельными ветками при общей папке — прежде чем\n'
91
+ printf 'считать, что записей нет, смотрят в названные.\n\n'
92
+ fi
61
93
  # О соседнем ресурсе — условно и по имени: пакет не знает, разложен ли он здесь,
62
94
  # а сказанное безусловно приходит в контекст каждой сессии и врёт про дерево тем
63
95
  # увереннее, что печатает это сам инструмент.