@rt-tools/agent-kit 0.9.1 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/README.md +5 -0
  2. package/assets/agents/conscience.md +58 -0
  3. package/assets/agents/prose-editor.md +44 -0
  4. package/assets/agents/strict-teacher.md +59 -0
  5. package/assets/checks/board.github.mjs +56 -1
  6. package/assets/checks/check-board.github.mjs +24 -0
  7. package/assets/checks/check-doc-paths.mjs +4 -15
  8. package/assets/checks/check-dupes.mjs +5 -6
  9. package/assets/checks/check-file-size.mjs +6 -20
  10. package/assets/checks/check-prose-style.mjs +137 -0
  11. package/assets/checks/check-reuse.mjs +5 -5
  12. package/assets/checks/check-state-next.mjs +194 -0
  13. package/assets/checks/check-states.mjs +142 -0
  14. package/assets/checks/check-styles.mjs +5 -5
  15. package/assets/checks/check-turn-map.mjs +146 -0
  16. package/assets/checks/lib-common.mjs +3 -3
  17. package/assets/checks/rt-kit-checks.config.mjs +85 -1
  18. package/assets/commands/agent-kit-digest.md +6 -5
  19. package/assets/defaults/project.sh +108 -6
  20. package/assets/defaults/turn-map.md +46 -0
  21. package/assets/docs/GLOSSARY.md +17 -15
  22. package/assets/hooks/browser-device-id.sh +2 -0
  23. package/assets/hooks/browser-guard-device-id.sh +11 -1
  24. package/assets/hooks/browser-guard-no-asking.sh +11 -1
  25. package/assets/hooks/browser-guard-no-listing.sh +11 -1
  26. package/assets/hooks/browser-guard-no-other-drivers.sh +9 -1
  27. package/assets/hooks/browser-guard-require-select.sh +13 -2
  28. package/assets/hooks/claim-guard.sh +115 -0
  29. package/assets/hooks/commit-msg.sh +2 -0
  30. package/assets/hooks/conscience-guard.sh +100 -0
  31. package/assets/hooks/constitution-index.sh +2 -0
  32. package/assets/hooks/deny-tail.sh +32 -0
  33. package/assets/hooks/dev-server-guard.sh +10 -2
  34. package/assets/hooks/docs-guard.sh +16 -2
  35. package/assets/hooks/exam-guard.sh +123 -0
  36. package/assets/hooks/git-guard-delivery-signature.sh +77 -0
  37. package/assets/hooks/git-guard-delivery.sh +156 -68
  38. package/assets/hooks/git-guard-main.sh +14 -0
  39. package/assets/hooks/git-guard-push-tests.sh +51 -2
  40. package/assets/hooks/glossary-load.sh +2 -0
  41. package/assets/hooks/grill-gate.sh +14 -0
  42. package/assets/hooks/handoff-entry-guard.sh +73 -0
  43. package/assets/hooks/handoff-write.sh +103 -0
  44. package/assets/hooks/lint-after-edit.sh +2 -0
  45. package/assets/hooks/observe.sh +2 -0
  46. package/assets/hooks/postmortem-guard.sh +14 -0
  47. package/assets/hooks/proposal-guard.sh +14 -0
  48. package/assets/hooks/prose-style-guard.sh +75 -0
  49. package/assets/hooks/qa-dataid-guard.sh +14 -1
  50. package/assets/hooks/rerun-guard.sh +88 -0
  51. package/assets/hooks/reuse-first-guard.sh +14 -1
  52. package/assets/hooks/roles.sh +34 -0
  53. package/assets/hooks/skill-gate-layers.sh +2 -0
  54. package/assets/hooks/skill-gate-rearm.sh +2 -0
  55. package/assets/hooks/skill-gate.sh +14 -0
  56. package/assets/hooks/skill-loaded.sh +2 -0
  57. package/assets/hooks/sql-guard-parse.sh +2 -0
  58. package/assets/hooks/sql-guard-request.sh +2 -0
  59. package/assets/hooks/sql-guard-target.sh +2 -0
  60. package/assets/hooks/sql-guard-write.sh +4 -1
  61. package/assets/hooks/sql-guard.sh +14 -1
  62. package/assets/hooks/task-context-load.sh +2 -0
  63. package/assets/hooks/task-flow-guard.sh +73 -7
  64. package/assets/hooks/turn-entry-load.sh +62 -0
  65. package/assets/hooks/turn-exit-guard.sh +191 -0
  66. package/assets/hooks/utf8.sh +35 -0
  67. package/assets/hooks/waiting-turn-guard.sh +14 -0
  68. package/assets/hooks/window-fill-guard.sh +43 -2
  69. package/assets/laws/work-conduct.md +29 -0
  70. package/assets/patterns/cargo-triage-mark.md +119 -0
  71. package/assets/patterns/task-flow-close.md +29 -8
  72. package/assets/patterns/task-flow-handoff.md +19 -1
  73. package/assets/patterns/task-flow-resume.md +36 -4
  74. package/assets/patterns/task-flow-start.md +56 -6
  75. package/assets/patterns/turn-entry-map.md +81 -0
  76. package/assets/rules/cargo-triage.md +126 -0
  77. package/assets/rules/git-workflow.azure.md +36 -9
  78. package/assets/rules/git-workflow.github.md +74 -9
  79. package/assets/rules/git-workflow.gitlab.md +40 -12
  80. package/assets/rules/task-flow.md +136 -71
  81. package/assets/rules/testing.md +15 -1
  82. package/assets/rules/turn-entry.md +93 -0
  83. package/assets/samples/tasks/_template/plan.md +4 -1
  84. package/assets/samples/tasks/_template/progress.md +1 -0
  85. package/assets/skills/agent-kit.md +33 -0
  86. package/assets/templates/project.sh +16 -0
  87. package/bin/agent-kit.d.ts.map +1 -1
  88. package/bin/agent-kit.js +42 -1
  89. package/bin/agent-kit.js.map +1 -1
  90. package/lib/cargo-state.d.ts +62 -0
  91. package/lib/cargo-state.d.ts.map +1 -0
  92. package/lib/cargo-state.js +118 -0
  93. package/lib/cargo-state.js.map +1 -0
  94. package/lib/cargo.d.ts +42 -0
  95. package/lib/cargo.d.ts.map +1 -1
  96. package/lib/cargo.js +2 -0
  97. package/lib/cargo.js.map +1 -1
  98. package/lib/commands.d.ts.map +1 -1
  99. package/lib/commands.js +64 -4
  100. package/lib/commands.js.map +1 -1
  101. package/lib/config.d.ts +8 -0
  102. package/lib/config.d.ts.map +1 -1
  103. package/lib/config.js +1 -0
  104. package/lib/config.js.map +1 -1
  105. package/lib/observations.d.ts +35 -1
  106. package/lib/observations.d.ts.map +1 -1
  107. package/lib/observations.js +14 -2
  108. package/lib/observations.js.map +1 -1
  109. package/lib/ship.d.ts +3 -0
  110. package/lib/ship.d.ts.map +1 -1
  111. package/lib/ship.js +56 -0
  112. package/lib/ship.js.map +1 -1
  113. package/lib/thresholds.d.ts +49 -0
  114. package/lib/thresholds.d.ts.map +1 -0
  115. package/lib/thresholds.js +151 -0
  116. package/lib/thresholds.js.map +1 -0
  117. package/package.json +1 -1
  118. package/rt-tools-agent-kit-0.11.0.tgz +0 -0
  119. package/rt-tools-agent-kit-0.9.1.tgz +0 -0
@@ -50,6 +50,27 @@ RT_HOOKS_TESTS="${RT_HOOKS_TESTS:-.claude/hooks/tests/run.sh}"
50
50
  # её делают, а всплывает на чужой работе — раскладка отказывает по правленому файлу целиком и не
51
51
  # кладёт ни одного другого. Признак — сама настройка раскладки: дерево без неё пакета не ставит,
52
52
  # и звать в нём нечего.
53
+ # Тронула ли ветка только тексты. Нулевой код — только тексты, иначе — код.
54
+ #
55
+ # Признак нужен тому дереву, которое дописало в набор гейта тяжёлый шаг: стенд, снимки витрин,
56
+ # сборку образов. Без него такой шаг идёт подряд, чем бы ветка ни была, и пуш коммита, менявшего
57
+ # одну строку в таблице markdown, занимает минуты — за которые владелец успевает прочитать его
58
+ # как зависший и отбить. Правило поставки требует того же от конвейера: ветка, не тронувшая ни
59
+ # строки кода, не поднимает стенда, не снимает кадров и не собирает образов.
60
+ #
61
+ # Текстовой считается ветка, у которой каждый задетый файл — либо `.md`, либо лежит под каталогом
62
+ # текстов. Всё остальное, включая обвязку и настройки, считается кодом: признак обязан ошибаться
63
+ # в сторону лишнего прогона, а не пропущенного. Пустая база означает, что сравнивать не с чем, —
64
+ # тогда признак молчит и гоняется всё.
65
+ rt_push_docs_only_default() {
66
+ [ -z "$1" ] && return 1
67
+ changed="$(git diff --name-only "$1"...HEAD 2>/dev/null)"
68
+ [ -z "$changed" ] && return 1
69
+ printf '%s\n' "$changed" | grep -qvE "(^${RT_DOCS_DIR:-docs}/|\.md\$)" && return 1
70
+
71
+ return 0
72
+ }
73
+
53
74
  rt_push_checks_default() {
54
75
  runner="$(rt_runner)"
55
76
  root="${CLAUDE_PROJECT_DIR:-.}"
@@ -66,7 +87,8 @@ rt_push_checks_default() {
66
87
  [ -x "$root/$RT_HOOKS_TESTS" ] && printf '%s\n' "bash $RT_HOOKS_TESTS"
67
88
 
68
89
  for check in check-doc-paths check-specs check-file-size check-dupes check-styles \
69
- check-lib-layers check-reuse check-schema-drift check-push-gate; do
90
+ check-lib-layers check-reuse check-schema-drift check-states check-state-next \
91
+ check-turn-map check-push-gate; do
70
92
  [ -f "$root/$RT_CHECKS_DIR/$check.mjs" ] && printf '%s\n' "node $RT_CHECKS_DIR/$check.mjs"
71
93
  done
72
94
 
@@ -101,6 +123,9 @@ rt_lint_for_default() {
101
123
  }
102
124
 
103
125
  # Каталог папок задач. Пусто — ведения работы папкой в дереве нет, и гард замысла молчит.
126
+ # Каталог текстов проекта: по нему признак состава правки отличает текстовую ветку от кодовой.
127
+ RT_DOCS_DIR="${RT_DOCS_DIR:-docs}"
128
+
104
129
  RT_TASKS_DIR="${RT_TASKS_DIR:-docs/tasks}"
105
130
 
106
131
  # Каталог записей о законченных работах. Туда переносят то, что объясняет решения закрытой
@@ -111,9 +136,27 @@ RT_ARCHIVE_DIR="${RT_ARCHIVE_DIR:-docs/archive}"
111
136
  # Размер окна захода в токенах и пороги стража. Пусто — стража нет: считать долю не от чего, а
112
137
  # выведенный из записи захода размер врал бы — модель записана там без пометки о расширенном
113
138
  # окне. Дерево задаёт его в настройке агента, переменной окружения того же имени.
139
+ #
140
+ # Теми же двумя числами задаётся порог, на котором инструмент сжимает контекст сам, — а значит и
141
+ # порог, на котором пишется передача захода. В настройке агента им отвечает пара: размер окна
142
+ # автосжатия и доля в процентах, при которой оно приходит. Разъехавшись, они дают заход, который
143
+ # либо сжимается до того, как передача написана, либо доживает до предела окна. Сведены они или
144
+ # нет — говорит разбор состояния раскладки; здесь у пакета своих значений нет, потому что окно
145
+ # принадлежит дереву, а не ему.
146
+ #
147
+ # Сведёнными считаются не совпавшие числа, а разведённые. Порог сжатия обязан стоять НИЖЕ порога
148
+ # остановки: первым срабатывает то, что заход продолжает, а не то, что его останавливает.
149
+ # Совпавшая пара — гонка, и выигрывает её страж: он стоит на вызове инструмента, а сжатие
150
+ # приходит между ходами. Ровно так заход и вставал на пороге вместо того, чтобы продолжиться
151
+ # сжатым, — при том что сверка обе стороны считала настроенными.
152
+ #
153
+ # Насколько ниже — своё число дерева: сжатие идёт не мгновенно, и разница в один процент
154
+ # требование «ниже» удовлетворяет, а работу не спасает. Запас объявляется, а не выводится
155
+ # разницей.
114
156
  RT_WINDOW_TOKENS="${RT_WINDOW_TOKENS:-}"
115
157
  RT_WINDOW_WARN_PCT="${RT_WINDOW_WARN_PCT:-40}"
116
158
  RT_WINDOW_STOP_PCT="${RT_WINDOW_STOP_PCT:-50}"
159
+ RT_WINDOW_MARGIN_PCT="${RT_WINDOW_MARGIN_PCT:-5}"
117
160
 
118
161
  # Куда кладётся передача захода. Вне дерева: состояние работы живёт в ходе работы и коммитится,
119
162
  # а передача его пересказывает для вставки в новый заход и в историю не едет.
@@ -144,6 +187,11 @@ RT_SPECS_DIR="${RT_SPECS_DIR:-docs/specs}"
144
187
  # папкой задачи и с архивом. Если общего предка нет, сравнивать не с чем — проверка молчит.
145
188
  RT_MAIN_BRANCH="${RT_MAIN_BRANCH:-main}"
146
189
 
190
+ # Имя клиента хостинга в строке запуска. По нему гард перезапуска узнаёт вызов: у каждого вида
191
+ # хостинга свой клиент, и угаданное имя не совпадает ни с чем. Умолчание записано под самый
192
+ # распространённый вид; дерево с другим хостингом называет своё.
193
+ RT_HOST_CLI="${RT_HOST_CLI:-gh}"
194
+
147
195
  # Тело PR по его номеру. Обход требования пишут в PR, а в команде слияния его нет — там только
148
196
  # номер. Пустой ответ значит «спросить не у кого»: тогда обход ищется только в тексте команды.
149
197
  rt_report_body_default() {
@@ -185,19 +233,52 @@ rt_is_app_code_default() {
185
233
  # Список намеренно широк, и цена этого названа: команда чтения, в которой стоит имя
186
234
  # интерпретатора, будет отбита наравне с командой правки. Узкий список стоил бы дороже —
187
235
  # пропущенная форма записи возвращает обход целиком, а найти её можно только промахом.
236
+ #
237
+ # Перенаправление в пустое устройство и в поток ошибок снимается до разбора: файла оно не
238
+ # пишет, а выглядит как перенаправление в файл. Так глушат вывод команды чтения, и без этого
239
+ # `grep -rn x libs/ 2>/dev/null` судится наравне с записью — правило требуется на чтение, а
240
+ # отбитий, пришедшихся не на правку файла, набирается большинство. Настоящая запись рядом с
241
+ # заглушённым потоком остаётся видной: снимается перенаправление, а не команда целиком.
188
242
  rt_shell_writes_default() {
189
- printf '%s' "$1" | grep -Eq \
190
- '>>?[[:space:]]*[^|&>[:space:]]|\btee\b|\bsed\b[^|]*-i|\bperl\b[^|]*-i|\bpython3?\b|\bnode\b|\bruby\b|\bdd\b[^|]*of=|\bcp\b|\bmv\b|\brm\b|\btouch\b|\btruncate\b|\binstall\b|\bpatch\b|\bgit[[:space:]]+(checkout|restore|apply|stash)\b'
243
+ printf '%s' "$1" \
244
+ | sed -E 's#(&|[0-9]*)>>?[[:space:]]*/dev/(null|stderr)##g; s#[0-9]*>&[0-9-]##g' \
245
+ | grep -Eq \
246
+ '>>?[[:space:]]*[^|&>[:space:]]|\btee\b|\bsed\b[^|]*-i|\bperl\b[^|]*-i|\bpython3?\b|\bnode\b|\bruby\b|\bdd\b[^|]*of=|\bcp\b|\bmv\b|\brm\b|\btouch\b|\btruncate\b|\binstall\b|\bpatch\b|\bgit[[:space:]]+(checkout|restore|apply|stash)\b'
191
247
  }
192
248
 
193
249
  # Пути, названные командой оболочки. Печатает по одному в строке; судит их зовущий.
194
250
  #
195
251
  # Разбирать оболочку по-настоящему нечем — здесь и не разбирают: из текста вынимается всё, что
196
252
  # похоже на путь, и каждое отдаётся признаку. Лишнее он отсеет сам, а пропущенное вернуло бы
197
- # обход. Кавычки и heredoc снимаются заменой на пробел: путь внутри них тот же самый.
253
+ # обход. Кавычки снимаются заменой на пробел: путь внутри них тот же самый.
254
+ #
255
+ # Тело документа на месте путей не даёт: там лежит текст, который команда кладёт в файл, а
256
+ # чужой путь, названный в нём словами, требовал бы правила под запись, которой нет. Запись
257
+ # замысла так отбивалась трижды подряд, пока пути под каталогом кода не были названы иначе.
258
+ # Судится заголовок команды — именно в нём стоит тот путь, куда команда пишет.
259
+ #
260
+ # Исключение — интерпретатор: ему код приходит телом, и путь записи стоит именно там. Признак
261
+ # ошибается в сторону лишнего чтения тела: имя интерпретатора, стоящее в команде где угодно,
262
+ # возвращает разбор тела целиком.
198
263
  rt_shell_paths_default() {
199
- printf '%s' "$1" \
200
- | tr "\"'\`(),;=" ' ' \
264
+ text="$(printf '%s' "$1" | tr "\"'\`" ' ')"
265
+ if ! printf '%s' "$text" \
266
+ | grep -Eq '(^|[|;&(]|[[:space:]])(python3?|node|ruby|perl|php|deno|bun|bash|sh|zsh)([[:space:]]|$)'; then
267
+ text="$(printf '%s' "$text" | awk '
268
+ function trim(s) { sub(/^[ \t]+/, "", s); sub(/[ \t]+$/, "", s); return s }
269
+ tag != "" { if (trim($0) == tag) tag = ""; next }
270
+ {
271
+ print
272
+ if (match($0, /<<-?[ \t]*[A-Za-z_][A-Za-z0-9_]*/)) {
273
+ t = substr($0, RSTART, RLENGTH)
274
+ sub(/^<<-?[ \t]*/, "", t)
275
+ tag = t
276
+ }
277
+ }')"
278
+ fi
279
+
280
+ printf '%s' "$text" \
281
+ | tr "(),;=" ' ' \
201
282
  | tr '[:space:]' '\n' \
202
283
  | grep -E '^[A-Za-z0-9_@.-]*/[A-Za-z0-9_@./-]+$' \
203
284
  | sed 's|^\./||' \
@@ -239,6 +320,12 @@ RT_BOARD_CHECK_CMD="${RT_BOARD_CHECK_CMD:-npm run check:board}"
239
320
  # Учётная запись, которую ставят исполнителем. Умолчание молчит: у каждого дерева она своя.
240
321
  RT_TASK_BOT="${RT_TASK_BOT:-}"
241
322
 
323
+ # Команда перевода задачи между колонками очереди работ и имя первой колонки — той, из которой
324
+ # задача уходит, когда её берут в работу. Имя без умолчания: колонки дерево называет своими
325
+ # словами, а выдуманное не совпало бы ни с чем и молча выключило бы проверку колонки.
326
+ RT_TASK_MOVE_CMD="${RT_TASK_MOVE_CMD:-npm run task:move}"
327
+ RT_BOARD_BACKLOG="${RT_BOARD_BACKLOG:-}"
328
+
242
329
  # Почта, которой подписан коммит машинной учётной записи. Целым значением, а не образцом:
243
330
  # служебный адрес хостинга состоит из числа, логина и домена, а сопоставляется по числу — логин
244
331
  # рядом с ним не сверяет никто. Образец «число, плюс, логин» прошёл бы с чужим числом, то есть
@@ -268,6 +355,18 @@ rt_task_state_default() {
268
355
  printf '%s' "$state"
269
356
  }
270
357
 
358
+ # Состояние заявки одним объектом: exists, draft, author, reviewers, reviewed. Спрашивает того
359
+ # же помощника очереди работ, что и состояние задачи, — чтобы гард и сверка одинаково понимали
360
+ # «у заявки есть разбор». Нет узла, нет помощника, нет сети — молчание, и ярус пропускается.
361
+ rt_pull_state_default() {
362
+ command -v node >/dev/null 2>&1 || return 1
363
+ [ -f "${RT_BOARD_HELPER:-tools/board.mjs}" ] || return 1
364
+ state="$(node "${RT_BOARD_HELPER:-tools/board.mjs}" pr "$1" 2>/dev/null)" || return 1
365
+ [ -z "$state" ] && return 1
366
+ printf '%s' "$state" | jq -e 'has("offline") | not' >/dev/null 2>&1 || return 1
367
+ printf '%s' "$state"
368
+ }
369
+
271
370
  # Что в этом дереве считается переизобретением. По строке «образец<таб>чем заменить».
272
371
  # Образцы узкие намеренно: гард сверяет только НОВЫЙ текст, и широкий образец отбивал бы
273
372
  # правку, которая ничего нового не заводит.
@@ -306,6 +405,8 @@ rt_qa_decorative_default() {
306
405
  # Умолчания под общими именами. Надстройка проекта объявит любое из них заново — и позовёт
307
406
  # отсюда то же имя с суффиксом `_default` для всего, чего не назвала сама.
308
407
  rt_push_checks() { rt_push_checks_default "$@"; }
408
+ rt_push_docs_only() { rt_push_docs_only_default "$@"; }
409
+
309
410
  rt_docs_pair_for() { rt_docs_pair_for_default "$@"; }
310
411
  rt_lint_for() { rt_lint_for_default "$@"; }
311
412
  rt_task_branch_ok() { rt_task_branch_ok_default "$@"; }
@@ -315,5 +416,6 @@ rt_shell_writes() { rt_shell_writes_default "$@"; }
315
416
  rt_shell_paths() { rt_shell_paths_default "$@"; }
316
417
  rt_qa_decorative() { rt_qa_decorative_default "$@"; }
317
418
  rt_task_state() { rt_task_state_default "$@"; }
419
+ rt_pull_state() { rt_pull_state_default "$@"; }
318
420
  rt_report_body() { rt_report_body_default "$@"; }
319
421
  rt_handoff_allowed_cmd() { rt_handoff_allowed_cmd_default "$@"; }
@@ -0,0 +1,46 @@
1
+ # Карта хода
2
+
3
+ Это не правило, а его короткая выжимка: правило объясняет, карта называет. Полный текст —
4
+ скил `task-flow`; он же называет паттерн, который ведёт каждое состояние.
5
+
6
+ Состояние работы объявлено строкой в разделе «Где стоим» хода работы. Пока обязательное
7
+ действие не сделано, работа стоит в том же состоянии.
8
+
9
+ ## Состояния и обязательные действия
10
+
11
+ | Состояние | Обязательное действие | Ведёт паттерн |
12
+ | ------------------------- | ------------------------------------------------------ | ------------------ |
13
+ | `просьба-не-разобрана` | разведка по дереву, затем вопросы | `task-flow-start` |
14
+ | `разбор-закрыт` | договорённость о продукте либо причина её отсутствия | `task-flow-start` |
15
+ | `договорённость-записана` | завести задачу, ветку и папку | `task-flow-start` |
16
+ | `задача-взята` | написать замысел | `task-flow-start` |
17
+ | `замысел-записан` | делать первый этап | `task-flow-start` |
18
+ | `этап-идёт` | доделать этап и отметить в ходе работы | `task-flow-resume` |
19
+ | `этапы-кончились` | прогнать набор и открыть PR черновиком | `task-flow-close` |
20
+ | `работа-отдана` | взять следующую задачу | `task-flow-resume` |
21
+ | `разбор-кончился` | влить договорённость, привести тексты, разобрать папку | `task-flow-close` |
22
+ | `папка-разобрана` | снять черновик и попросить влить | `task-flow-close` |
23
+ | `влито` | разбор работы правилами и сверка очереди | `task-flow-close` |
24
+
25
+ Ни у одного состояния обязательное действие не звучит как «ждать». Прогон, разбор владельцем и
26
+ слияние идут без исполнителя и от взгляда быстрее не становятся.
27
+
28
+ ## Чем ход кончается
29
+
30
+ Способов четыре, и других нет.
31
+
32
+ | Выход | Чем подтверждается |
33
+ | -------------------------------------------------- | --------------------------------------------------------------- |
34
+ | вопрос владельцу, ответа на который в правилах нет | вопрос задан, и за тот же ход правила читались |
35
+ | отказ гарда | отказ назван владельцу, обход не искался |
36
+ | заполненное окно там, где сжатия нет | ход работы дописан, передача написана |
37
+ | работа отдана, и следующая начата | PR открыт, и по следующей задаче сделано действие, а не сказано |
38
+
39
+ Там, где дерево объявило порог сжатия ниже порога остановки, заполненное окно ход не кончает:
40
+ контекст сжимается, передача приходит входом, и работа идёт дальше тем же заходом. Порог
41
+ остановки там — страховка на случай, когда сжатие не пришло.
42
+
43
+ Всё остальное — продолжение хода. Ходом не кончаются: коммит, записанный замысел, закрытый
44
+ разбор просьбы, прочитанная договорённость, зелёная проверка, сводка о чужом шаге и объявление
45
+ намерения. Переход из состояния в состояние — тем более: обязательное действие сделано, и
46
+ следующее делается тем же ходом. Названо может быть только сделанное.
@@ -30,6 +30,7 @@
30
30
  | Раскладка | Перенос ресурса из пакета в дерево по его роду и настройке слоя |
31
31
  | Разложенный файл | Файл в дереве с шапкой пакета. Правится не на месте, а надстройкой: правка на месте теряется на следующей раскладке |
32
32
  | Надстройка | Файл дерева, который сливается с разложенным по заголовкам разделов |
33
+ | Выключенная роль | Роль, вызов которой дерево перестало считать обязательным: гард при ней молчит. Названа списком в настройке дерева, из раскладки не убирается и зовётся руками |
33
34
  | Наблюдение | Строка о событии слоя правил: правило загружено, гейт отбил, гард отказал. Пишет гард, живёт в дереве, наружу уезжает счётчиками. Журналом не называется |
34
35
  | Сводка | Что наблюдения говорят за отрезок дней: чем пользовались, чем ни разу, обо что спотыкались |
35
36
  | Предложение | Готовая формулировка правки правил с адресом: пакет, компаньон или дерево. Приносит разбор закрытой задачи или слово посреди работы, отправляет человек командой |
@@ -37,21 +38,22 @@
37
38
 
38
39
  ## Работа
39
40
 
40
- | Термин | Что это |
41
- | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
42
- | Задача | Единица работы в очереди работ. Заводится до ветки, и номер её стоит в имени ветки и в заголовке PR |
43
- | PR | Заявка на слияние: то же название, что у задачи, переведённое в сделанное. Отчётом, пул-реквестом и мёрдж-реквестом не называется — ни в файлах, ни в разговоре |
44
- | Очередь работ | Доска, на которой видно состояние каждой задачи. Ветки она не видит |
45
- | Папка задачи | Одна работа от разбора до слияния: разбор просьбы, замысел, ход работы. Умирает со слиянием — разбирается, и объясняющее решение уезжает в архив |
46
- | Разбор | Расспрос владельца до первой правки. Записывается его словами и задним числом не переписывается |
47
- | Замысел | Файл папки задачи: след задачи и этапы с признаками готовности. После написания не правится — с ним сверяют результат при приёмке |
48
- | Ход работы | Файл папки задачи: «Где стоим», решения по ходу с причинами, записи заходов. Единственное место, где отмечается сделанное. Журналом не называется |
49
- | След задачи | Раздел замысла: какие спеки, законы, правила и части кода работа задевает |
50
- | Заход | Одна сессия работы над задачей. Работа живёт дольше одного захода, и между ними её состояние держит только ход работы |
51
- | Заполнение окна | Доля места захода, которую он уже занял: вход, запись в кэш, прочитанное из кэша и вывод последнего ответа, делённые на размер окна. Не «расход» и не «бюджет»: речь о месте, а не о деньгах |
52
- | Передача | Текст, которым заход закрывается: рабочее дерево, ветка, задача, где лежит ход работы, что сделано, следующий шаг, особенности захода. Кладётся вне дерева и не коммитится |
53
- | Эпик | Серия задач одной темы, выполняемых в назначенном порядке. Живёт в двух местах сразу: карточка в очереди работ с меткой эпика и замысел рядом с ней — что за возможность разрабатывается, какие задачи входят и в каком порядке. Шире одной ветки. Линией работ не называется |
54
- | Архив | Записи о состоявшемся: что объясняет закрытое решение. После выкатки не правится |
41
+ | Термин | Что это |
42
+ | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
43
+ | Задача | Единица работы в очереди работ. Заводится до ветки, и номер её стоит в имени ветки и в заголовке PR |
44
+ | PR | Заявка на слияние: то же название, что у задачи, переведённое в сделанное. Отчётом, пул-реквестом и мёрдж-реквестом не называется — ни в файлах, ни в разговоре |
45
+ | Очередь работ | Доска, на которой видно состояние каждой задачи. Ветки она не видит |
46
+ | Папка задачи | Одна работа от разбора до слияния: разбор просьбы, замысел, ход работы. Умирает со слиянием — разбирается, и объясняющее решение уезжает в архив |
47
+ | Разбор | Расспрос владельца до первой правки. Записывается его словами и задним числом не переписывается |
48
+ | Замысел | Файл папки задачи: след задачи и этапы с признаками готовности. После написания не правится — с ним сверяют результат при приёмке |
49
+ | Ход работы | Файл папки задачи: «Где стоим», решения по ходу с причинами, записи заходов. Единственное место, где отмечается сделанное. Журналом не называется |
50
+ | Состояние работы | Единица, которой работа ведётся: у каждого состояния названы вход, обязательное действие и выход. Объявляется машиночитаемой строкой в разделе «Где стоим» хода работы — гард судит объявленный переход, а не наличие файлов |
51
+ | След задачи | Раздел замысла: какие спеки, законы, правила и части кода работа задевает |
52
+ | Заход | Одна сессия работы над задачей. Работа живёт дольше одного захода, и между ними её состояние держит только ход работы |
53
+ | Заполнение окна | Доля места захода, которую он уже занял: вход, запись в кэш, прочитанное из кэша и вывод последнего ответа, делённые на размер окна. Не «расход» и не «бюджет»: речь о месте, а не о деньгах |
54
+ | Передача | Текст, которым заход закрывается: рабочее дерево, ветка, задача, где лежит ход работы, что сделано, следующий шаг, особенности захода. Кладётся вне дерева и не коммитится |
55
+ | Эпик | Серия задач одной темы, выполняемых в назначенном порядке. Живёт в двух местах сразу: карточка в очереди работ с меткой эпика и замысел рядом с ней — что за возможность разрабатывается, какие задачи входят и в каком порядке. Шире одной ветки. Линией работ не называется |
56
+ | Архив | Записи о состоявшемся: что объясняет закрытое решение. После выкатки не правится |
55
57
 
56
58
  ## Проверки
57
59
 
@@ -8,6 +8,8 @@
8
8
  #
9
9
  # Файл с идентификатором в репозиторий не коммитится: у каждой машины он свой.
10
10
 
11
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
12
+
11
13
  if [ -n "${RT_BROWSER_DEVICE_ID:-}" ]; then
12
14
  printf '%s\n' "$RT_BROWSER_DEVICE_ID"
13
15
  exit 0
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env bash
2
2
  # rt-hook: PreToolUse mcp__claude-in-chrome__select_browser
3
+ # Требует: hooks/deny-tail.sh
3
4
  # Гард выбора браузера. PreToolUse на выборе браузера расширением.
4
5
  #
5
6
  # Отклоняет любой профиль, кроме закреплённого: чужой стоит лишнего круга и приводит в браузер,
@@ -10,6 +11,8 @@
10
11
  #
11
12
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: помощник не назвал профиль — пропуск.
12
13
 
14
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
15
+
13
16
  input="$(cat 2>/dev/null)"
14
17
 
15
18
  device_id="$("${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/browser-device-id.sh" 2>/dev/null)"
@@ -24,5 +27,12 @@ if [ "$requested" = "$device_id" ]; then
24
27
  exit 0
25
28
  fi
26
29
 
27
- echo "Профиль «${requested}» не тот, что закреплён за проектом. Бери ${device_id} единственный профиль, где сделан вход." >&2
30
+ # Общий хвост отказа: два законных хода и законная форма обхода, если она у отказа есть. Файл
31
+ # может быть не разложен — тогда хвоста нет, а причина отказа остаётся прежней.
32
+ # shellcheck disable=SC1090
33
+ [ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" ] \
34
+ && . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" 2>/dev/null
35
+ command -v rt_deny_tail >/dev/null 2>&1 || rt_deny_tail() { :; }
36
+
37
+ echo "Профиль «${requested}» не тот, что закреплён за проектом. Бери ${device_id} — единственный профиль, где сделан вход. $(rt_deny_tail)" >&2
28
38
  exit 2
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env bash
2
2
  # rt-hook: PreToolUse AskUserQuestion
3
+ # Требует: hooks/deny-tail.sh
3
4
  # PreToolUse guard for AskUserQuestion.
4
5
  #
5
6
  # The browser choice is already made and pinned in browser-device-id.sh — asking again is noise, and the
@@ -13,6 +14,8 @@
13
14
  #
14
15
  # FAIL-OPEN when browser-device-id.sh prints nothing.
15
16
 
17
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
18
+
16
19
  input="$(cat 2>/dev/null)"
17
20
 
18
21
  device_id="$("${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/browser-device-id.sh" 2>/dev/null)"
@@ -23,5 +26,12 @@ questions="$(printf '%s' "$input" | jq -r '[.tool_input.questions[]? | .question
23
26
 
24
27
  printf '%s' "$questions" | grep -qiE 'deviceid|device id|(which|what|pick|choose|select)[^.]{0,25}\bbrowser\b|\bbrowser\b[^.]{0,25}(profile|to use)|как(ой|ую)?[^.]{0,15}браузер|брауз[а-я]*[^.]{0,20}(использовать|выбрать|выбор|нужен|запустить)' || exit 0
25
28
 
26
- echo "Do not ask which browser to use the profile is pinned. Call select_browser with deviceId ${device_id} ('Main' profile)." >&2
29
+ # Общий хвост отказа: два законных хода и законная форма обхода, если она у отказа есть. Файл
30
+ # может быть не разложен — тогда хвоста нет, а причина отказа остаётся прежней.
31
+ # shellcheck disable=SC1090
32
+ [ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" ] \
33
+ && . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" 2>/dev/null
34
+ command -v rt_deny_tail >/dev/null 2>&1 || rt_deny_tail() { :; }
35
+
36
+ echo "Do not ask which browser to use — the profile is pinned. Call select_browser with deviceId ${device_id} ('Main' profile). $(rt_deny_tail)" >&2
27
37
  exit 2
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env bash
2
2
  # rt-hook: PreToolUse mcp__claude-in-chrome__(list_connected_browsers|switch_browser)
3
+ # Требует: hooks/deny-tail.sh
3
4
  # Гард перечисления и переключения браузеров. PreToolUse.
4
5
  #
5
6
  # Сессии проекта живут в одном закреплённом профиле. Перечисление и переключение отдают общие
@@ -9,10 +10,19 @@
9
10
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: помощник не назвал профиль — пропуск. Гард, который не может назвать
10
11
  # нужный профиль, ничего не предлагает взамен, и слепой отказ только заводил бы работу в тупик.
11
12
 
13
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
14
+
12
15
  cat >/dev/null 2>&1
13
16
 
14
17
  device_id="$("${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/browser-device-id.sh" 2>/dev/null)"
15
18
  [ -z "$device_id" ] && exit 0
16
19
 
17
- echo "Не перечисляй и не переключай браузеры. Вызови выбор браузера с профилем ${device_id} единственным, где сделан вход." >&2
20
+ # Общий хвост отказа: два законных хода и законная форма обхода, если она у отказа есть. Файл
21
+ # может быть не разложен — тогда хвоста нет, а причина отказа остаётся прежней.
22
+ # shellcheck disable=SC1090
23
+ [ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" ] \
24
+ && . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" 2>/dev/null
25
+ command -v rt_deny_tail >/dev/null 2>&1 || rt_deny_tail() { :; }
26
+
27
+ echo "Не перечисляй и не переключай браузеры. Вызови выбор браузера с профилем ${device_id} — единственным, где сделан вход. $(rt_deny_tail)" >&2
18
28
  exit 2
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env bash
2
2
  # rt-hook: PreToolUse mcp__playwright__.*|mcp__chrome-devtools__.*|Bash
3
+ # Требует: hooks/deny-tail.sh
3
4
  # Гард обходных путей к браузеру. PreToolUse.
4
5
  #
5
6
  # Закрепление профиля чего-то стоит только тогда, когда дверь одна. Здесь перечислены двери,
@@ -12,6 +13,8 @@
12
13
  #
13
14
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: помощник не назвал профиль — пропуск.
14
15
 
16
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
17
+
15
18
  input="$(cat 2>/dev/null)"
16
19
 
17
20
  device_id="$("${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/browser-device-id.sh" 2>/dev/null)"
@@ -19,8 +22,13 @@ device_id="$("${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/browser-device-id.sh" 2>/de
19
22
 
20
23
  tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
21
24
 
25
+ # shellcheck disable=SC1090
26
+ [ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" ] \
27
+ && . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" 2>/dev/null
28
+ command -v rt_deny_tail >/dev/null 2>&1 || rt_deny_tail() { :; }
29
+
22
30
  deny() {
23
- echo "$1 Води браузер закреплённым расширением: выбери профиль ${device_id} и работай его инструментами." >&2
31
+ echo "$1 Води браузер закреплённым расширением: выбери профиль ${device_id} и работай его инструментами. $(rt_deny_tail)" >&2
24
32
  exit 2
25
33
  }
26
34
 
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env bash
2
2
  # rt-hook: PreToolUse mcp__claude-in-chrome__.*
3
+ # Требует: hooks/deny-tail.sh
3
4
  # Гард свежести выбора браузера. PreToolUse на всех остальных вызовах расширения.
4
5
  #
5
6
  # ЗАЧЕМ ОН ЕСТЬ — отказ, из которого он вырос: расширение действует на тот браузер, который
@@ -19,6 +20,8 @@
19
20
  #
20
21
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: помощник не назвал профиль — пропуск.
21
22
 
23
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
24
+
22
25
  input="$(cat 2>/dev/null)"
23
26
 
24
27
  device_id="$("${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/browser-device-id.sh" 2>/dev/null)"
@@ -35,8 +38,16 @@ ttl=300
35
38
  sid="$(printf '%s' "$input" | jq -r '.session_id // "nosession"' 2>/dev/null)"
36
39
  marker="${TMPDIR:-/tmp}/claude-browser-guard/${sid}"
37
40
 
41
+ # Общий хвост отказа: два законных хода и законная форма обхода, если она у отказа есть. Файл
42
+ # может быть не разложен — тогда хвоста нет, а причина отказа остаётся прежней.
43
+ # shellcheck disable=SC1090
44
+ [ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" ] \
45
+ && . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" 2>/dev/null
46
+ command -v rt_deny_tail >/dev/null 2>&1 || rt_deny_tail() { :; }
47
+
48
+
38
49
  if [ ! -f "$marker" ]; then
39
- echo "В этой сессии браузер не выбран. Вызови выбор браузера с профилем ${device_id} до любого другого вызова." >&2
50
+ echo "В этой сессии браузер не выбран. Вызови выбор браузера с профилем ${device_id} до любого другого вызова. $(rt_deny_tail)" >&2
40
51
  exit 2
41
52
  fi
42
53
 
@@ -46,7 +57,7 @@ age=$(( now - stamped ))
46
57
 
47
58
  if [ "$age" -gt "$ttl" ]; then
48
59
  rm -f "$marker" 2>/dev/null
49
- echo "Последний выбор браузера был ${age} с назад (предел ${ttl} с) — на таких перерывах активный браузер расширения уплывает, и это может быть уже не закреплённый профиль. Вызови выбор с профилем ${device_id} заново и повтори." >&2
60
+ echo "Последний выбор браузера был ${age} с назад (предел ${ttl} с) — на таких перерывах активный браузер расширения уплывает, и это может быть уже не закреплённый профиль. Вызови выбор с профилем ${device_id} заново и повтори. $(rt_deny_tail)" >&2
50
61
  exit 2
51
62
  fi
52
63
 
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env bash
2
+ # rt-hook: Stop
3
+ # Гард утверждения: сказанное владельцу о состоянии дерева несёт команду, показавшую это. Stop.
4
+ #
5
+ # Зачем именно так. Правило текстов само говорит, что ответ владельцу не читает ни одна
6
+ # проверка: за файл отвечает гейт, за ответ — автор, и цену ошибки в ответе платит владелец.
7
+ # Восемь разборов подряд про одно и то же — готовым названо лежащее на диске, снятыми названы
8
+ # только локальные ветки, проверенным назван набор уже конвейерного, состоянием дерева названа
9
+ # отставшая локальная ссылка. Каждый раз в правило дописывалась статья, и каждый раз промах
10
+ # повторялся: текст читают в начале захода, а утверждение говорят в конце.
11
+ #
12
+ # Что судится: текст, сказанный владельцу за этот ход. Вывод инструмента и записанное в файл
13
+ # сюда не входят — их читают гейт и сверки.
14
+ #
15
+ # Как судится: у каждого слова-утверждения назван свой род команды. Утверждение о зелёном
16
+ # наборе подтверждает прогон набора, о запушенном — вызов пуша, о снятом — вызов удаления.
17
+ # Команда ищется в этом же ходе: состояние дерева меняется, и вывод прошлого хода о нынешнем
18
+ # уже не говорит.
19
+ #
20
+ # Чего гард не судит. Неверный вывод: об образце, судимом по одному его файлу, и о пути, которым
21
+ # человек не пойдёт, машине судить нечем — там нет ни слова-утверждения, ни команды, с которой
22
+ # сверять. Эти случаи держит правило ведения работы, и это известная граница гарда, а не
23
+ # обещание.
24
+ #
25
+ # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: при любой ошибке, нехватке `jq`, отсутствии записи хода или пустом
26
+ # тексте ход РАЗРЕШАЕТСЯ (exit 0). Сломанный гард не имеет права заклинить разговор.
27
+
28
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
29
+
30
+ input="$(cat 2>/dev/null)"
31
+ [ -z "$input" ] && exit 0
32
+ command -v jq >/dev/null 2>&1 || exit 0
33
+
34
+ # Повторный заход по тому же ходу не судится: гард сказал своё один раз и отпускает.
35
+ active="$(printf '%s' "$input" | jq -r '.stop_hook_active // false' 2>/dev/null)"
36
+ [ "$active" = "true" ] && exit 0
37
+
38
+ transcript="$(printf '%s' "$input" | jq -r '.transcript_path // empty' 2>/dev/null)"
39
+ [ -z "$transcript" ] && exit 0
40
+ [ -f "$transcript" ] || exit 0
41
+
42
+ # Ход — всё, что записано после последней настоящей реплики владельца: ответ инструмента
43
+ # приходит той же ролью и репликой не считается.
44
+ turn="$(tail -n 400 "$transcript" 2>/dev/null | jq -s -r '
45
+ def is_input:
46
+ .type == "user"
47
+ and (((.message.content // []) | if type == "array"
48
+ then ([.[] | select(.type == "tool_result")] | length)
49
+ else 0 end) == 0);
50
+
51
+ (map(is_input) | rindex(true)) as $i
52
+ | (if $i == null then [] else .[$i:] end) as $turn
53
+ | [$turn[] | select(.type == "assistant") | (.message.content // [])[]
54
+ | select(.type == "text") | (.text // "")] as $said
55
+ | [$turn[] | select(.type == "assistant") | (.message.content // [])[]
56
+ | select(.type == "tool_use") | (.input.command // "")] as $ran
57
+ | { said: ($said | join("\n")), ran: ($ran | join("\n")) }
58
+ ' 2>/dev/null)"
59
+
60
+ [ -z "$turn" ] && exit 0
61
+
62
+ said="$(printf '%s' "$turn" | jq -r '.said // ""' 2>/dev/null)"
63
+ ran="$(printf '%s' "$turn" | jq -r '.ran // ""' 2>/dev/null)"
64
+ [ -z "$said" ] && exit 0
65
+
66
+ # Карта утверждений: что сказано о дереве | чем это показывают | что назвать в отказе.
67
+ #
68
+ # Слово берётся в той форме, в какой его говорят владельцу. Будущее время сюда не идёт:
69
+ # «проверю» и «запущу» утверждениями не являются — они обещание, и врать им нечем.
70
+ claims=(
71
+ 'проверено|прогнал[а]?|тесты (зелёные|прошли)|линт(ер)? (зелёный|прошёл|чистый)|сборка (зелёная|прошла)|набор зелёный|проверки зелёные|спеки зелёные|всё зелен(о|ое)§nx (test|lint|build|run|affected|run-many)|npm (run|test)|pnpm (run|exec|test)|jest|vitest|playwright|check:|run\.sh§команду набора — прогон тестов, линта или сборки'
72
+ 'запушен[аоы]?|запушил[а]?|пуш прошёл|ветка уехала§git push§`git push`'
73
+ '(PR|правка|ветка|работа)[^.]{0,20}(влит|слит|смержен)|влит[оа] в|слит[оа] в§git merge|gh pr merge§`git merge` или `gh pr merge`'
74
+ '(ветки|ветка|файлы|файл|папка|каталог)[^.]{0,40}(снят|удал|почищ|вычищ)|снят[оыа] с§git branch|git push .*--delete|git rm|gh api|rm §команду удаления — `git branch -d`, `git push --delete` или `git rm`'
75
+ 'прогон (зелёный|прошёл|кончился)|конвейер зелёный|проверки на PR зелёные§gh run§`gh run list` или `gh run view`'
76
+ '(работа|правка|задача) готова|можно вливать|PR открыт|черновик снят§gh pr §`gh pr create`, `gh pr view` или `gh pr ready`'
77
+ 'задача заведена|задача (в|переведена в) колонк|колонка переведена§gh issue|gh api|task:new|task:move|board\.mjs§команду очереди работ — заведение задачи или перевод колонки'
78
+ '(в дереве|в репозитории|здесь|такого файла|такой команды)[^.]{0,30}(нет|не бывает)|не заводили|нигде не встречается§grep|rg |ls |find |git ls-files|git grep|git log|cat §команду поиска — `grep`, `git ls-files` или обход каталога'
79
+ )
80
+
81
+ for row in "${claims[@]}"; do
82
+ words="${row%%§*}"
83
+ rest="${row#*§}"
84
+ proof="${rest%%§*}"
85
+ name="${rest#*§}"
86
+
87
+ found="$(printf '%s' "$said" | grep -oiE "$words" 2>/dev/null | head -1)"
88
+ [ -z "$found" ] && continue
89
+ printf '%s' "$ran" | grep -qiE "$proof" 2>/dev/null && continue
90
+
91
+ reason="BLOCKED by claim-guard: владельцу сказано «${found}» — это утверждение о состоянии дерева, а команды, которая его показывает, за этот ход не было.
92
+
93
+ Утверждение о дереве стоит ровно столько, сколько команда, его показавшая: сказанное без команды владелец читает как проверенный факт и узнаёт о расхождении последним.
94
+
95
+ Запусти ${name} этим же ходом и назови её вывод — либо убери утверждение из ответа.
96
+
97
+ Гард судит один ход: следующий заход не отбивается."
98
+
99
+ # Общий хвост отказа: два законных хода. Файл может быть не разложен — тогда хвоста нет,
100
+ # а причина отказа остаётся прежней.
101
+ # shellcheck disable=SC1090
102
+ [ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" ] \
103
+ && . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" 2>/dev/null
104
+ command -v rt_deny_tail >/dev/null 2>&1 || rt_deny_tail() { :; }
105
+ deny_tail_text="$(rt_deny_tail "")"
106
+ [ -n "$deny_tail_text" ] && reason="${reason}
107
+
108
+ ${deny_tail_text}"
109
+
110
+ jq -n --arg r "$reason" '{decision:"block",reason:$r}' 2>/dev/null \
111
+ || printf '{"decision":"block","reason":"claim-guard: утверждение о дереве не подтверждено командой."}\n'
112
+ exit 0
113
+ done
114
+
115
+ exit 0