@rt-tools/agent-kit 0.17.0 → 0.19.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 (75) hide show
  1. package/assets/checks/board-epics.github.mjs +123 -0
  2. package/assets/checks/board-gh.github.mjs +121 -0
  3. package/assets/checks/board.github.mjs +77 -73
  4. package/assets/checks/check-board.github.mjs +39 -3
  5. package/assets/checks/check-hook-scope.mjs +126 -0
  6. package/assets/checks/check-profile-drift.mjs +195 -0
  7. package/assets/checks/check-specs.mjs +1 -1
  8. package/assets/checks/rt-kit-checks.config.mjs +13 -0
  9. package/assets/checks/spec-anchors.mjs +2 -2
  10. package/assets/defaults/project.sh +21 -117
  11. package/assets/defaults/shell.sh +139 -0
  12. package/assets/hooks/browser-guard-no-other-drivers.sh +41 -2
  13. package/assets/hooks/claim-guard.sh +22 -1
  14. package/assets/hooks/docs-guard.sh +1 -1
  15. package/assets/hooks/exam-guard.sh +85 -12
  16. package/assets/hooks/git-guard-delivery-conflict.sh +85 -0
  17. package/assets/hooks/git-guard-delivery-signature.sh +14 -5
  18. package/assets/hooks/git-guard-delivery.sh +15 -2
  19. package/assets/hooks/grill-gate.sh +63 -6
  20. package/assets/hooks/proposal-guard.sh +11 -4
  21. package/assets/hooks/rule-source-guard.sh +8 -13
  22. package/assets/hooks/task-context-load.sh +22 -0
  23. package/assets/hooks/task-flow-context.sh +9 -0
  24. package/assets/hooks/task-flow-draft-guard.sh +17 -0
  25. package/assets/hooks/turn-exit-guard.sh +45 -87
  26. package/assets/hooks/waiting-turn-guard.sh +65 -2
  27. package/assets/hooks/work-start-guard.sh +166 -0
  28. package/assets/hooks/write-targets.sh +24 -0
  29. package/assets/laws/verifiability.md +28 -0
  30. package/assets/patterns/browser-verification-measure.md +49 -1
  31. package/assets/patterns/browser-verification-stand.md +10 -2
  32. package/assets/patterns/doc-style-write.md +21 -1
  33. package/assets/patterns/git-workflow-commit.github.md +32 -3
  34. package/assets/patterns/git-workflow-docker.md +8 -0
  35. package/assets/patterns/git-workflow-freshness.md +87 -0
  36. package/assets/patterns/git-workflow-merge.md +23 -0
  37. package/assets/patterns/git-workflow-pr.github.md +6 -0
  38. package/assets/patterns/git-workflow-restart.md +18 -0
  39. package/assets/patterns/git-workflow-stack.md +30 -0
  40. package/assets/patterns/lib-layers-move.md +4 -0
  41. package/assets/patterns/spec-driven-domain.md +13 -1
  42. package/assets/patterns/spec-driven-rule.md +5 -4
  43. package/assets/patterns/spec-driven-sweep.md +1 -1
  44. package/assets/patterns/status-report-table.github.md +1 -1
  45. package/assets/patterns/task-flow-archive.md +12 -2
  46. package/assets/patterns/task-flow-close.md +31 -24
  47. package/assets/patterns/task-flow-handoff.md +14 -2
  48. package/assets/patterns/task-flow-resume.md +18 -3
  49. package/assets/patterns/task-flow-start.md +23 -23
  50. package/assets/patterns/testing-e2e.md +23 -0
  51. package/assets/patterns/turn-entry-map.md +1 -1
  52. package/assets/pitfalls/agent-kit.md +4 -6
  53. package/assets/pitfalls/doc-style.md +10 -0
  54. package/assets/pitfalls/git-workflow.github.md +20 -0
  55. package/assets/pitfalls/spec-driven.md +6 -0
  56. package/assets/pitfalls/task-flow.md +20 -0
  57. package/assets/pitfalls/testing.md +6 -0
  58. package/assets/pitfalls/turn-conduct.md +35 -0
  59. package/assets/rules/browser-verification.md +19 -0
  60. package/assets/rules/deploy-flow.github.md +9 -0
  61. package/assets/rules/doc-style.md +6 -0
  62. package/assets/rules/git-workflow.azure.md +17 -2
  63. package/assets/rules/git-workflow.github.md +89 -90
  64. package/assets/rules/git-workflow.gitlab.md +12 -4
  65. package/assets/rules/lists.md +5 -0
  66. package/assets/rules/observability.needs-app.md +4 -0
  67. package/assets/rules/reuse-first.md +6 -0
  68. package/assets/rules/spec-driven.md +29 -25
  69. package/assets/rules/task-flow.md +52 -46
  70. package/assets/rules/testing.md +27 -1
  71. package/assets/rules/turn-conduct.md +46 -43
  72. package/assets/skills/agent-kit.md +9 -0
  73. package/package.json +1 -1
  74. package/rt-tools-agent-kit-0.19.0.tgz +0 -0
  75. package/rt-tools-agent-kit-0.17.0.tgz +0 -0
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Сверка объявления гарда с тем, что разбирает его тело.
4
+ *
5
+ * Гард несёт своё событие и образец вызова сам — строкой `# rt-hook:` во второй строке файла; по
6
+ * ней его подписывают в настройке агента. Тело при этом умеет больше, чем объявлено: разбирает
7
+ * имя инструмента, которого в образце нет, — и до этой ветки вызов не доходит никогда. Снаружи
8
+ * гард выглядит работающим: путь назван, файл разложен, набор сценариев зелёный, потому что зовёт
9
+ * гард напрямую с подставленным вводом и объявления не читает вовсе.
10
+ *
11
+ * Хуже того, расхождение читается как промах дерева. Дерево, подписавшее гард шире объявления,
12
+ * получает отказ сверки раскладки и сужает подписку до объявления — вместе с расхождением снимая
13
+ * работавшее покрытие. Чинится это в пакете, а платит за него дерево.
14
+ *
15
+ * Что сверяется: имена инструментов из веток `case` по имени инструмента против образца
16
+ * объявления. Комментарии и тексты отказов не читаются: имя инструмента упоминают и там, а
17
+ * судится то, на что гард ветвится.
18
+ *
19
+ * FAIL-OPEN: каталога хуков нет — сверять нечего, нулевой код.
20
+ *
21
+ * Ненулевой код возврата и перечень расхождений.
22
+ */
23
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
24
+ import { join } from 'node:path';
25
+
26
+ const ROOT = process.cwd();
27
+ const HOOKS = join(ROOT, '.claude/hooks');
28
+
29
+ /** Объявление гарда: событие и образец вызова. */
30
+ const DECLARATION = /^#\s*rt-hook:\s*(\S+)(?:[ \t]+(\S.*))?$/m;
31
+
32
+ /** Ветка `case` по имени инструмента: `Bash | mcp__webstorm__execute_tool)`. */
33
+ const TOOL_CASE = /^[ \t]*([A-Za-z_][\w-]*(?:__[\w-]+)*(?:[ \t]*\|[ \t]*[A-Za-z_][\w-]*(?:__[\w-]+)*)*)\)/;
34
+
35
+ /** Имена инструментов, на которые ветвится тело гарда. */
36
+ function branchedTools(text) {
37
+ const lines = text.split('\n');
38
+ const tools = new Set();
39
+ let inside = false;
40
+
41
+ for (const line of lines) {
42
+ const code = line.replace(/^([^#]*)#.*$/, '$1');
43
+
44
+ if (/case[ \t]+"?\$(\{)?(tool|RT_HOOK_TOOL)/.test(code) || /case[ \t]+"\$\(rt_hook_tool\)"/.test(code)) {
45
+ inside = true;
46
+ continue;
47
+ }
48
+
49
+ if (inside && /^[ \t]*esac\b/.test(code)) {
50
+ inside = false;
51
+ continue;
52
+ }
53
+
54
+ if (!inside) {
55
+ continue;
56
+ }
57
+
58
+ const branch = code.match(TOOL_CASE);
59
+ if (branch) {
60
+ for (const name of branch[1].split('|')) {
61
+ const tool = name.trim();
62
+ // Звёздочка — ветка «всё остальное», именем инструмента она не является.
63
+ if (tool && tool !== '*') {
64
+ tools.add(tool);
65
+ }
66
+ }
67
+ }
68
+ }
69
+
70
+ return [...tools];
71
+ }
72
+
73
+ /** Покрывает ли образец объявления это имя инструмента. */
74
+ function covers(matcher, tool) {
75
+ try {
76
+ return new RegExp(`^(?:${matcher})$`).test(tool);
77
+ } catch {
78
+ // Образец, который не разобрать, — своё расхождение, и молчать о нём нельзя: имя
79
+ // инструмента под ним не совпадёт ни с чем, а выглядит объявление написанным.
80
+ return false;
81
+ }
82
+ }
83
+
84
+ function main() {
85
+ if (!existsSync(HOOKS)) {
86
+ console.log('check-hook-scope: каталога хуков в дереве нет — сверять нечего');
87
+
88
+ return 0;
89
+ }
90
+
91
+ const faults = [];
92
+
93
+ for (const file of readdirSync(HOOKS).filter((one) => one.endsWith('.sh')).sort()) {
94
+ const text = readFileSync(join(HOOKS, file), 'utf8');
95
+ const declared = text.match(DECLARATION);
96
+ if (!declared || !declared[2]) {
97
+ continue;
98
+ }
99
+
100
+ const matcher = declared[2].trim();
101
+ for (const tool of branchedTools(text)) {
102
+ if (!covers(matcher, tool)) {
103
+ faults.push(`${file}: тело ветвится на «${tool}», а объявление его не называет`);
104
+ }
105
+ }
106
+ }
107
+
108
+ if (faults.length > 0) {
109
+ console.log(`check-hook-scope: расхождений ${faults.length}\n`);
110
+
111
+ for (const fault of faults) {
112
+ console.log(` ${fault}`);
113
+ }
114
+
115
+ console.log('\nВетка тела, которой нет в объявлении, не исполняется ни разу: под этим именем гард');
116
+ console.log('не зовут. Объявление правится в источнике пакета, а не в разложенной копии.');
117
+
118
+ return 1;
119
+ }
120
+
121
+ console.log('check-hook-scope: объявления гардов покрывают то, на что ветвятся их тела — сошлось');
122
+
123
+ return 0;
124
+ }
125
+
126
+ process.exit(main());
@@ -0,0 +1,195 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Сверка надстроек профиля с таблицами компаньонов.
4
+ *
5
+ * Правило говорит одно, а дерево работает по-другому — и оба места законны. Расхождение
6
+ * объявляется переменной профиля, а читатель ищет его в компаньоне правила, где перечислено, чем
7
+ * здесь зовётся сказанное правилом. Пока эти два места никто не сравнивает, компаньон обещает
8
+ * пакетное умолчание там, где дерево давно работает по-своему: правило требовало одной формы
9
+ * заголовка заявки, дерево замещало её переменной, и гард, читавший форму, номера не доставал
10
+ * вовсе — сверка номера молча не выполнялась, выглядя сошедшейся.
11
+ *
12
+ * Что сверяется: имя каждой переменной профиля, чьё значение разошлось с умолчанием пакета,
13
+ * против текстов компаньонов правил. Значение не сверяется ни с чем: сказать, верно ли оно,
14
+ * машине нечем, а назвать замещённое в компаньоне — можно.
15
+ *
16
+ * FAIL-OPEN: нет профиля, нет умолчаний, нет каталога правил — сверять нечего, нулевой код.
17
+ * Дерево вправе не держать ни того, ни другого.
18
+ *
19
+ * Ненулевой код возврата и перечень расхождений.
20
+ */
21
+ import { spawnSync } from 'node:child_process';
22
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
23
+ import { basename, join } from 'node:path';
24
+
25
+ const ROOT = process.cwd();
26
+ const PROFILE = join(ROOT, '.claude/rt-kit/project.sh');
27
+ const DEFAULTS = join(ROOT, '.claude/rt-kit/defaults/project.sh');
28
+ const RULES = join(ROOT, '.claude/skills');
29
+
30
+ /** Присвоение верхнего уровня: `RT_ИМЯ='значение'` либо `RT_ИМЯ="значение"`. */
31
+ const ASSIGN = /^(RT_[A-Z0-9_]+)=(.*)$/;
32
+
33
+ /** Умолчание пакета: `RT_ИМЯ="${RT_ИМЯ:-значение}"` — значение берётся из подстановки. */
34
+ const FALLBACK = /^RT_[A-Z0-9_]+="\$\{RT_[A-Z0-9_]+:-(.*)\}"$/;
35
+
36
+ /** Значение без окружающих кавычек: сравниваются строки, а не их запись. */
37
+ function unquote(text) {
38
+ const trimmed = text.trim();
39
+ const paired = trimmed.length > 1 && (trimmed.startsWith("'") || trimmed.startsWith('"')) && trimmed.endsWith(trimmed[0]);
40
+
41
+ return paired ? trimmed.slice(1, -1) : trimmed;
42
+ }
43
+
44
+ /** Карта «имя переменной → значение» по присвоениям верхнего уровня. */
45
+ function valuesOf(path) {
46
+ const values = new Map();
47
+
48
+ for (const line of readFileSync(path, 'utf8').split('\n')) {
49
+ const assign = line.match(ASSIGN);
50
+ if (!assign) {
51
+ continue;
52
+ }
53
+
54
+ const fallback = line.match(FALLBACK);
55
+ values.set(assign[1], fallback ? unquote(fallback[1]) : unquote(assign[2]));
56
+ }
57
+
58
+ return values;
59
+ }
60
+
61
+ /** Тексты всех компаньонов правил дерева, склеенные в один: ищется в них имя, а не место. */
62
+ function companionsText() {
63
+ if (!existsSync(RULES)) {
64
+ return null;
65
+ }
66
+
67
+ const texts = [];
68
+
69
+ for (const rule of readdirSync(RULES, { withFileTypes: true })) {
70
+ if (!rule.isDirectory()) {
71
+ continue;
72
+ }
73
+
74
+ const companion = join(RULES, rule.name, 'implementation.md');
75
+ if (existsSync(companion)) {
76
+ texts.push(readFileSync(companion, 'utf8'));
77
+ }
78
+ }
79
+
80
+ return texts.length > 0 ? texts.join('\n') : null;
81
+ }
82
+
83
+ /**
84
+ * Набор гейта пуша, собранный оболочкой: с надстройкой дерева и без неё.
85
+ *
86
+ * Собирается вызовом, а не чтением текста: набор — функция, и она смотрит на дерево — есть ли
87
+ * настройка раскладки, лежит ли конфиг оформления, исполним ли набор сценариев. Прочитанный
88
+ * текстом, он назвал бы командами то, чего в этом дереве нет вовсе.
89
+ *
90
+ * Пусто — собрать нечем: нет оболочки, нет функции, отказ вызова. Сверять тогда нечего.
91
+ */
92
+ function gateSet(withProfile) {
93
+ const source = withProfile ? `. '${DEFAULTS}'; . '${PROFILE}';` : `. '${DEFAULTS}';`;
94
+ const run = spawnSync('bash', ['-c', `${source} command -v rt_push_checks >/dev/null 2>&1 || exit 9; rt_push_checks ''`], {
95
+ cwd: ROOT,
96
+ encoding: 'utf8',
97
+ });
98
+
99
+ return run.status === 0 && typeof run.stdout === 'string' ? run.stdout.split('\n').filter(Boolean) : null;
100
+ }
101
+
102
+ /**
103
+ * Проверки, названные в наборе, — по имени файла, а не по всей строке команды.
104
+ *
105
+ * Строка целиком сверке не годится: дерево вправе позвать ту же проверку другим запускателем или
106
+ * с другим доводом, и расхождением это не является. Пропажа самой проверки — является.
107
+ */
108
+ function checksIn(lines) {
109
+ const names = new Set();
110
+
111
+ for (const line of lines) {
112
+ for (const [word] of line.matchAll(/[\w./-]+\.(?:mjs|sh)/g)) {
113
+ names.add(basename(word));
114
+ }
115
+ }
116
+
117
+ return names;
118
+ }
119
+
120
+ /**
121
+ * Проверки, которые умолчание пакета зовёт, а набор дерева — нет.
122
+ *
123
+ * Набор гейта собирается умолчанием и надстройкой, и надстройка вправе объявить функцию заново.
124
+ * Выкушенная так проверка ничем не отличима от проверки, которой в дереве нет вовсе: гейт зелен,
125
+ * потому что её никто не звал, а сводка раскладки о наборе не знает ничего — она сличает
126
+ * переменные, а тут заменена функция.
127
+ *
128
+ * Отказ в пользу работы: собрать набор нечем — строк нет, и сверка идёт дальше своим делом.
129
+ */
130
+ function cutFromGate() {
131
+ const packaged = gateSet(false);
132
+ const here = gateSet(true);
133
+ if (packaged === null || here === null) {
134
+ return [];
135
+ }
136
+
137
+ const mine = checksIn(here);
138
+
139
+ return [...checksIn(packaged)].filter((name) => !mine.has(name)).sort((first, second) => first.localeCompare(second, 'ru'));
140
+ }
141
+
142
+ function main() {
143
+ if (!existsSync(PROFILE) || !existsSync(DEFAULTS)) {
144
+ console.log('check-profile-drift: профиля дерева или умолчаний пакета нет — сверять нечего');
145
+
146
+ return 0;
147
+ }
148
+
149
+ const companions = companionsText();
150
+ if (companions === null) {
151
+ console.log('check-profile-drift: компаньонов правил в дереве нет — сверять не с чем');
152
+
153
+ return 0;
154
+ }
155
+
156
+ const cut = cutFromGate();
157
+ if (cut.length > 0) {
158
+ console.log(`check-profile-drift: набор гейта пуша не зовёт проверок умолчания: ${cut.length}\n`);
159
+
160
+ for (const name of cut) {
161
+ console.log(` ${name}`);
162
+ }
163
+
164
+ console.log('\nНабор гейта собирается умолчанием пакета и надстройкой дерева. Проверка, выкушенная');
165
+ console.log('надстройкой, ничем не отличима от проверки, которой в дереве нет: гейт зелен потому,');
166
+ console.log('что её никто не звал. Верни её в набор либо объясни отказ в компаньоне правила поставки.');
167
+
168
+ return 1;
169
+ }
170
+
171
+ const defaults = valuesOf(DEFAULTS);
172
+ const profile = valuesOf(PROFILE);
173
+ const overridden = [...profile].filter(([name, value]) => !defaults.has(name) || defaults.get(name) !== value);
174
+ const unnamed = overridden.filter(([name]) => !companions.includes(name));
175
+
176
+ if (unnamed.length > 0) {
177
+ console.log(`check-profile-drift: замещено ${overridden.length}, не названо компаньоном ${unnamed.length}\n`);
178
+
179
+ for (const [name, value] of unnamed) {
180
+ console.log(` ${name} = ${value.length > 60 ? `${value.slice(0, 57)}…` : value}`);
181
+ }
182
+
183
+ console.log('\nЗамещённое читают в компаньоне правила: там перечислено, чем здесь зовётся сказанное');
184
+ console.log('правилом. Не названное там, оно оставляет читателю пакетное умолчание вместо того, по');
185
+ console.log('чему дерево работает на самом деле.');
186
+
187
+ return 1;
188
+ }
189
+
190
+ console.log(`check-profile-drift: замещено ${overridden.length}, все названы компаньонами — сошлось`);
191
+
192
+ return 0;
193
+ }
194
+
195
+ process.exit(main());
@@ -17,7 +17,7 @@
17
17
  * владельца, а четырём незаведённым механикам скидок привязки не нашлось бы
18
18
  * вовсе. Ключ связи — сам текст правила, поэтому переформулировать его, забыв
19
19
  * поправить привязку, нельзя. Правило без привязки — намерение, и писать его
20
- * надо как `Q-N`.
20
+ * надо как открытый вопрос закона — `Q-<буква закона>-<номер>`.
21
21
  *
22
22
  * 2. КОНТРАКТ ПРОТИВ ДЕКОРАТОРОВ. Таблица процедур сверяется с тем, что
23
23
  * объявлено в `*.procedure.ts` домена: `@RequiresPermission` / `@PublicProcedure`
@@ -185,6 +185,19 @@ const DEFAULTS = {
185
185
  bot: '',
186
186
  tokenPath: '',
187
187
  reviewer: '',
188
+ /**
189
+ * Метка карточки эпика. По ней сверка находит эпики и сверяет их связь с задачами в обе
190
+ * стороны. Слово у каждого дерева своё, общего умолчания нет: не названа — связь не
191
+ * судится вовсе, потому что отличить карточку эпика от обычной задачи станет нечем.
192
+ */
193
+ epicLabel: '',
194
+ /**
195
+ * Метки, которыми в очереди работ помечен груз — присланные деревьями разборы и
196
+ * предложения. Задачами они не судятся вовсе: заголовка с номером, исполнителя и места
197
+ * на борде у них нет и не будет. Слово у каждого дерева своё, общего умолчания нет:
198
+ * пусто — сверка судит как прежде.
199
+ */
200
+ cargoLabels: [],
188
201
  },
189
202
  };
190
203
 
@@ -140,7 +140,7 @@ function checkRuleImplementation(specFile, text, mapFile, heading = '## Прав
140
140
  report(
141
141
  mapFile,
142
142
  `правило без привязки: «${head.slice(0, 60)}…» — допиши строку с \`файл:символ\`, ` +
143
- 'вердиктом «Не исполняется» с причиной либо перенеси правило в «Открытые вопросы» как Q-N'
143
+ 'вердиктом «Не исполняется» с причиной либо перенеси правило в «Открытые вопросы» закона как Q-<буква>-<номер>'
144
144
  );
145
145
  continue;
146
146
  }
@@ -263,7 +263,7 @@ function checkTracedAnchors() {
263
263
  report(
264
264
  mapFile,
265
265
  `привязка ведёт в мёртвый код: \`${symbol}\` объявлен в \`${path}\` и больше нигде не встречается — ` +
266
- 'либо правило исполняется в другом месте, либо ему место в «Открытых вопросах» как Q-N'
266
+ 'либо правило исполняется в другом месте, либо ему место в «Открытых вопросах» закона как Q-<буква>-<номер>'
267
267
  );
268
268
  }
269
269
  }
@@ -88,7 +88,7 @@ rt_push_checks_default() {
88
88
 
89
89
  for check in check-doc-paths check-specs check-file-size check-dupes check-styles \
90
90
  check-glossary check-lib-layers check-reuse check-schema-drift check-states check-state-next \
91
- check-turn-map check-archive-age check-push-gate; do
91
+ check-turn-map check-archive-age check-profile-drift check-hook-scope 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
94
94
 
@@ -225,122 +225,10 @@ rt_is_app_code_default() {
225
225
  esac
226
226
  }
227
227
 
228
- # Пишет ли команда оболочки файл. Успех да, и тогда пути из неё судятся тем же признаком,
229
- # что и путь из вызова инструмента правки.
230
- #
231
- # Гарды подписаны на инструменты правки файла, и этого мало: ту же правку кладут командой —
232
- # перенаправлением, `tee`, `sed -i`, интерпретатором с heredoc. Отбитая правка дважды за один
233
- # заход легла именно так, и не увидел этого никто: в дереве она неотличима от положенной
234
- # инструментом. Разбор — `2026-08-15-guard-denied-shell-wrote-anyway.md`.
235
- #
236
- # Список намеренно широк, и цена этого названа: команда чтения, в которой стоит имя
237
- # интерпретатора, будет отбита наравне с командой правки. Узкий список стоил бы дороже —
238
- # пропущенная форма записи возвращает обход целиком, а найти её можно только промахом.
239
- #
240
- # Перенаправление в пустое устройство и в поток ошибок снимается до разбора: файла оно не
241
- # пишет, а выглядит как перенаправление в файл. Так глушат вывод команды чтения, и без этого
242
- # `grep -rn x libs/ 2>/dev/null` судится наравне с записью — правило требуется на чтение, а
243
- # отбитий, пришедшихся не на правку файла, набирается большинство. Настоящая запись рядом с
244
- # заглушённым потоком остаётся видной: снимается перенаправление, а не команда целиком.
245
- #
246
- # Стрелка снимается там же и по той же причине. `->` и `=>` в оболочке не значат ничего, а знак
247
- # в них тот же: команда, печатавшая таблицу «путь -> правило», объявлялась пишущей и отдавала
248
- # все свои пути под суд. Туда же закрывающая скобка комментария разметки.
249
- #
250
- # Цель перенаправления сужена до знаков, из которых собирают пути. Строка цитаты разметки —
251
- # знак и слово через пробел — от записи в файл одним знаком неотличима, и различает их только
252
- # цель: за настоящим знаком стоит путь, а не слово словами. Цена названа прямо: путь, набранный
253
- # не латиницей, записью больше не считается — в дереве таких нет ни одного, а появятся, признак
254
- # придётся расширить.
255
- rt_shell_writes_default() {
256
- printf '%s' "$1" \
257
- | sed -E 's#(&|[0-9]*)>>?[[:space:]]*/dev/(null|stderr)##g; s#[0-9]*>&[0-9-]##g; s#[-=]+>##g' \
258
- | grep -Eq \
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'
260
- }
261
-
262
- # Пути, названные командой оболочки. Печатает по одному в строке; судит их зовущий.
263
- #
264
- # Разбирать оболочку по-настоящему нечем — здесь и не разбирают: из текста вынимается всё, что
265
- # похоже на путь, и каждое отдаётся признаку. Лишнее он отсеет сам, а пропущенное вернуло бы
266
- # обход. Кавычки снимаются заменой на пробел: путь внутри них тот же самый.
267
- #
268
- # Тело документа на месте путей не даёт: там лежит текст, который команда кладёт в файл, а
269
- # чужой путь, названный в нём словами, требовал бы правила под запись, которой нет. Запись
270
- # замысла так отбивалась трижды подряд, пока пути под каталогом кода не были названы иначе.
271
- # Судится заголовок команды — именно в нём стоит тот путь, куда команда пишет.
272
- #
273
- # Исключение — интерпретатор: ему код приходит телом, и путь записи стоит именно там. Признак
274
- # читается у той строки, которая тело открыла, а не у всей команды: тело принадлежит команде
275
- # своего заголовка. Прежде он читался у всего текста разом, и слово из документа отключало
276
- # вырезание целиком — строка «**Чем проверяется:** `bash projects/…`» в замысле делала запись
277
- # `plan.md` правкой кода приложения. Гард отбивал тем самым запись того файла, отсутствием
278
- # которого он же и отказывает.
279
- rt_shell_paths_default() {
280
- text="$(printf '%s' "$1" | tr "\"'\`" ' ')"
281
- text="$(printf '%s' "$text" | awk '
282
- function trim(s) { sub(/^[ \t]+/, "", s); sub(/[ \t]+$/, "", s); return s }
283
- tag != "" {
284
- if (keep) { print }
285
- if (trim($0) == tag) { tag = ""; keep = 0 }
286
- next
287
- }
288
- {
289
- print
290
- if (match($0, /<<-?[ \t]*[A-Za-z_][A-Za-z0-9_]*/)) {
291
- t = substr($0, RSTART, RLENGTH)
292
- sub(/^<<-?[ \t]*/, "", t)
293
- tag = t
294
- keep = ($0 ~ /(^|[|;&(]|[ \t])(python3?|node|ruby|perl|php|deno|bun|bash|sh|zsh)([ \t]|$)/)
295
- }
296
- }')"
297
-
298
- # Пути берутся только у тех кусков команды, которые пишут. Прежде брались у всей строки
299
- # целиком, и команда чтения, сцепленная с записью, отдавала свои пути как цели записи:
300
- # `python3 <<PY … PY` рядом с `grep -n … projects/…` отбивался за правку кода, которой в нём
301
- # не было. Отбитий, пришедшихся не на правку файла, набиралось большинство, и цену платил
302
- # тот, кто просто читал соседний файл в той же строке.
303
- #
304
- # Кусок — строка верхнего уровня, а внутри неё `;`, `&&` и `||`. Тело heredoc от своей
305
- # команды не отрывается: оно едет вместе с ней одним куском, потому что путь записи
306
- # интерпретатора стоит именно там.
307
- printf '%s' "$text" | awk '
308
- function trim(s) { sub(/^[ \t]+/, "", s); sub(/[ \t]+$/, "", s); return s }
309
- function flush( n, i, part) {
310
- if (chunk == "") { return }
311
- if (tag != "") { part = chunk; gsub(/\n/, " ", part); print part; chunk = ""; return }
312
- n = split(chunk, parts, /;|&&|\|\|/)
313
- for (i = 1; i <= n; i++) { print parts[i] }
314
- chunk = ""
315
- }
316
- tag != "" {
317
- chunk = chunk "\n" $0
318
- if (trim($0) == tag) { flush(); tag = "" }
319
- next
320
- }
321
- {
322
- chunk = $0
323
- if (match($0, /<<-?[ \t]*[A-Za-z_][A-Za-z0-9_]*/)) {
324
- t = substr($0, RSTART, RLENGTH)
325
- sub(/^<<-?[ \t]*/, "", t)
326
- tag = t
327
- next
328
- }
329
- flush()
330
- }
331
- END { if (chunk != "") { part = chunk; gsub(/\n/, " ", part); print part } }
332
- ' \
333
- | while IFS= read -r piece; do
334
- [ -z "$piece" ] && continue
335
- rt_shell_writes "$piece" || continue
336
- printf '%s' "$piece" \
337
- | tr "(),;=" ' ' \
338
- | tr '[:space:]' '\n' \
339
- | grep -E '^[A-Za-z0-9_@.-]*/[A-Za-z0-9_@./-]+$' \
340
- | sed 's|^\./||'
341
- done \
342
- | sort -u
343
- }
228
+ # Разбор команды оболочки пишет ли она и какие пути называет живёт соседним файлом:
229
+ # вдвоём с ним профиль перерастал предел длины, а читают эти две функции порознь.
230
+ # shellcheck disable=SC1090
231
+ [ -f "$(dirname "${BASH_SOURCE[0]}")/shell.sh" ] && . "$(dirname "${BASH_SOURCE[0]}")/shell.sh"
344
232
 
345
233
  # Имя ветки, с которой разрешено открывать заявку на слияние: в имени стоит номер задачи.
346
234
  # Приставка — либо род правки, либо метка очереди работ: обе формы носят номер, а он и нужен.
@@ -438,6 +326,21 @@ rt_pull_state_default() {
438
326
  printf '%s' "$state"
439
327
  }
440
328
 
329
+ # Свои открытые заявки, помеченные конфликтующими, — по строке на заявку: «#номер ветка».
330
+ # Спрашивает того же помощника очереди работ, что и два соседа выше. Пусто на выходе значит
331
+ # «конфликтующих нет», отказ кода возврата — «спросить некого», и ярус гарда пропускается:
332
+ # отбивать работу на молчании сети значило бы останавливать её всякий раз, когда её не с чем
333
+ # сверить.
334
+ rt_conflicting_pulls_default() {
335
+ command -v node >/dev/null 2>&1 || return 1
336
+ command -v jq >/dev/null 2>&1 || return 1
337
+ [ -f "${RT_BOARD_HELPER:-tools/board.mjs}" ] || return 1
338
+ state="$(node "${RT_BOARD_HELPER:-tools/board.mjs}" conflicts 2>/dev/null)" || return 1
339
+ [ -z "$state" ] && return 1
340
+ printf '%s' "$state" | jq -e 'has("conflicting")' >/dev/null 2>&1 || return 1
341
+ printf '%s' "$state" | jq -r '.conflicting[]? | "#\(.number) \(.branch)"'
342
+ }
343
+
441
344
  # Что в этом дереве считается переизобретением. По строке «образец<таб>чем заменить».
442
345
  # Образцы узкие намеренно: гард сверяет только НОВЫЙ текст, и широкий образец отбивал бы
443
346
  # правку, которая ничего нового не заводит.
@@ -494,5 +397,6 @@ rt_kit_sources_dir() { rt_kit_sources_dir_default "$@"; }
494
397
  rt_qa_decorative() { rt_qa_decorative_default "$@"; }
495
398
  rt_task_state() { rt_task_state_default "$@"; }
496
399
  rt_pull_state() { rt_pull_state_default "$@"; }
400
+ rt_conflicting_pulls() { rt_conflicting_pulls_default "$@"; }
497
401
  rt_report_body() { rt_report_body_default "$@"; }
498
402
  rt_handoff_allowed_cmd() { rt_handoff_allowed_cmd_default "$@"; }