@rt-tools/agent-kit 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/assets/checks/check-file-size.mjs +19 -4
  2. package/assets/checks/check-state-next.mjs +10 -2
  3. package/assets/checks/rt-kit-checks.config.mjs +16 -2
  4. package/assets/defaults/project.sh +9 -1
  5. package/assets/defaults/turn-map.md +8 -6
  6. package/assets/hooks/browser-guard-device-id.sh +3 -1
  7. package/assets/hooks/browser-guard-no-asking.sh +3 -1
  8. package/assets/hooks/browser-guard-no-other-drivers.sh +5 -3
  9. package/assets/hooks/browser-guard-require-select.sh +4 -2
  10. package/assets/hooks/claim-guard.sh +3 -1
  11. package/assets/hooks/conscience-guard.sh +3 -1
  12. package/assets/hooks/dev-server-guard.sh +5 -3
  13. package/assets/hooks/dispatch.sh +69 -0
  14. package/assets/hooks/docs-guard.sh +6 -4
  15. package/assets/hooks/exam-guard.sh +5 -3
  16. package/assets/hooks/git-guard-delivery.sh +37 -5
  17. package/assets/hooks/git-guard-main.sh +6 -4
  18. package/assets/hooks/git-guard-push-tests.sh +6 -4
  19. package/assets/hooks/grill-gate.sh +4 -2
  20. package/assets/hooks/handoff-entry-guard.sh +4 -2
  21. package/assets/hooks/handoff-write.sh +27 -6
  22. package/assets/hooks/hook-input.sh +54 -0
  23. package/assets/hooks/lint-after-edit.sh +5 -3
  24. package/assets/hooks/postmortem-guard.sh +3 -1
  25. package/assets/hooks/proposal-guard.sh +3 -1
  26. package/assets/hooks/prose-style-guard.sh +5 -3
  27. package/assets/hooks/qa-dataid-guard.sh +4 -2
  28. package/assets/hooks/rerun-guard.sh +5 -3
  29. package/assets/hooks/reuse-first-guard.sh +5 -3
  30. package/assets/hooks/rule-article.sh +99 -0
  31. package/assets/hooks/skill-gate-rearm.sh +3 -1
  32. package/assets/hooks/skill-gate.sh +23 -2
  33. package/assets/hooks/skill-loaded.sh +3 -1
  34. package/assets/hooks/sql-guard-request.sh +2 -1
  35. package/assets/hooks/sql-guard.sh +4 -2
  36. package/assets/hooks/task-flow-guard.sh +6 -4
  37. package/assets/hooks/turn-exit-guard.sh +42 -17
  38. package/assets/hooks/waiting-turn-guard.sh +3 -1
  39. package/assets/hooks/window-fill-guard.sh +6 -4
  40. package/assets/laws/work-conduct.md +5 -9
  41. package/assets/patterns/dependencies-upgrade.md +1 -1
  42. package/assets/patterns/doc-style-write.md +3 -3
  43. package/assets/patterns/git-workflow-commit.azure.md +2 -202
  44. package/assets/patterns/git-workflow-commit.github.md +2 -258
  45. package/assets/patterns/git-workflow-commit.gitlab.md +1 -217
  46. package/assets/patterns/git-workflow-docker.md +3 -3
  47. package/assets/patterns/git-workflow-merge.md +3 -2
  48. package/assets/patterns/git-workflow-migration.md +3 -3
  49. package/assets/patterns/git-workflow-pr.azure.md +224 -0
  50. package/assets/patterns/git-workflow-pr.github.md +280 -0
  51. package/assets/patterns/git-workflow-pr.gitlab.md +240 -0
  52. package/assets/patterns/git-workflow-restart.md +3 -3
  53. package/assets/patterns/git-workflow-secrets.md +3 -3
  54. package/assets/patterns/task-flow-archive.md +193 -0
  55. package/assets/patterns/task-flow-close.md +3 -173
  56. package/assets/patterns/task-flow-handoff.md +4 -4
  57. package/assets/pitfalls/doc-style.md +80 -0
  58. package/assets/pitfalls/git-workflow.azure.md +50 -0
  59. package/assets/pitfalls/git-workflow.github.md +78 -0
  60. package/assets/pitfalls/git-workflow.gitlab.md +49 -0
  61. package/assets/pitfalls/spec-driven.md +36 -0
  62. package/assets/pitfalls/styling-bem.md +45 -0
  63. package/assets/pitfalls/task-flow.md +62 -0
  64. package/assets/pitfalls/testing.md +70 -0
  65. package/assets/rules/deploy-flow.azure.md +106 -0
  66. package/assets/rules/deploy-flow.github.md +113 -0
  67. package/assets/rules/deploy-flow.gitlab.md +108 -0
  68. package/assets/rules/doc-style.md +25 -76
  69. package/assets/rules/git-workflow.azure.md +6 -92
  70. package/assets/rules/git-workflow.github.md +14 -127
  71. package/assets/rules/git-workflow.gitlab.md +6 -93
  72. package/assets/rules/spec-driven.md +39 -30
  73. package/assets/rules/styling-bem.md +20 -39
  74. package/assets/rules/task-flow.md +17 -199
  75. package/assets/rules/testing.md +3 -64
  76. package/assets/rules/turn-conduct.md +206 -0
  77. package/assets/rules/typescript-conventions.md +15 -0
  78. package/assets/skills/agent-kit.md +35 -12
  79. package/assets/templates/pitfalls.md +10 -0
  80. package/assets/templates/rule.md +5 -3
  81. package/bin/agent-kit.d.ts.map +1 -1
  82. package/bin/agent-kit.js +1 -42
  83. package/bin/agent-kit.js.map +1 -1
  84. package/lib/assets.d.ts.map +1 -1
  85. package/lib/assets.js +6 -1
  86. package/lib/assets.js.map +1 -1
  87. package/lib/cascade.d.ts.map +1 -1
  88. package/lib/cascade.js +19 -1
  89. package/lib/cascade.js.map +1 -1
  90. package/lib/commands.d.ts.map +1 -1
  91. package/lib/commands.js +1 -0
  92. package/lib/commands.js.map +1 -1
  93. package/lib/config.d.ts +16 -1
  94. package/lib/config.d.ts.map +1 -1
  95. package/lib/config.js +8 -0
  96. package/lib/config.js.map +1 -1
  97. package/lib/hooks-map.d.ts +13 -0
  98. package/lib/hooks-map.d.ts.map +1 -1
  99. package/lib/hooks-map.js +33 -1
  100. package/lib/hooks-map.js.map +1 -1
  101. package/lib/ship.d.ts +1 -2
  102. package/lib/ship.d.ts.map +1 -1
  103. package/lib/ship.js +0 -54
  104. package/lib/ship.js.map +1 -1
  105. package/package.json +1 -1
  106. package/rt-tools-agent-kit-0.12.0.tgz +0 -0
  107. package/assets/commands/agent-kit-digest.md +0 -89
  108. package/assets/commands/rules-review.md +0 -98
  109. package/assets/patterns/cargo-triage-mark.md +0 -119
  110. package/assets/rules/cargo-triage.md +0 -126
  111. package/lib/cargo-state.d.ts +0 -62
  112. package/lib/cargo-state.d.ts.map +0 -1
  113. package/lib/cargo-state.js +0 -118
  114. package/lib/cargo-state.js.map +0 -1
  115. package/rt-tools-agent-kit-0.11.0.tgz +0 -0
@@ -35,8 +35,18 @@ import { join } from 'node:path';
35
35
  import { allowlistOf, baselineOf, CONFIG, ROOT, parseAllowlist } from './rt-kit-checks.config.mjs';
36
36
 
37
37
  const ALLOWLIST = allowlistOf('file-size');
38
- /** Предел один на все роды файлов: своё число каждому роду спор о числе на каждой правке. */
38
+ /** Пределов два: код и текст слоя правил. Какой из них применён, каждая строка отказа называет. */
39
39
  const LIMIT = CONFIG.fileSizeLimit;
40
+ const PROSE_LIMIT = CONFIG.proseSizeLimit ?? CONFIG.fileSizeLimit;
41
+ /** Корни текста слоя правил; дерево, их не назвавшее, судится одним пределом. */
42
+ const PROSE_ROOTS = CONFIG.proseRoots ?? [];
43
+
44
+ /** Предел для файла и имя предела для отказа: по корню, а не по расширению — код лежит и в `.md`. */
45
+ function limitOf(path) {
46
+ return PROSE_ROOTS.some((root) => root && path.startsWith(root))
47
+ ? { limit: PROSE_LIMIT, title: 'предел текста' }
48
+ : { limit: LIMIT, title: 'предел кода' };
49
+ }
40
50
 
41
51
  /** Роды файлов, которых не читает линтер. Код остаётся за ним. */
42
52
  const JUDGED = ['.md', '.scss', '.html', '.js', '.mjs', '.sh'];
@@ -78,7 +88,7 @@ const tracked = trackedFiles().filter(judged);
78
88
 
79
89
  for (const path of tracked) {
80
90
  const lines = lineCount(path);
81
- if (lines > LIMIT) {
91
+ if (lines > limitOf(path).limit) {
82
92
  tooLong.set(path, lines);
83
93
  }
84
94
  }
@@ -95,7 +105,10 @@ const gone = [...known.keys()].filter((path) => !existsSync(join(ROOT, path)));
95
105
  const shrunk = [...known.keys()].filter((path) => !tooLong.has(path) && existsSync(join(ROOT, path)));
96
106
 
97
107
  const problems = [
98
- ...fresh.map(([path, lines]) => `${path}: ${lines} строк, предел ${LIMIT} — делить, а не дописывать строку в ${ALLOWLIST}`),
108
+ ...fresh.map(([path, lines]) => {
109
+ const { limit, title } = limitOf(path);
110
+ return `${path}: ${lines} строк, ${title} ${limit} — делить, а не дописывать строку в ${ALLOWLIST}`;
111
+ }),
99
112
  ...gone.map((path) => `${path}: строка в ${ALLOWLIST} устарела — файла в дереве нет`),
100
113
  ...shrunk.map((path) => `${path}: значится в ${ALLOWLIST}, но уже короче предела — строку убрать`),
101
114
  ];
@@ -107,7 +120,9 @@ if (problems.length > 0) {
107
120
  process.exit(1);
108
121
  }
109
122
 
123
+ const limits = PROSE_ROOTS.length > 0 ? `предел кода ${LIMIT}, предел текста ${PROSE_LIMIT}` : `предел ${LIMIT}`;
124
+
110
125
  console.log(
111
- `check-file-size: проверено ${tracked.length} файлов, длиннее ${LIMIT} строк ${tooLong.size}, ` +
126
+ `check-file-size: проверено ${tracked.length} файлов, ${limits}, длиннее предела ${tooLong.size}, ` +
112
127
  `из них принято ${accepted.size}, долг ${debt.size} — новых нет`
113
128
  );
@@ -31,6 +31,13 @@ import { join } from 'node:path';
31
31
 
32
32
  const ROOT = process.cwd();
33
33
  const RULE = join(ROOT, '.claude/skills/task-flow/SKILL.md');
34
+ /**
35
+ * Правило хода захода: граница состояния живёт там, а не в правиле ведения работы.
36
+ * Разделились они, когда правило ведения работы вышло за предел длины: ход работы
37
+ * остался в одном, выходы хода уехали в другое. Дерево, где разложено только первое,
38
+ * судится по нему одному — второго файла у него нет.
39
+ */
40
+ const TURN_RULE = join(ROOT, '.claude/skills/turn-conduct/SKILL.md');
34
41
  const SKILLS = join(ROOT, '.claude/skills');
35
42
  const MAP = join(ROOT, '.claude/rt-kit/defaults/turn-map.md');
36
43
  const LAW = join(ROOT, 'docs/constitution/work-conduct.md');
@@ -167,8 +174,9 @@ if (counted === 0) {
167
174
  problems.push('ни одного раздела состояния не нашлось: паттерны не разложены или заголовки в них другие');
168
175
  }
169
176
 
170
- if (!ruleText.includes(BOUNDARY)) {
171
- problems.push(`правило ведения работы о границе состояния молчит: строки «${BOUNDARY}» в нём нет`);
177
+ const turnText = existsSync(TURN_RULE) ? readFileSync(TURN_RULE, 'utf8') : '';
178
+ if (!ruleText.includes(BOUNDARY) && !turnText.includes(BOUNDARY)) {
179
+ problems.push(`правило хода захода о границе состояния молчит: строки «${BOUNDARY}» в нём нет`);
172
180
  }
173
181
 
174
182
  for (const [file, what] of [
@@ -67,10 +67,24 @@ const DEFAULTS = {
67
67
  */
68
68
  generatedDirs: [],
69
69
  /**
70
- * Предел длины файла одно число на все роды: своё число каждому роду означает спор о
71
- * числе на каждой правке, а не о длине файла.
70
+ * Предел длины файла для кода и обвязки. Родов у него по-прежнему не два десятка, а два:
71
+ * код и текст слоя правил. Спорить о числе на каждой правке нечему оба числа стоят
72
+ * здесь и меняются работой, а не по ходу.
72
73
  */
73
74
  fileSizeLimit: 500,
75
+ /**
76
+ * Предел длины текста слоя правил — закона, правила, паттерна, холодной части и их
77
+ * источников. Он ниже предела кода вдвое, и это не строгость ради строгости: текст, не
78
+ * влезающий на экран целиком, дописывают в конец, не перечитав начала, — так в одном файле
79
+ * и оказываются два ответа на один вопрос. У кода от этого спасает линтер, у текста —
80
+ * только это число.
81
+ */
82
+ proseSizeLimit: 300,
83
+ /**
84
+ * Корни, под которыми лежит текст слоя правил, и его источники. Файл отсюда судится
85
+ * пределом текста, всё остальное — пределом кода. Пусто — предел один на всё дерево.
86
+ */
87
+ proseRoots: [],
74
88
  /**
75
89
  * Внешние пакеты, чьи перечисления считаются наравне с либами: своё перечисление под уже
76
90
  * объявленный там набор — такая же копия, как и между двумя либами. Каждая запись — имя
@@ -317,9 +317,17 @@ RT_TASK_TITLE_RE="${RT_TASK_TITLE_RE:-^\[[A-Za-z]+-[0-9]+\][[:space:]]+[^[:space
317
317
  RT_TASK_NEW_CMD="${RT_TASK_NEW_CMD:-npm run task:new}"
318
318
  RT_BOARD_CHECK_CMD="${RT_BOARD_CHECK_CMD:-npm run check:board}"
319
319
 
320
- # Учётная запись, которую ставят исполнителем. Умолчание молчит: у каждого дерева она своя.
320
+ # Учётная запись, которую ставят исполнителем и которой открывают заявку. Умолчание молчит: у
321
+ # каждого дерева она своя, а у иного её нет вовсе.
321
322
  RT_TASK_BOT="${RT_TASK_BOT:-}"
322
323
 
324
+ # Чем вызову подставляют токен машинной записи и как эта подстановка выглядит целиком. Клиент
325
+ # хостинга держит две записи сразу — залогиненную и ту, чей токен стоит в окружении вызова, — и
326
+ # из текста команды видно только явную подстановку. Оба умолчания молчат: дерево без отдельной
327
+ # машинной записи автора заявки не судит вовсе.
328
+ RT_PULL_TOKEN_VAR="${RT_PULL_TOKEN_VAR:-}"
329
+ RT_PULL_TOKEN_HINT="${RT_PULL_TOKEN_HINT:-}"
330
+
323
331
  # Команда перевода задачи между колонками очереди работ и имя первой колонки — той, из которой
324
332
  # задача уходит, когда её берут в работу. Имя без умолчания: колонки дерево называет своими
325
333
  # словами, а выдуманное не совпало бы ни с чем и молча выключило бы проверку колонки.
@@ -1,7 +1,8 @@
1
1
  # Карта хода
2
2
 
3
3
  Это не правило, а его короткая выжимка: правило объясняет, карта называет. Полный текст —
4
- скил `task-flow`; он же называет паттерн, который ведёт каждое состояние.
4
+ скил `task-flow`; он же называет паттерн, который ведёт каждое состояние. Чем ход
5
+ кончается — скил `turn-conduct` под тем же законом.
5
6
 
6
7
  Состояние работы объявлено строкой в разделе «Где стоим» хода работы. Пока обязательное
7
8
  действие не сделано, работа стоит в том же состоянии.
@@ -19,8 +20,8 @@
19
20
  | `этапы-кончились` | прогнать набор и открыть PR черновиком | `task-flow-close` |
20
21
  | `работа-отдана` | взять следующую задачу | `task-flow-resume` |
21
22
  | `разбор-кончился` | влить договорённость, привести тексты, разобрать папку | `task-flow-close` |
22
- | `папка-разобрана` | снять черновик и попросить влить | `task-flow-close` |
23
- | `влито` | разбор работы правилами и сверка очереди | `task-flow-close` |
23
+ | `папка-разобрана` | снять черновик и попросить влить | `task-flow-archive` |
24
+ | `влито` | разбор работы правилами и сверка очереди | `task-flow-archive` |
24
25
 
25
26
  Ни у одного состояния обязательное действие не звучит как «ждать». Прогон, разбор владельцем и
26
27
  слияние идут без исполнителя и от взгляда быстрее не становятся.
@@ -41,6 +42,7 @@
41
42
  остановки там — страховка на случай, когда сжатие не пришло.
42
43
 
43
44
  Всё остальное — продолжение хода. Ходом не кончаются: коммит, записанный замысел, закрытый
44
- разбор просьбы, прочитанная договорённость, зелёная проверка, сводка о чужом шаге и объявление
45
- намерения. Переход из состояния в состояние — тем более: обязательное действие сделано, и
46
- следующее делается тем же ходом. Названо может быть только сделанное.
45
+ разбор просьбы, прочитанная договорённость, зелёная проверка, сводка о чужом шаге, объявление
46
+ намерения и названная, но не запущенная команда. Переход из состояния в состояние — тем более:
47
+ обязательное действие сделано, и следующее делается тем же ходом. Названо может быть только
48
+ сделанное.
@@ -12,8 +12,10 @@
12
12
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: помощник не назвал профиль — пропуск.
13
13
 
14
14
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
15
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
15
16
 
16
- input="$(cat 2>/dev/null)"
17
+ rt_hook_read
18
+ input="$RT_HOOK_INPUT"
17
19
 
18
20
  device_id="$("${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/browser-device-id.sh" 2>/dev/null)"
19
21
  [ -z "$device_id" ] && exit 0
@@ -15,8 +15,10 @@
15
15
  # FAIL-OPEN when browser-device-id.sh prints nothing.
16
16
 
17
17
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
18
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
18
19
 
19
- input="$(cat 2>/dev/null)"
20
+ rt_hook_read
21
+ input="$RT_HOOK_INPUT"
20
22
 
21
23
  device_id="$("${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/browser-device-id.sh" 2>/dev/null)"
22
24
  [ -z "$device_id" ] && exit 0
@@ -14,13 +14,15 @@
14
14
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: помощник не назвал профиль — пропуск.
15
15
 
16
16
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
17
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
17
18
 
18
- input="$(cat 2>/dev/null)"
19
+ rt_hook_read
20
+ input="$RT_HOOK_INPUT"
19
21
 
20
22
  device_id="$("${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/browser-device-id.sh" 2>/dev/null)"
21
23
  [ -z "$device_id" ] && exit 0
22
24
 
23
- tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
25
+ tool="$(rt_hook_tool)"
24
26
 
25
27
  # shellcheck disable=SC1090
26
28
  [ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" ] \
@@ -45,7 +47,7 @@ case "$tool" in
45
47
  *) exit 0 ;;
46
48
  esac
47
49
 
48
- cmd="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null)"
50
+ cmd="$(rt_hook_cmd)"
49
51
  [ -z "$cmd" ] && exit 0
50
52
 
51
53
  if [ "$tool" = "mcp__webstorm__execute_tool" ] && command -v perl >/dev/null 2>&1; then
@@ -21,13 +21,15 @@
21
21
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: помощник не назвал профиль — пропуск.
22
22
 
23
23
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
24
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
24
25
 
25
- input="$(cat 2>/dev/null)"
26
+ rt_hook_read
27
+ input="$RT_HOOK_INPUT"
26
28
 
27
29
  device_id="$("${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/browser-device-id.sh" 2>/dev/null)"
28
30
  [ -z "$device_id" ] && exit 0
29
31
 
30
- tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
32
+ tool="$(rt_hook_tool)"
31
33
  # У перечисления, переключения и самого выбора свои гарды.
32
34
  case "$tool" in
33
35
  *list_connected_browsers|*switch_browser|*select_browser) exit 0 ;;
@@ -26,8 +26,10 @@
26
26
  # тексте ход РАЗРЕШАЕТСЯ (exit 0). Сломанный гард не имеет права заклинить разговор.
27
27
 
28
28
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
29
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
29
30
 
30
- input="$(cat 2>/dev/null)"
31
+ rt_hook_read
32
+ input="$RT_HOOK_INPUT"
31
33
  [ -z "$input" ] && exit 0
32
34
  command -v jq >/dev/null 2>&1 || exit 0
33
35
 
@@ -20,8 +20,10 @@
20
20
  # разрешается. Сломанная совесть не имеет права заклинить разговор.
21
21
 
22
22
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
23
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
23
24
 
24
- input="$(cat 2>/dev/null)"
25
+ rt_hook_read
26
+ input="$RT_HOOK_INPUT"
25
27
  [ -z "$input" ] && exit 0
26
28
  command -v jq >/dev/null 2>&1 || exit 0
27
29
 
@@ -15,10 +15,12 @@
15
15
  # RT_STANDS. Нет профиля — текст отказа остаётся общим, сам гард работает.
16
16
 
17
17
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
18
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
18
19
 
19
- input="$(cat 2>/dev/null)"
20
+ rt_hook_read
21
+ input="$RT_HOOK_INPUT"
20
22
 
21
- tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
23
+ tool="$(rt_hook_tool)"
22
24
  case "$tool" in
23
25
  Bash | mcp__webstorm__execute_terminal_command | mcp__webstorm__execute_tool) ;;
24
26
  # Готовая конфигурация запуска командной строки не показывает — видно только её имя.
@@ -48,7 +50,7 @@ case "$tool" in
48
50
  *) exit 0 ;;
49
51
  esac
50
52
 
51
- cmd="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null)"
53
+ cmd="$(rt_hook_cmd)"
52
54
  [ -z "$cmd" ] && exit 0
53
55
 
54
56
  # Универсальный исполнитель среды передаёт настоящую команду вложенной строкой. Разбирать надо
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env bash
2
+ # Диспетчер событий агента. НЕ гард: объявления `rt-hook:` у него нет — наоборот, он читает
3
+ # такие объявления у остальных. В настройке агента он стоит один на событие вместо списка.
4
+ #
5
+ # Зачем он есть. Агент зовёт каждый гард своим процессом и каждому подаёт один и тот же ввод.
6
+ # Гардов на вызове инструмента восемнадцать, и каждый разбирает ввод заново — по шесть-семь
7
+ # вызовов разборщика. Замер этого дерева: восемьсот двадцать шесть миллисекунд на вызов, из
8
+ # которых восемьдесят пять — запуск оболочек, а остальное — повторный разбор одного текста.
9
+ #
10
+ # Что он делает. Читает ввод один раз, разбирает его один раз, кладёт поля в окружение и зовёт
11
+ # ветки события по порядку. Первый ненулевой код возврата отдаётся агенту вместе с выводом
12
+ # гарда — остальные ветки не зовутся: отказ и раньше кончал вызов на первом же гарде.
13
+ #
14
+ # Чего он не делает. Он не судит ничего сам и ни одного гарда не заменяет: файлы остаются как
15
+ # были, меняется точка входа. Событие и образец вызова гард несёт сам, строкой `# rt-hook:` в
16
+ # своей шапке, и карта собирается из них — список, выписанный отдельно, разошёлся бы с набором
17
+ # файлов на первом же добавленном.
18
+ #
19
+ # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: нет события в доводе, нет каталога гардов, нет разборщика — выход нулём.
20
+ # Сломанный диспетчер не имеет права заклинить работу.
21
+
22
+ set -u
23
+
24
+ here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
25
+ . "$here/utf8.sh" 2>/dev/null || true
26
+
27
+ event="${1:-}"
28
+ [ -z "$event" ] && exit 0
29
+
30
+ input="$(cat 2>/dev/null)"
31
+ [ -z "$input" ] && exit 0
32
+
33
+ # Разбор один на все ветки: четыре поля одним вызовом разборщика вместо шести на каждый гард.
34
+ # Значения приходят уже закавыченными для оболочки — `@sh` в разборщике для того и сделан:
35
+ # командная строка держит и кавычки, и переводы строк, и подставить её иначе нельзя.
36
+ assignments="$(printf '%s' "$input" | jq -r '@sh "RT_HOOK_TOOL=\(.tool_name // "") RT_HOOK_CMD=\(.tool_input.command // "") RT_HOOK_FILE=\(.tool_input.file_path // "") RT_HOOK_CWD=\(.cwd // "")"' 2>/dev/null)"
37
+ if [ -n "$assignments" ]; then
38
+ eval "$assignments" 2>/dev/null || true
39
+ export RT_HOOK_TOOL RT_HOOK_CMD RT_HOOK_FILE RT_HOOK_CWD
40
+ # Признак разбора: по нему ветки отличают готовое поле от пустой переменной, случайно
41
+ # оказавшейся в окружении прогона. Без него пустое значение читается как «поля нет».
42
+ export RT_HOOK_PARSED=1
43
+ fi
44
+ export RT_HOOK_INPUT="$input"
45
+
46
+ branches="$(grep -l '^# rt-hook:' "$here"/*.sh 2>/dev/null | sort)"
47
+ [ -z "$branches" ] && exit 0
48
+
49
+ for branch in $branches; do
50
+ declaration="$(sed -n 's/^# rt-hook:[[:space:]]*//p' "$branch" 2>/dev/null | head -1)"
51
+ [ -z "$declaration" ] && continue
52
+
53
+ branch_event="${declaration%% *}"
54
+ [ "$branch_event" = "$event" ] || continue
55
+
56
+ # Образец вызова: его нет вовсе — гард зовётся на любом; есть — сверяется с именем
57
+ # инструмента целиком, а не куском. Звёздочка и точка со звёздочкой значат одно: любой вызов.
58
+ matcher="${declaration#"$branch_event"}"
59
+ matcher="${matcher#"${matcher%%[![:space:]]*}"}"
60
+ if [ -n "$matcher" ] && [ "$matcher" != '*' ] && [ "$matcher" != '.*' ]; then
61
+ [[ "${RT_HOOK_TOOL:-}" =~ ^(${matcher})$ ]] || continue
62
+ fi
63
+
64
+ printf '%s' "$input" | bash "$branch"
65
+ code=$?
66
+ [ "$code" -ne 0 ] && exit "$code"
67
+ done
68
+
69
+ exit 0
@@ -27,12 +27,14 @@
27
27
  # пропуск.
28
28
 
29
29
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
30
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
30
31
 
31
- input="$(cat 2>/dev/null)"
32
+ rt_hook_read
33
+ input="$RT_HOOK_INPUT"
32
34
  [ -z "$input" ] && exit 0
33
35
  command -v jq >/dev/null 2>&1 || exit 0
34
36
 
35
- tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
37
+ tool="$(rt_hook_tool)"
36
38
 
37
39
  decide() {
38
40
  # Наблюдение пишется только на отказе: подсказку гард раздаёт и там, где всё в порядке, и
@@ -117,7 +119,7 @@ case "$tool" in
117
119
  *) exit 0 ;;
118
120
  esac
119
121
 
120
- cmd="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null)"
122
+ cmd="$(rt_hook_cmd)"
121
123
 
122
124
  # Универсальный исполнитель передаёт настоящую команду вложенной строкой. Разбирать надо её,
123
125
  # иначе имя команды стоит сразу за кавычкой и ни одно правило до него не дотягивается.
@@ -146,7 +148,7 @@ if printf '%s' "$cmd" | grep -qiE '(^|#)[[:space:]]*Docs-skip:[[:space:]]*[^[:sp
146
148
  exit 0
147
149
  fi
148
150
 
149
- workdir="$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null)"
151
+ workdir="$(rt_hook_cwd)"
150
152
  [ -z "$workdir" ] && workdir="${CLAUDE_PROJECT_DIR:-.}"
151
153
  cd "$workdir" 2>/dev/null || exit 0
152
154
  git rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0
@@ -19,8 +19,10 @@
19
19
  # мешать работать.
20
20
 
21
21
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
22
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
22
23
 
23
- input="$(cat 2>/dev/null)"
24
+ rt_hook_read
25
+ input="$RT_HOOK_INPUT"
24
26
  [ -z "$input" ] && exit 0
25
27
  command -v jq >/dev/null 2>&1 || exit 0
26
28
 
@@ -32,14 +34,14 @@ rt_hooks_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
32
34
  [ -f "$rt_hooks_dir/roles.sh" ] && . "$rt_hooks_dir/roles.sh" 2>/dev/null
33
35
  command -v rt_role_off >/dev/null 2>&1 && rt_role_off strict-teacher && exit 0
34
36
 
35
- tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
37
+ tool="$(rt_hook_tool)"
36
38
  # Второй экзамен спрашивается на снятии черновика: работа кончилась, и правила поставки к этому
37
39
  # моменту читались давно — между их чтением и этой минутой прошёл весь заход.
38
40
  ready=0
39
41
  case "$tool" in
40
42
  Edit | Write | MultiEdit | mcp__webstorm__create_new_file) ;;
41
43
  Bash)
42
- cmd="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null)"
44
+ cmd="$(rt_hook_cmd)"
43
45
  printf '%s' "$cmd" | grep -qE 'pr[[:space:]]+ready|mr[[:space:]]+update[^|;&]*--ready' || exit 0
44
46
  ready=1
45
47
  ;;
@@ -30,7 +30,9 @@
30
30
  # numbered); молчание значит «спросить некого»;
31
31
  # RT_TASK_NEW_CMD — чем заводится задача;
32
32
  # RT_BOARD_CHECK_CMD — чем сверяется очередь работ;
33
- # RT_TASK_BOT — учётная запись, которую ставят исполнителем;
33
+ # RT_TASK_BOT — учётная запись, которую ставят исполнителем; ею же открывается заявка;
34
+ # RT_PULL_TOKEN_VAR — переменная, которой вызову подставляют её токен;
35
+ # RT_PULL_TOKEN_HINT — готовая подстановка этого токена, целиком;
34
36
  # RT_COMMIT_EMAIL — почта, которой подписан машинный коммит; её же левой частью он и
35
37
  # опознаётся.
36
38
  # Отказ называет и то, что не так, и чем это чинится: отказ без действия обходят, а не исполняют.
@@ -38,12 +40,14 @@
38
40
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: не репозиторий, нет разборщика, битый ввод, нет профиля — пропуск.
39
41
 
40
42
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
43
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
41
44
 
42
- input="$(cat 2>/dev/null)"
45
+ rt_hook_read
46
+ input="$RT_HOOK_INPUT"
43
47
  [ -z "$input" ] && exit 0
44
48
  command -v jq >/dev/null 2>&1 || exit 0
45
49
 
46
- tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
50
+ tool="$(rt_hook_tool)"
47
51
  sid="$(printf '%s' "$input" | jq -r '.session_id // "nosession"' 2>/dev/null)"
48
52
  case "$tool" in
49
53
  # Терминал среды и универсальный исполнитель кладут команду в то же поле.
@@ -51,7 +55,7 @@ case "$tool" in
51
55
  *) exit 0 ;;
52
56
  esac
53
57
 
54
- cmd="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null)"
58
+ cmd="$(rt_hook_cmd)"
55
59
  [ -z "$cmd" ] && exit 0
56
60
 
57
61
  # Универсальный исполнитель передаёт настоящую команду вложенной строкой. Разбирать надо её,
@@ -65,7 +69,7 @@ if [ "$tool" = "mcp__webstorm__execute_tool" ] && command -v perl >/dev/null 2>&
65
69
  [ -n "$inner" ] && cmd="$inner"
66
70
  fi
67
71
 
68
- workdir="$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null)"
72
+ workdir="$(rt_hook_cwd)"
69
73
  [ -z "$workdir" ] && workdir="${CLAUDE_PROJECT_DIR:-.}"
70
74
  cd "$workdir" 2>/dev/null || exit 0
71
75
  git rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0
@@ -98,6 +102,11 @@ task_move="${RT_TASK_MOVE_CMD:-npm run task:move}"
98
102
  # молча выключило бы проверку.
99
103
  backlog_column="${RT_BOARD_BACKLOG:-}"
100
104
  task_bot="${RT_TASK_BOT:-}"
105
+ # Личность вызова приходит окружением, а не словом в строке: из текста команды видно только то,
106
+ # подставлен ли токен явно. Дерево, не назвавшее переменной, автора заявки не судит вовсе —
107
+ # у него может не быть отдельной машинной записи.
108
+ pull_token_var="${RT_PULL_TOKEN_VAR:-}"
109
+ pull_token_hint="${RT_PULL_TOKEN_HINT:-}"
101
110
  commit_email="${RT_COMMIT_EMAIL:-}"
102
111
  tasks_dir="${RT_TASKS_DIR:-}"
103
112
  archive_dir="${RT_ARCHIVE_DIR:-}"
@@ -315,6 +324,16 @@ if printf '%s' "$cmd" | grep -qE '(^|[;&|(]|&&|\|\|)[[:space:]]*(gh[[:space:]]+p
315
324
  # и «ещё не посчитано» — не «конфликтует».
316
325
  printf '%s' "$pull" | jq -e '.conflicting' >/dev/null 2>&1 \
317
326
  && fault "заявка${pull_name} конфликтует с главной веткой. Влей её в свою ветку, разбери конфликт и повтори: снятый черновик читается как «можно вливать», а слить эту заявку нельзя."
327
+
328
+ # Автор заявки. На открытии судить было нечем, кроме текста команды: личность вызова
329
+ # приходит окружением. Здесь она уже названа хостингом, и это последний ход, где
330
+ # промах ещё исправим — после снятия черновика заявку вливают, а переоткрыть влитую
331
+ # нельзя. Дерево, не назвавшее машинной записи, автора не судит.
332
+ if [ -n "$task_bot" ]; then
333
+ pull_author="$(printf '%s' "$pull" | jq -r '.author // empty' 2>/dev/null)"
334
+ [ -n "$pull_author" ] && [ "$pull_author" != "$task_bot" ] \
335
+ && fault "заявку${pull_name} открыла запись «${pull_author}», а не машинная «${task_bot}». Автор заявки её ревьювером не бывает, и разбор ей назначить нечем. Автора не сменить — закрой заявку и открой заново${pull_token_hint:+, подставив токен: ${pull_token_hint} …}."
336
+ fi
318
337
  fi
319
338
  fi
320
339
  deny_faults
@@ -440,6 +459,19 @@ if [ -n "$remote_main" ] && [ -n "$local_main" ] && [ "$remote_main" != "$local_
440
459
  fault "твоя ссылка origin/${main_branch} отстала от удалённой — ${local_main:0:8} против ${remote_main:0:8}.${age} Гард сравнивает ветку с тем, что лежит в дереве, поэтому молчание первого яруса значит «ссылка не старше ветки», а не «главная ветка влита». Влей и повтори: git fetch origin && git merge origin/${main_branch}."
441
460
  fi
442
461
 
462
+ # Личность вызова. Клиент хостинга держит две записи сразу — залогиненную и ту, чей токен стоит
463
+ # в окружении вызова, — и какая из них откроет заявку, из текста команды не видно ничем, кроме
464
+ # явной подстановки. Читающие вызовы идут от залогиненной и работают, поэтому пишущий выглядит
465
+ # таким же обычным; промах всплывает шагом позже, на назначении ревьювера: автор заявки её
466
+ # ревьювером не бывает. Чинится это только переоткрытием — автора у заявки не сменить.
467
+ #
468
+ # Судится текст команды, а не ответ хостинга: спросить, чьим токеном пойдёт вызов, до его
469
+ # исполнения нельзя. Дерево, не назвавшее переменной токена, требования не получает.
470
+ if [ -n "$pull_token_var" ] \
471
+ && ! printf '%s' "$cmd" | grep -qE "(^|[;&|(]|&&|\|\||[[:space:]])${pull_token_var}="; then
472
+ fault "заявка открывается без токена машинной записи: в команде нет подстановки «${pull_token_var}». Открытая залогиненной записью, она выйдет от владельца — ревьювером его тогда не назначить, и чинится это только переоткрытием.${pull_token_hint:+ Подставь токен: ${pull_token_hint} …}"
473
+ fi
474
+
443
475
  check_task "$number" "заявка с ветки «${branch}»" да
444
476
 
445
477
  # Всё несошедшееся названо здесь, разом: до этой строки собирались условия, каждое из которых
@@ -14,11 +14,13 @@
14
14
  # Сломанный гард не должен мешать работать.
15
15
 
16
16
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
17
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
17
18
 
18
- input="$(cat 2>/dev/null)"
19
+ rt_hook_read
20
+ input="$RT_HOOK_INPUT"
19
21
  [ -z "$input" ] && exit 0
20
22
 
21
- tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
23
+ tool="$(rt_hook_tool)"
22
24
  # Терминал среды разработки исполняет ту же командную строку и кладёт её в то же поле. Пока
23
25
  # гард проверял только оболочку, весь его смысл обходился сменой инструмента.
24
26
  case "$tool" in
@@ -26,7 +28,7 @@ case "$tool" in
26
28
  *) exit 0 ;;
27
29
  esac
28
30
 
29
- cmd="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null)"
31
+ cmd="$(rt_hook_cmd)"
30
32
 
31
33
  # Универсальный исполнитель среды передаёт настоящую команду вложенной строкой. Разбирать надо
32
34
  # её, а не обёртку: иначе имя команды стоит сразу за кавычкой и ни одно правило до него не
@@ -46,7 +48,7 @@ esac
46
48
 
47
49
  # Коммит выполнится в рабочем каталоге вызова, поэтому и ветку смотрим там же; корень проекта
48
50
  # — запасной вариант, и он важен для отдельного рабочего дерева, где ветка своя.
49
- workdir="$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null)"
51
+ workdir="$(rt_hook_cwd)"
50
52
  [ -z "$workdir" ] && workdir="${CLAUDE_PROJECT_DIR:-.}"
51
53
  cd "$workdir" 2>/dev/null || exit 0
52
54
 
@@ -26,18 +26,20 @@
26
26
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: не репозиторий, битый ввод, нет профиля — пропуск.
27
27
 
28
28
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
29
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
29
30
 
30
- input="$(cat 2>/dev/null)"
31
+ rt_hook_read
32
+ input="$RT_HOOK_INPUT"
31
33
  [ -z "$input" ] && exit 0
32
34
 
33
- tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
35
+ tool="$(rt_hook_tool)"
34
36
  case "$tool" in
35
37
  # Терминал среды и универсальный исполнитель кладут команду в то же поле.
36
38
  Bash | mcp__webstorm__execute_terminal_command | mcp__webstorm__execute_tool) ;;
37
39
  *) exit 0 ;;
38
40
  esac
39
41
 
40
- cmd="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null)"
42
+ cmd="$(rt_hook_cmd)"
41
43
 
42
44
  # Вызов пуша узнаётся по двум признакам сразу — команда `git` в начале строки или за
43
45
  # разделителем и слово `push` отдельным словом. Тем же приёмом, что у гарда поставки: одной
@@ -88,7 +90,7 @@ ${deny_tail_text}"
88
90
  exit 0
89
91
  fi
90
92
 
91
- workdir="$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null)"
93
+ workdir="$(rt_hook_cwd)"
92
94
  [ -z "$workdir" ] && workdir="${CLAUDE_PROJECT_DIR:-.}"
93
95
  cd "$workdir" 2>/dev/null || exit 0
94
96
  git rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0
@@ -23,14 +23,16 @@
23
23
  # РАЗРЕШАЕТСЯ (exit 0). Сломанный гард не имеет права заклинить разговор.
24
24
 
25
25
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
26
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
26
27
 
27
- input="$(cat 2>/dev/null)"
28
+ rt_hook_read
29
+ input="$RT_HOOK_INPUT"
28
30
  [ -z "$input" ] && exit 0
29
31
 
30
32
  command -v jq >/dev/null 2>&1 || exit 0
31
33
 
32
34
  # Какое событие пришло. У вызова инструмента есть его имя, у завершения хода — нет.
33
- tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
35
+ tool="$(rt_hook_tool)"
34
36
 
35
37
  # Повторный заход по тому же ходу не судится: иначе ход не кончится никогда — гард сказал своё
36
38
  # один раз и отпускает. К вызову инструмента это не относится: там судится сам вызов.
@@ -15,12 +15,14 @@
15
15
  # FAIL-OPEN: нет `jq`, нет записи хода, передачи в реплике нет → пропуск.
16
16
 
17
17
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
18
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
18
19
 
19
- input="$(cat 2>/dev/null)"
20
+ rt_hook_read
21
+ input="$RT_HOOK_INPUT"
20
22
  [ -z "$input" ] && exit 0
21
23
  command -v jq >/dev/null 2>&1 || exit 0
22
24
 
23
- tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
25
+ tool="$(rt_hook_tool)"
24
26
  case "$tool" in
25
27
  Edit | Write | MultiEdit) ;;
26
28
  *) exit 0 ;;