@rt-tools/agent-kit 0.16.0 → 0.17.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 (106) hide show
  1. package/assets/checks/board-paths.github.mjs +88 -0
  2. package/assets/checks/board-runs.github.mjs +8 -0
  3. package/assets/checks/board-titles.github.mjs +66 -0
  4. package/assets/checks/check-board.github.mjs +17 -4
  5. package/assets/checks/check-file-size.mjs +10 -2
  6. package/assets/checks/check-glossary.mjs +170 -0
  7. package/assets/checks/check-push-gate.mjs +59 -1
  8. package/assets/checks/check-schema-drift.mjs +12 -5
  9. package/assets/checks/spec-contract.mjs +9 -0
  10. package/assets/defaults/gate-map.sh +13 -4
  11. package/assets/defaults/project.sh +22 -3
  12. package/assets/docs/GLOSSARY.md +1 -1
  13. package/assets/hooks/browser-device-id.sh +20 -4
  14. package/assets/hooks/browser-guard-device-id.sh +5 -2
  15. package/assets/hooks/git-guard-delivery-folder.sh +19 -0
  16. package/assets/hooks/git-guard-delivery.sh +39 -0
  17. package/assets/hooks/git-guard-push-tests.sh +21 -1
  18. package/assets/hooks/grill-gate-ask.sh +25 -0
  19. package/assets/hooks/grill-gate.sh +12 -5
  20. package/assets/hooks/lint-after-edit.sh +44 -16
  21. package/assets/hooks/proposal-guard.sh +17 -1
  22. package/assets/hooks/skill-gate-layers.sh +7 -0
  23. package/assets/hooks/skill-gate.sh +29 -0
  24. package/assets/hooks/task-context-load.sh +10 -0
  25. package/assets/hooks/task-flow-context.sh +185 -0
  26. package/assets/hooks/task-flow-draft-guard.sh +92 -0
  27. package/assets/hooks/task-flow-guard.sh +27 -159
  28. package/assets/hooks/turn-exit-guard.sh +8 -1
  29. package/assets/hooks/window-fill-guard.sh +5 -1
  30. package/assets/laws/delivery.md +19 -0
  31. package/assets/laws/frontend-application.md +4 -0
  32. package/assets/laws/verifiability.md +21 -0
  33. package/assets/laws/work-conduct.md +15 -0
  34. package/assets/patterns/browser-verification-stand.md +7 -2
  35. package/assets/patterns/doc-style-write.md +41 -1
  36. package/assets/patterns/git-workflow-commit.github.md +15 -2
  37. package/assets/patterns/git-workflow-docker.md +14 -0
  38. package/assets/patterns/git-workflow-merge.md +18 -0
  39. package/assets/patterns/git-workflow-migration.md +11 -0
  40. package/assets/patterns/git-workflow-pr.github.md +6 -1
  41. package/assets/patterns/git-workflow-secrets.md +14 -0
  42. package/assets/patterns/git-workflow-stack.md +63 -1
  43. package/assets/patterns/spec-driven-domain.md +34 -1
  44. package/assets/patterns/spec-driven-rule.md +11 -0
  45. package/assets/patterns/spec-driven-sweep.md +57 -0
  46. package/assets/patterns/task-flow-archive.md +46 -14
  47. package/assets/patterns/task-flow-close.md +78 -2
  48. package/assets/patterns/task-flow-resume.md +17 -5
  49. package/assets/patterns/task-flow-start.md +42 -4
  50. package/assets/pitfalls/agent-kit.md +73 -3
  51. package/assets/pitfalls/doc-style.md +5 -0
  52. package/assets/pitfalls/git-workflow.github.md +68 -0
  53. package/assets/pitfalls/spec-driven.md +16 -0
  54. package/assets/pitfalls/task-flow.md +93 -0
  55. package/assets/pitfalls/turn-conduct.md +14 -0
  56. package/assets/rules/browser-verification.md +39 -0
  57. package/assets/rules/deploy-flow.azure.md +8 -0
  58. package/assets/rules/deploy-flow.github.md +18 -0
  59. package/assets/rules/deploy-flow.gitlab.md +8 -0
  60. package/assets/rules/doc-style.md +14 -0
  61. package/assets/rules/git-workflow.azure.md +5 -0
  62. package/assets/rules/git-workflow.github.md +85 -75
  63. package/assets/rules/git-workflow.gitlab.md +5 -0
  64. package/assets/rules/reuse-first.md +8 -0
  65. package/assets/rules/shared-code.md +5 -0
  66. package/assets/rules/spec-driven.md +14 -0
  67. package/assets/rules/task-flow.md +112 -112
  68. package/assets/rules/testing.md +14 -1
  69. package/assets/rules/turn-conduct.md +40 -27
  70. package/assets/rules/turn-entry.md +6 -0
  71. package/assets/samples/tasks/_template/grill.md +5 -0
  72. package/assets/samples/tasks/_template/plan.md +3 -0
  73. package/assets/skills/agent-kit.md +99 -76
  74. package/assets/templates/postmortem.md +5 -1
  75. package/bin/agent-kit.d.ts.map +1 -1
  76. package/bin/agent-kit.js +30 -6
  77. package/bin/agent-kit.js.map +1 -1
  78. package/lib/catalog.d.ts.map +1 -1
  79. package/lib/catalog.js +2 -1
  80. package/lib/catalog.js.map +1 -1
  81. package/lib/commands.d.ts.map +1 -1
  82. package/lib/commands.js +96 -7
  83. package/lib/commands.js.map +1 -1
  84. package/lib/hooks-map.d.ts +26 -0
  85. package/lib/hooks-map.d.ts.map +1 -1
  86. package/lib/hooks-map.js +58 -2
  87. package/lib/hooks-map.js.map +1 -1
  88. package/lib/sections.d.ts +6 -0
  89. package/lib/sections.d.ts.map +1 -1
  90. package/lib/sections.js +19 -0
  91. package/lib/sections.js.map +1 -1
  92. package/lib/shipment.d.ts +2 -0
  93. package/lib/shipment.d.ts.map +1 -1
  94. package/lib/shipment.fixture.d.ts +5 -0
  95. package/lib/shipment.fixture.d.ts.map +1 -1
  96. package/lib/shipment.fixture.js +7 -0
  97. package/lib/shipment.fixture.js.map +1 -1
  98. package/lib/shipment.js +13 -1
  99. package/lib/shipment.js.map +1 -1
  100. package/lib/sync.d.ts +35 -3
  101. package/lib/sync.d.ts.map +1 -1
  102. package/lib/sync.js +59 -8
  103. package/lib/sync.js.map +1 -1
  104. package/package.json +1 -1
  105. package/rt-tools-agent-kit-0.17.0.tgz +0 -0
  106. package/rt-tools-agent-kit-0.16.0.tgz +0 -0
@@ -87,7 +87,7 @@ rt_push_checks_default() {
87
87
  [ -x "$root/$RT_HOOKS_TESTS" ] && printf '%s\n' "bash $RT_HOOKS_TESTS"
88
88
 
89
89
  for check in check-doc-paths check-specs check-file-size check-dupes check-styles \
90
- check-lib-layers check-reuse check-schema-drift check-states check-state-next \
90
+ check-glossary check-lib-layers check-reuse check-schema-drift check-states check-state-next \
91
91
  check-turn-map check-archive-age check-push-gate; do
92
92
  [ -f "$root/$RT_CHECKS_DIR/$check.mjs" ] && printf '%s\n' "node $RT_CHECKS_DIR/$check.mjs"
93
93
  done
@@ -183,6 +183,9 @@ RT_LAWS_DIR="${RT_LAWS_DIR:-docs/constitution}"
183
183
  RT_RULES_DIR="${RT_RULES_DIR:-.claude/skills}"
184
184
  RT_SPECS_DIR="${RT_SPECS_DIR:-docs/specs}"
185
185
 
186
+ # Каталог замыслов, переживающих одну задачу: порядок задач эпика лежит там, а не в правилах.
187
+ RT_PLANS_DIR="${RT_PLANS_DIR:-docs/plans}"
188
+
186
189
  # Главная ветка. Гарду она нужна, чтобы найти общего предка и понять, что ветка сделала с
187
190
  # папкой задачи и с архивом. Если общего предка нет, сравнивать не с чем — проверка молчит.
188
191
  RT_MAIN_BRANCH="${RT_MAIN_BRANCH:-main}"
@@ -239,11 +242,21 @@ rt_is_app_code_default() {
239
242
  # `grep -rn x libs/ 2>/dev/null` судится наравне с записью — правило требуется на чтение, а
240
243
  # отбитий, пришедшихся не на правку файла, набирается большинство. Настоящая запись рядом с
241
244
  # заглушённым потоком остаётся видной: снимается перенаправление, а не команда целиком.
245
+ #
246
+ # Стрелка снимается там же и по той же причине. `->` и `=>` в оболочке не значат ничего, а знак
247
+ # в них тот же: команда, печатавшая таблицу «путь -> правило», объявлялась пишущей и отдавала
248
+ # все свои пути под суд. Туда же закрывающая скобка комментария разметки.
249
+ #
250
+ # Цель перенаправления сужена до знаков, из которых собирают пути. Строка цитаты разметки —
251
+ # знак и слово через пробел — от записи в файл одним знаком неотличима, и различает их только
252
+ # цель: за настоящим знаком стоит путь, а не слово словами. Цена названа прямо: путь, набранный
253
+ # не латиницей, записью больше не считается — в дереве таких нет ни одного, а появятся, признак
254
+ # придётся расширить.
242
255
  rt_shell_writes_default() {
243
256
  printf '%s' "$1" \
244
- | sed -E 's#(&|[0-9]*)>>?[[:space:]]*/dev/(null|stderr)##g; s#[0-9]*>&[0-9-]##g' \
257
+ | sed -E 's#(&|[0-9]*)>>?[[:space:]]*/dev/(null|stderr)##g; s#[0-9]*>&[0-9-]##g; s#[-=]+>##g' \
245
258
  | 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'
259
+ '>>?[[:space:]]*[A-Za-z0-9_./~$"'"'"'-]|\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'
247
260
  }
248
261
 
249
262
  # Пути, названные командой оболочки. Печатает по одному в строке; судит их зовущий.
@@ -372,6 +385,12 @@ RT_TASK_BOT="${RT_TASK_BOT:-}"
372
385
  RT_PULL_TOKEN_VAR="${RT_PULL_TOKEN_VAR:-}"
373
386
  RT_PULL_TOKEN_HINT="${RT_PULL_TOKEN_HINT:-}"
374
387
 
388
+ # Раздел, который тело заявки обязано нести с минуты открытия: решение о слиянии принимается на
389
+ # её странице, где переписки нет вовсе, и сказанного вслух там не остаётся. Умолчание молчит —
390
+ # заголовок пишется языком заявки, а чужих слов пакет не знает: не названный деревом, раздел не
391
+ # судится вовсе.
392
+ RT_PULL_BODY_SECTION="${RT_PULL_BODY_SECTION:-}"
393
+
375
394
  # Команда перевода задачи между колонками очереди работ и имя первой колонки — той, из которой
376
395
  # задача уходит, когда её берут в работу. Имя без умолчания: колонки дерево называет своими
377
396
  # словами, а выдуманное не совпало бы ни с чем и молча выключило бы проверку колонки.
@@ -73,4 +73,4 @@
73
73
  - **бэклог** — очередь работ
74
74
  - **линия работ** — эпик
75
75
  - **контекст-виндоу** — окно захода, а его доля — заполнение окна
76
- - **скилл, скилы** — правило, паттерн или скил без закона — по тому, что это на самом деле
76
+ - **скилл, скиллы** — правило, паттерн или скил без закона — по тому, что это на самом деле
@@ -1,10 +1,16 @@
1
1
  #!/usr/bin/env bash
2
+ # Местное значение: .claude/rt-kit/browser-device-id — без него браузерные гарды пропускают любой профиль
2
3
  # Общий помощник: печатает идентификатор закреплённого профиля браузера.
3
4
  #
4
5
  # Идентификатор локален для машины и в пакет не едет вовсе. Он берётся из переменной окружения,
5
- # а если её нет — из файла рядом с конфигом раскладки. Ни там ни там ничего нет — помощник
6
- # молчит, и все браузерные гарды пропускают: гард, который не может назвать нужный профиль,
7
- # ничего не предлагает взамен, и слепой отказ только заводил бы работу в тупик.
6
+ # а если её нет — из файла рядом с конфигом раскладки. Ни там ни там ничего нет — все браузерные
7
+ # гарды пропускают: гард, который не может назвать нужный профиль, ничего не предлагает взамен,
8
+ # и слепой отказ только заводил бы работу в тупик.
9
+ #
10
+ # Пропуск при этом не молчит. Ненастроенное дерево получает строку в поток ошибок — один раз на
11
+ # признак, чтобы она не тонула в каждом вызове: молчание тут неотличимо от «всё в порядке», и
12
+ # читается оно как разрешение водить браузер каким угодно профилем. Признак приходит первым
13
+ # доводом от того, кто зовёт; довода нет — метка дневная.
8
14
  #
9
15
  # Файл с идентификатором в репозиторий не коммитится: у каждой машины он свой.
10
16
 
@@ -16,7 +22,17 @@ if [ -n "${RT_BROWSER_DEVICE_ID:-}" ]; then
16
22
  fi
17
23
 
18
24
  file="${CLAUDE_PROJECT_DIR:-.}/.claude/rt-kit/browser-device-id"
19
- [ -f "$file" ] || exit 0
25
+
26
+ if [ ! -f "$file" ]; then
27
+ key="${1:-$(date +%Y%m%d 2>/dev/null || printf 'nokey')}"
28
+ marker_dir="${TMPDIR:-/tmp}/claude-browser-guard"
29
+ marker="$marker_dir/silent-$key"
30
+ if [ ! -f "$marker" ]; then
31
+ mkdir -p "$marker_dir" 2>/dev/null && : >"$marker" 2>/dev/null
32
+ echo "Профиль браузера этому дереву не назван: нет ни RT_BROWSER_DEVICE_ID, ни .claude/rt-kit/browser-device-id. Браузерные гарды поэтому пропускают всё подряд — это не разрешение ехать, а повод остановиться и сказать владельцу." >&2
33
+ fi
34
+ exit 0
35
+ fi
20
36
 
21
37
  tr -d '[:space:]' <"$file"
22
38
  printf '\n'
@@ -17,13 +17,16 @@
17
17
  rt_hook_read
18
18
  input="$RT_HOOK_INPUT"
19
19
 
20
- device_id="$("${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/browser-device-id.sh" 2>/dev/null)"
20
+ # Признак сессии помощнику передаётся: без него слово о ненастроенном дереве метится днём и
21
+ # приходит один раз в сутки, а не один раз за заход.
22
+ sid="$(printf '%s' "$input" | jq -r '.session_id // "nosession"' 2>/dev/null)"
23
+
24
+ device_id="$("${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/browser-device-id.sh" "$sid")"
21
25
  [ -z "$device_id" ] && exit 0
22
26
 
23
27
  requested="$(printf '%s' "$input" | jq -r '.tool_input.deviceId // empty' 2>/dev/null)"
24
28
 
25
29
  if [ "$requested" = "$device_id" ]; then
26
- sid="$(printf '%s' "$input" | jq -r '.session_id // "nosession"' 2>/dev/null)"
27
30
  marker_dir="${TMPDIR:-/tmp}/claude-browser-guard"
28
31
  mkdir -p "$marker_dir" 2>/dev/null && : >"$marker_dir/${sid}" 2>/dev/null
29
32
  exit 0
@@ -61,6 +61,25 @@ rt_delivery_open_folder() {
61
61
  && fault "папку задачи удалили, но в «${archive_dir}» ветка ничего не добавила. Удалить проще, чем разобрать, — и вместе с папкой пропадает разбор просьбы, единственная запись слов владельца. Перенеси то, что объясняет принятые решения, одним файлом с понятным именем и повтори."
62
62
  }
63
63
 
64
+ # Условие снятия черновика: тот же предмет между открытием и слиянием. Снятый черновик читается
65
+ # владельцем как приглашение влить, и кнопку он нажимает, не дожидаясь коммита уборки: гард
66
+ # слияния сюда не достаёт вовсе — нажимает человек на хостинге. Три работы подряд уехали в
67
+ # главную ветку именно так, и в теле каждой заявки стояло обещание убрать папку после одобрения.
68
+ #
69
+ # Ветку функция спрашивает сама: на этом ходе гард её ещё не посчитал.
70
+ rt_delivery_ready_folder() {
71
+ [ -n "$tasks_dir" ] || return 0
72
+ printf '%s' "$cmd" | grep -qiE "$folder_skip_re" && return 0
73
+
74
+ _branch="$(git branch --show-current 2>/dev/null)"
75
+ [ -z "$_branch" ] && return 0
76
+ rt_task_branch_ok "$_branch" || return 0 # за беззадачной веткой папки не стоит
77
+
78
+ _lying="$(rt_folder_in_branch "$tasks_dir/$_branch")"
79
+ [ -n "$_lying" ] \
80
+ && fault "в ветке лежит папка задачи «${_lying}» — снятый черновик читается как «можно вливать», а вливать её туда нельзя. Кнопку нажимает человек на хостинге, и до его руки папка уедет в главную. Перенеси в «${archive_dir:-архив}» то, что объясняет принятые решения, остальное удали, закоммить и повтори. Если работа вливается частями, поставь в команду комментарий «# Task-folder-skip: <причина>»."
81
+ }
82
+
64
83
  # Условие слияния: тот же предмет вторым рубежом. Он ловит слияние, идущее командой, — то, до
65
84
  # которого гард достаёт. Отсюда выходят только отказом или молчанием: слияние решается целиком.
66
85
  rt_delivery_merge_folder() {
@@ -107,6 +107,11 @@ task_bot="${RT_TASK_BOT:-}"
107
107
  # у него может не быть отдельной машинной записи.
108
108
  pull_token_var="${RT_PULL_TOKEN_VAR:-}"
109
109
  pull_token_hint="${RT_PULL_TOKEN_HINT:-}"
110
+ # Раздел, который тело заявки обязано нести с минуты открытия. Кнопку слияния нажимает человек
111
+ # на хостинге, куда гард не достаёт: всё, чем требование там держится, — то, что владелец увидел
112
+ # на странице. Заголовок раздела пишется языком заявки, поэтому образец называет дерево, а не
113
+ # пакет: чужих слов пакет не знает, и выдуманное умолчание не совпало бы ни с чем.
114
+ pull_body_section="${RT_PULL_BODY_SECTION:-}"
110
115
  commit_email="${RT_COMMIT_EMAIL:-}"
111
116
  tasks_dir="${RT_TASKS_DIR:-}"
112
117
  archive_dir="${RT_ARCHIVE_DIR:-}"
@@ -327,6 +332,11 @@ if printf '%s' "$cmd" | grep -qE "${RT_CMD_BOUND}(gh[[:space:]]+pr[[:space:]]+re
327
332
  fi
328
333
  fi
329
334
  fi
335
+
336
+ # Папка задачи: тот же предмет, что на открытии и на слиянии, третьим рубежом. Условие
337
+ # местное — оно читает ветку, а не хостинг, — и потому стоит вне сетевого яруса выше.
338
+ command -v rt_delivery_ready_folder >/dev/null 2>&1 && rt_delivery_ready_folder
339
+
330
340
  deny_faults
331
341
  fi
332
342
 
@@ -424,6 +434,35 @@ if [ -n "$pull_token_var" ] \
424
434
  fault "заявка открывается без токена машинной записи: в команде нет подстановки «${pull_token_var}». Открытая залогиненной записью, она выйдет от владельца — ревьювером его тогда не назначить, и чинится это только переоткрытием.${pull_token_hint:+ Подставь токен: ${pull_token_hint} …}"
425
435
  fi
426
436
 
437
+ # Тело заявки несёт раздел об оставшемся шаге, и несёт его с минуты открытия. Требование
438
+ # записано было и прежде — прозой в паттерне закрытия работы, ниже того места, где описано само
439
+ # открытие: читающий доходил до открытия, открывал и раздела ещё не прочитал. Так заявка и
440
+ # уехала без него, а владелец влил её кнопкой, пока шёл прогон.
441
+ #
442
+ # Судится текст команды: тело приходит доводом либо файлом, и оба вида читаются здесь же. Файл
443
+ # читается с диска — к моменту разбора он уже написан. Ни того ни другого в команде нет —
444
+ # требования нет: заявка без тела вовсе судится строкой выше, а не этой.
445
+ if [ -n "$pull_body_section" ]; then
446
+ body=''
447
+ if command -v perl >/dev/null 2>&1; then
448
+ body="$(printf '%s' "$cmd" | perl -0ne '
449
+ if (/(?:--body|-b)(?:=|\s+)(?:"((?:[^"\\]|\\.)*)"|\x27([^\x27]*)\x27|(\S+))/s) {
450
+ print defined $1 ? $1 : (defined $2 ? $2 : $3);
451
+ }
452
+ ' 2>/dev/null)"
453
+ body_file="$(printf '%s' "$cmd" | perl -0ne '
454
+ if (/(?:--body-file|-F)(?:=|\s+)(?:"((?:[^"\\]|\\.)*)"|\x27([^\x27]*)\x27|(\S+))/s) {
455
+ print defined $1 ? $1 : (defined $2 ? $2 : $3);
456
+ }
457
+ ' 2>/dev/null)"
458
+ [ -z "$body" ] && [ -n "$body_file" ] && [ -f "$body_file" ] && body="$(cat "$body_file" 2>/dev/null)"
459
+ fi
460
+
461
+ if [ -n "$body" ] && ! printf '%s' "$body" | grep -qE "$pull_body_section"; then
462
+ fault "в теле заявки нет раздела об оставшемся шаге. Кнопку слияния нажимает человек на хостинге, где гардов нет, и вливает он, как только видит зелёное: всё, чем требование там держится, — то, что владелец прочитал на странице. Раздел стоит последним и говорит ровно одно — осталось ли что-то до слияния; переписывается он тем же вызовом, которым правится тело."
463
+ fi
464
+ fi
465
+
427
466
  check_task "$number" "заявка с ветки «${branch}»" да
428
467
 
429
468
  # Папка задачи разбирается до открытия заявки, а не после одобрения. Прежде здесь стояло
@@ -119,11 +119,25 @@ if git fetch --quiet origin "$main_branch" 2>/dev/null && git rev-parse --verify
119
119
  base="origin/$main_branch"
120
120
  fi
121
121
 
122
+ # Код, которым проверка объявляет, что смотреть было не на что: ни «сошлось», ни «расхождение».
123
+ # Прежде такая проверка говорила о себе строкой вывода и выходила нулём — в наборе этот ноль
124
+ # стоял рядом с пройденными и ничем от них не отличался, а сводка читалась как проверенная
125
+ # целиком. Пуш он не отбивает: проверка, которой нечего смотреть, поломкой не является.
126
+ rt_skip_code="${RT_SKIP_CODE:-7}"
127
+
122
128
  failed=""
123
129
  output=""
130
+ skipped=""
124
131
  while IFS= read -r check; do
125
132
  [ -z "$check" ] && continue
126
- out="$(eval "$check" 2>&1)" && continue
133
+ out="$(eval "$check" 2>&1)"
134
+ status=$?
135
+ [ "$status" -eq 0 ] && continue
136
+ if [ "$status" -eq "$rt_skip_code" ]; then
137
+ skipped="${skipped}${skipped:+
138
+ }${check}"
139
+ continue
140
+ fi
127
141
  failed="$check"
128
142
  output="$out"
129
143
  break
@@ -131,6 +145,12 @@ done <<EOF
131
145
  $(rt_push_checks "$base")
132
146
  EOF
133
147
 
148
+ # Пропущенное называется вслух и тогда, когда набор прошёл: молчание о нём и есть та самая
149
+ # неотличимость, ради которой код заведён. Пуш при этом идёт — отказа здесь нет.
150
+ if [ -z "$failed" ] && [ -n "$skipped" ]; then
151
+ printf 'гейт пуша: набор прошёл, но эти проверки смотреть было не на что:\n%s\n' "$skipped" >&2
152
+ fi
153
+
134
154
  [ -z "$failed" ] && exit 0
135
155
 
136
156
  # Хвост вывода, а не весь: у прогонщика он длинный, а нужна причина отказа.
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env bash
2
+ # rt-hook: PreToolUse AskUserQuestion
3
+ # Требует: hooks/grill-gate.sh
4
+ # Половина гарда разговора, стоящая на инструменте вопроса: меню судится до отправки.
5
+ #
6
+ # Своим ресурсом, а не строкой в шапке соседа, потому что инструмент вопроса — единственный
7
+ # способ спросить владельца, и у дерева на нём уже может стоять свой гард со своей причиной
8
+ # отказа. Второй отказ на том же инструменте делает вопрос недоступным вовсе, и дерево
9
+ # отказывается от гарда целиком вместо того, чтобы взять его половину. Разведённые по двум
10
+ # ресурсам, половины отменяются порознь: строка отказа снимает одну и оставляет другую.
11
+ #
12
+ # Судит вызов то же тело, что и завершение хода: требование одно, и расходиться двум его
13
+ # редакциям нельзя. Здесь только объявление события и передача ввода.
14
+ #
15
+ # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: нет соседнего файла — ход РАЗРЕШАЕТСЯ. Половина гарда, потерявшая
16
+ # тело, не имеет права заклинить разговор.
17
+
18
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
19
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
20
+
21
+ rt_hooks_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
22
+ [ -f "$rt_hooks_dir/grill-gate.sh" ] || exit 0
23
+
24
+ rt_hook_read
25
+ printf '%s' "$RT_HOOK_INPUT" | "$rt_hooks_dir/grill-gate.sh"
@@ -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,7 +13,9 @@
13
13
  # Одним этим перехватом дыра не закрывается: вопрос чаще задаётся прозой, и ровно так был задан
14
14
  # тот, из-за которого гард заведён. Прозаический вопрос инструментом не является, и поймать его
15
15
  # можно только на завершении хода — событие получает путь к записи хода и видит его целиком.
16
- # Отсюда два события: меню ловится до отправки, проза — после.
16
+ # Отсюда два события: меню ловится до отправки, проза — после. Объявлены они разными ресурсами
17
+ # затем, чтобы дерево, у которого инструмент вопроса занят своим гардом, могло взять половину, а
18
+ # не отказаться от требования целиком.
17
19
  #
18
20
  # Чтением правил считается любой из трёх путей: загрузка правила, чтение файла законов или
19
21
  # правил, поиск по ним. Требовать именно загрузку значило бы гнать на неё там, где хватило
@@ -57,13 +59,18 @@ done
57
59
  laws_dir="${RT_LAWS_DIR-docs/constitution}"
58
60
  rules_dir="${RT_RULES_DIR-.claude/skills}"
59
61
  specs_dir="${RT_SPECS_DIR-docs/specs}"
62
+ # Замысел эпика и описание прошлого читаются наравне с законами: решение, связывающее задачи
63
+ # эпика, лежит именно там. Прочитавший замысел получал отказ наравне с не читавшим ничего, и
64
+ # снимался тот отказ поиском по трём каталогам, среди которых нужного не было.
65
+ plans_dir="${RT_PLANS_DIR-docs/plans}"
66
+ archive_dir="${RT_ARCHIVE_DIR-docs/archive}"
60
67
 
61
68
  # Дерево, у которого нет ни законов, ни правил, требования не получает: читать нечего.
62
69
  [ -z "$laws_dir" ] && [ -z "$rules_dir" ] && exit 0
63
70
 
64
71
  # Образец, по которому вызов инструмента считается чтением правил. Каталоги идут в него как
65
72
  # есть: точка в `.claude` совпадает с любым знаком и лишнего сюда не приводит.
66
- read_re="$(printf '%s' "$laws_dir|$rules_dir|$specs_dir" | sed 's/^|*//; s/|*$//; s/||*/|/g')"
73
+ read_re="$(printf '%s' "$laws_dir|$rules_dir|$specs_dir|$plans_dir|$archive_dir" | sed 's/^|*//; s/|*$//; s/||*/|/g')"
67
74
  [ -z "$read_re" ] && exit 0
68
75
 
69
76
  # Ход — это всё, что записано после последнего настоящего ввода владельца. Ответ инструмента
@@ -106,7 +113,7 @@ fi
106
113
 
107
114
  reason="$head Вопрос, ответ на который уже записан, владельцу не задаётся — правило ведения работы. Прогони поиск по словам темы и ответь по найденному; спрашивай только то, что документацией не покрыто:
108
115
 
109
- grep -rn -i \"<слово темы>\" $laws_dir $rules_dir $specs_dir
116
+ grep -rn -i \"<слово темы>\" $laws_dir $rules_dir $specs_dir $plans_dir $archive_dir
110
117
 
111
118
  Гард судит один ход: следующий заход не отбивается."
112
119
 
@@ -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}
@@ -82,11 +82,27 @@ verdict="$(tail -n 400 "$transcript" 2>/dev/null | jq -s -r --arg asked "$asked_
82
82
 
83
83
  [ "$verdict" = "owe" ] || exit 0
84
84
 
85
+ # Команда отправки называется той, которая в этом дереве исполняется. Дерево, поставившее пакет
86
+ # зависимостью, зовёт бинарь из зависимостей; дерево, где пакет живёт исходниками, бинаря не
87
+ # имеет вовсе — там зовут собранный bin. Названная наугад команда стоит исполнителю хода: отказ
88
+ # читается как указание, и вызов `npx agent-kit` отвечает в таком дереве отказом установки.
89
+ root="${CLAUDE_PROJECT_DIR:-.}"
90
+ if [ -x "$root/node_modules/.bin/agent-kit" ]; then
91
+ propose_cmd="npx agent-kit propose"
92
+ else
93
+ built="$(ls "$root"/dist/*/bin/agent-kit.js 2>/dev/null | head -1)"
94
+ if [ -n "$built" ]; then
95
+ propose_cmd="node ${built#"$root"/} propose"
96
+ else
97
+ propose_cmd="npx agent-kit propose"
98
+ fi
99
+ fi
100
+
85
101
  reason="BLOCKED by proposal-guard: владелец сказал завести или отправить предложение слою правил, а отправки в этом ходе не было. Написанное и не отправленное лежит в дереве неотличимо от отправленного: своей записи в слое правил у него нет, и владелец читает работу сделанной, пока не спросит прямо.
86
102
 
87
103
  Предложение пишется файлом в \`$proposals_dir/\` и уезжает в тот же ход:
88
104
 
89
- npx agent-kit propose
105
+ $propose_cmd
90
106
 
91
107
  Сухой прогон отправкой не является: он показывает, что уехало бы, и следа наружу не оставляет. Отправка пишет отметки в файлы предложений и делает дерево грязным — при открытом PR они ложатся вторым коммитом в ту же ветку, и это их место, а не повод отложить.
92
108
 
@@ -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
@@ -58,6 +58,16 @@ if [ ! -d "$DIR" ]; then
58
58
  printf 'Ветка `%s` названа задачей, а `%s/` нет: ход работы записывать некуда,\n' "$branch" "$DIR"
59
59
  printf 'и следующий заход начнёт с расспросов владельца.\n\n'
60
60
  printf 'Собрать с образца:\n\n cp -r %s/_template %s\n\n' "$TASKS_DIR" "$DIR"
61
+ # Папка бывает названа не именем ветки: этапы одной большой задачи идут отдельными
62
+ # ветками при одной общей папке. Названная поимённо папка — единственное, по чему
63
+ # заход её найдёт; иначе он читает отказ как «записей нет» и отвечает владельцу из
64
+ # кода, минуя всё, что в этих записях решено.
65
+ others="$(find "$TASKS_DIR" -mindepth 1 -maxdepth 1 -type d ! -name '_template' 2>/dev/null | sort)"
66
+ if [ -n "$others" ]; then
67
+ printf 'В каталоге задач при этом лежит:\n\n%s\n\n' "$others"
68
+ printf 'Этапы одной задачи идут отдельными ветками при общей папке — прежде чем\n'
69
+ printf 'считать, что записей нет, смотрят в названные.\n\n'
70
+ fi
61
71
  # О соседнем ресурсе — условно и по имени: пакет не знает, разложен ли он здесь,
62
72
  # а сказанное безусловно приходит в контекст каждой сессии и врёт про дерево тем
63
73
  # увереннее, что печатает это сам инструмент.