@rt-tools/agent-kit 0.16.1 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/assets/checks/board-epics.github.mjs +123 -0
  2. package/assets/checks/board-gh.github.mjs +121 -0
  3. package/assets/checks/board-paths.github.mjs +88 -0
  4. package/assets/checks/board-runs.github.mjs +8 -0
  5. package/assets/checks/board-titles.github.mjs +66 -0
  6. package/assets/checks/board.github.mjs +77 -73
  7. package/assets/checks/check-board.github.mjs +56 -7
  8. package/assets/checks/check-file-size.mjs +10 -2
  9. package/assets/checks/check-glossary.mjs +170 -0
  10. package/assets/checks/check-hook-scope.mjs +126 -0
  11. package/assets/checks/check-profile-drift.mjs +195 -0
  12. package/assets/checks/check-push-gate.mjs +59 -1
  13. package/assets/checks/check-schema-drift.mjs +12 -5
  14. package/assets/checks/check-specs.mjs +1 -1
  15. package/assets/checks/rt-kit-checks.config.mjs +13 -0
  16. package/assets/checks/spec-anchors.mjs +2 -2
  17. package/assets/checks/spec-contract.mjs +9 -0
  18. package/assets/defaults/gate-map.sh +13 -4
  19. package/assets/defaults/project.sh +31 -108
  20. package/assets/defaults/shell.sh +139 -0
  21. package/assets/docs/GLOSSARY.md +1 -1
  22. package/assets/hooks/browser-device-id.sh +20 -4
  23. package/assets/hooks/browser-guard-device-id.sh +5 -2
  24. package/assets/hooks/browser-guard-no-other-drivers.sh +41 -2
  25. package/assets/hooks/claim-guard.sh +22 -1
  26. package/assets/hooks/docs-guard.sh +1 -1
  27. package/assets/hooks/exam-guard.sh +85 -12
  28. package/assets/hooks/git-guard-delivery-conflict.sh +85 -0
  29. package/assets/hooks/git-guard-delivery-folder.sh +19 -0
  30. package/assets/hooks/git-guard-delivery-signature.sh +14 -5
  31. package/assets/hooks/git-guard-delivery.sh +54 -2
  32. package/assets/hooks/git-guard-push-tests.sh +21 -1
  33. package/assets/hooks/grill-gate-ask.sh +25 -0
  34. package/assets/hooks/grill-gate.sh +75 -11
  35. package/assets/hooks/lint-after-edit.sh +44 -16
  36. package/assets/hooks/proposal-guard.sh +11 -4
  37. package/assets/hooks/rule-source-guard.sh +8 -13
  38. package/assets/hooks/skill-gate-layers.sh +7 -0
  39. package/assets/hooks/skill-gate.sh +29 -0
  40. package/assets/hooks/task-context-load.sh +32 -0
  41. package/assets/hooks/task-flow-context.sh +194 -0
  42. package/assets/hooks/task-flow-draft-guard.sh +109 -0
  43. package/assets/hooks/task-flow-guard.sh +27 -159
  44. package/assets/hooks/turn-exit-guard.sh +53 -88
  45. package/assets/hooks/waiting-turn-guard.sh +65 -2
  46. package/assets/hooks/window-fill-guard.sh +5 -1
  47. package/assets/hooks/work-start-guard.sh +166 -0
  48. package/assets/hooks/write-targets.sh +24 -0
  49. package/assets/laws/delivery.md +19 -0
  50. package/assets/laws/frontend-application.md +4 -0
  51. package/assets/laws/verifiability.md +44 -0
  52. package/assets/laws/work-conduct.md +15 -0
  53. package/assets/patterns/browser-verification-measure.md +3 -1
  54. package/assets/patterns/browser-verification-stand.md +17 -4
  55. package/assets/patterns/doc-style-write.md +62 -2
  56. package/assets/patterns/git-workflow-commit.github.md +33 -5
  57. package/assets/patterns/git-workflow-docker.md +22 -0
  58. package/assets/patterns/git-workflow-freshness.md +87 -0
  59. package/assets/patterns/git-workflow-merge.md +41 -0
  60. package/assets/patterns/git-workflow-migration.md +11 -0
  61. package/assets/patterns/git-workflow-pr.github.md +12 -1
  62. package/assets/patterns/git-workflow-restart.md +18 -0
  63. package/assets/patterns/git-workflow-secrets.md +14 -0
  64. package/assets/patterns/git-workflow-stack.md +93 -1
  65. package/assets/patterns/lib-layers-move.md +4 -0
  66. package/assets/patterns/spec-driven-domain.md +47 -2
  67. package/assets/patterns/spec-driven-rule.md +16 -4
  68. package/assets/patterns/spec-driven-sweep.md +57 -0
  69. package/assets/patterns/status-report-table.github.md +1 -1
  70. package/assets/patterns/task-flow-archive.md +58 -16
  71. package/assets/patterns/task-flow-close.md +106 -23
  72. package/assets/patterns/task-flow-handoff.md +14 -2
  73. package/assets/patterns/task-flow-resume.md +35 -8
  74. package/assets/patterns/task-flow-start.md +60 -22
  75. package/assets/patterns/testing-e2e.md +23 -0
  76. package/assets/patterns/turn-entry-map.md +1 -1
  77. package/assets/pitfalls/agent-kit.md +71 -3
  78. package/assets/pitfalls/doc-style.md +15 -0
  79. package/assets/pitfalls/git-workflow.github.md +88 -0
  80. package/assets/pitfalls/spec-driven.md +22 -0
  81. package/assets/pitfalls/task-flow.md +113 -0
  82. package/assets/pitfalls/testing.md +6 -0
  83. package/assets/pitfalls/turn-conduct.md +49 -0
  84. package/assets/rules/browser-verification.md +58 -0
  85. package/assets/rules/deploy-flow.azure.md +8 -0
  86. package/assets/rules/deploy-flow.github.md +27 -0
  87. package/assets/rules/deploy-flow.gitlab.md +8 -0
  88. package/assets/rules/doc-style.md +20 -0
  89. package/assets/rules/git-workflow.azure.md +22 -2
  90. package/assets/rules/git-workflow.github.md +127 -118
  91. package/assets/rules/git-workflow.gitlab.md +17 -4
  92. package/assets/rules/lists.md +5 -0
  93. package/assets/rules/observability.needs-app.md +4 -0
  94. package/assets/rules/reuse-first.md +14 -0
  95. package/assets/rules/shared-code.md +5 -0
  96. package/assets/rules/spec-driven.md +33 -15
  97. package/assets/rules/task-flow.md +132 -126
  98. package/assets/rules/testing.md +41 -2
  99. package/assets/rules/turn-conduct.md +73 -57
  100. package/assets/rules/turn-entry.md +6 -0
  101. package/assets/samples/tasks/_template/grill.md +5 -0
  102. package/assets/samples/tasks/_template/plan.md +3 -0
  103. package/assets/skills/agent-kit.md +108 -76
  104. package/assets/templates/postmortem.md +5 -1
  105. package/bin/agent-kit.d.ts.map +1 -1
  106. package/bin/agent-kit.js +30 -6
  107. package/bin/agent-kit.js.map +1 -1
  108. package/lib/catalog.d.ts.map +1 -1
  109. package/lib/catalog.js +2 -1
  110. package/lib/catalog.js.map +1 -1
  111. package/lib/commands.d.ts.map +1 -1
  112. package/lib/commands.js +96 -7
  113. package/lib/commands.js.map +1 -1
  114. package/lib/hooks-map.d.ts +26 -0
  115. package/lib/hooks-map.d.ts.map +1 -1
  116. package/lib/hooks-map.js +58 -2
  117. package/lib/hooks-map.js.map +1 -1
  118. package/lib/sections.d.ts +6 -0
  119. package/lib/sections.d.ts.map +1 -1
  120. package/lib/sections.js +19 -0
  121. package/lib/sections.js.map +1 -1
  122. package/lib/shipment.d.ts +2 -0
  123. package/lib/shipment.d.ts.map +1 -1
  124. package/lib/shipment.fixture.d.ts +5 -0
  125. package/lib/shipment.fixture.d.ts.map +1 -1
  126. package/lib/shipment.fixture.js +7 -0
  127. package/lib/shipment.fixture.js.map +1 -1
  128. package/lib/shipment.js +13 -1
  129. package/lib/shipment.js.map +1 -1
  130. package/lib/sync.d.ts +35 -3
  131. package/lib/sync.d.ts.map +1 -1
  132. package/lib/sync.js +59 -8
  133. package/lib/sync.js.map +1 -1
  134. package/package.json +1 -1
  135. package/rt-tools-agent-kit-0.18.0.tgz +0 -0
  136. package/rt-tools-agent-kit-0.16.1.tgz +0 -0
@@ -18,7 +18,9 @@
18
18
  * `rt_push_checks` профиля; она сверяется с его выводом, потому что объявление
19
19
  * без исполнения — та же дыра. Исключение — причина, по которой шага в гейте
20
20
  * нет; пустая причина исключением не считается: она единственное, чем
21
- * постоянная дыра отличается от забытой строки.
21
+ * постоянная дыра отличается от забытой строки. Причина, называющая задачу,
22
+ * судится ещё и на живость этой задачи: отсрочка со сроком и отсрочка без срока
23
+ * выглядят одинаково, пока номер никто не спросил.
22
24
  *
23
25
  * Дерево без файла конвейера сверки не получает: проверка, падающая там, где
24
26
  * конвейера нет, отбивала бы работу вместо промаха.
@@ -34,6 +36,8 @@ import { CONFIG, ROOT } from './rt-kit-checks.config.mjs';
34
36
  const GATE = CONFIG.pushGate ?? {};
35
37
  const PIPELINE = GATE.pipelineFile ?? '';
36
38
  const DECLARED = GATE.steps ?? {};
39
+ /** Ключ задач дерева: по нему номер в причине отличается от версии, порта и года. */
40
+ const TASK_KEY = CONFIG.board?.taskKey ?? '';
37
41
 
38
42
  /**
39
43
  * Профиль дерева ищется той же цепочкой, что и у гарда пуша: умолчание пакета, поверх него
@@ -85,9 +89,39 @@ rt_push_checks ""`;
85
89
  }
86
90
  }
87
91
 
92
+ /**
93
+ * Номера задач, названные причиной. Ключ дерева обязателен: без него «663» неотличимо от порта,
94
+ * года и номера редакции, и проверка спрашивала бы очередь работ обо всём подряд.
95
+ */
96
+ function taskNumbers(reason) {
97
+ if (!TASK_KEY) {
98
+ return [];
99
+ }
100
+ return [...reason.matchAll(new RegExp(`\\b${TASK_KEY}-(\\d+)\\b`, 'g'))].map((match) => Number(match[1]));
101
+ }
102
+
103
+ /**
104
+ * Очередь работ ищется разрешением модуля, а не собирается здесь: у каждого хостинга она своя, и
105
+ * дерево, у которого её нет, судится как прежде. Ярус тот же, что у гарда поставки: есть чем
106
+ * спросить — спрашивает, нет — молчит.
107
+ */
108
+ async function boardModule() {
109
+ if (!TASK_KEY) {
110
+ return null;
111
+ }
112
+ try {
113
+ const module = await import('./board.mjs');
114
+ return typeof module.taskState === 'function' ? module : null;
115
+ } catch {
116
+ return null;
117
+ }
118
+ }
119
+
88
120
  const steps = pipelineSteps();
89
121
  const checks = gateChecks();
90
122
  const problems = [];
123
+ /** Задачи, названные причинами исключений: спрашиваются пачкой после разбора всех шагов. */
124
+ const deferrals = [];
91
125
 
92
126
  for (const step of steps) {
93
127
  const declaration = DECLARED[step];
@@ -103,6 +137,10 @@ for (const step of steps) {
103
137
  const reason = String(declaration.skip ?? '').trim();
104
138
  if (!reason) {
105
139
  problems.push(`шаг конвейера «${step}» объявлен исключением без причины — пустая причина не считается`);
140
+ continue;
141
+ }
142
+ for (const number of taskNumbers(reason)) {
143
+ deferrals.push({ step, number });
106
144
  }
107
145
  continue;
108
146
  }
@@ -126,6 +164,26 @@ for (const step of Object.keys(DECLARED)) {
126
164
  }
127
165
  }
128
166
 
167
+ /**
168
+ * Мёртвый номер в причине делает исключение бессрочным, не сказав об этом ни строкой. Первый же
169
+ * отказ сети кончает опрос целиком: спрашивать остальные незачем, а падать проверке, которую
170
+ * гоняют в самолёте, — тем более.
171
+ */
172
+ const board = deferrals.length > 0 ? await boardModule() : null;
173
+ if (board) {
174
+ for (const { step, number } of deferrals) {
175
+ let state;
176
+ try {
177
+ state = board.taskState(number);
178
+ } catch {
179
+ break;
180
+ }
181
+ if (state && state.exists === false) {
182
+ problems.push(`шаг конвейера «${step}» отложен до задачи ${TASK_KEY}-${number}, а такой задачи в очереди работ нет — отсрочка бессрочная`);
183
+ }
184
+ }
185
+ }
186
+
129
187
  if (problems.length > 0) {
130
188
  console.error(`check-push-gate: расхождений ${problems.length}\n`);
131
189
  problems.forEach((problem) => console.error(` ${problem}`));
@@ -76,6 +76,13 @@ function shadowAddresses(url) {
76
76
  return { shadowName, shadowUrl: shadow.toString(), serviceUrl: service.toString() };
77
77
  }
78
78
 
79
+ // Код, которым проверка объявляет, что смотреть было не на что. Прежде каждый такой выход был
80
+ // нулём: строка о пропуске уходила в вывод, а в сводке гейта пуша ноль стоял рядом с
81
+ // пройденными проверками и ничем от них не отличался — набор читался как проверенный целиком.
82
+ // Число знает и гард пуша: он называет пропущенное вслух, не отбивая пуш, потому что проверка,
83
+ // которой нечего смотреть, поломкой не является.
84
+ const SKIP = Number(process.env.RT_SKIP_CODE ?? 7);
85
+
79
86
  function prisma(args, url) {
80
87
  return spawnSync('npx', ['prisma', ...args], {
81
88
  cwd: ROOT,
@@ -98,7 +105,7 @@ async function withServiceClient(serviceUrl, run) {
98
105
  if (SERVER_DOWN_CODES.includes(error?.code)) {
99
106
  console.log('check-schema-drift: база недоступна — сверять негде');
100
107
 
101
- return 0;
108
+ return SKIP;
102
109
  }
103
110
  throw error;
104
111
  }
@@ -117,26 +124,26 @@ async function main() {
117
124
  if (!CONFIG.schemaFile) {
118
125
  console.log('check-schema-drift: имя файла схемы не задано — сверять нечего');
119
126
 
120
- return 0;
127
+ return SKIP;
121
128
  }
122
129
 
123
130
  if (!existsSync(join(ROOT, CONFIG.schemaFile))) {
124
131
  console.log('check-schema-drift: схемы нет — сверять нечего');
125
132
 
126
- return 0;
133
+ return SKIP;
127
134
  }
128
135
 
129
136
  const url = databaseUrl();
130
137
  if (!url) {
131
138
  console.log('check-schema-drift: адрес базы не задан — сверять негде');
132
139
 
133
- return 0;
140
+ return SKIP;
134
141
  }
135
142
 
136
143
  if (PRODUCTION_MARKS.some((mark) => url.includes(mark))) {
137
144
  console.log('check-schema-drift: адрес боевой — проверка туда не ходит');
138
145
 
139
- return 0;
146
+ return SKIP;
140
147
  }
141
148
 
142
149
  const { shadowName, shadowUrl, serviceUrl } = shadowAddresses(url);
@@ -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
  }
@@ -64,6 +64,15 @@ function contractRows(text) {
64
64
  continue;
65
65
  }
66
66
  const permission = (cells[1].match(/`([^`]+)`/) || [])[1] || cells[1];
67
+ // Строка с кодом отказа во второй ячейке процедурой не считается. Раздел «Контракта»
68
+ // держит две разные таблицы: список процедур с правами и список кодов отказа с поводами,
69
+ // — и по форме строки они неотличимы, обе несут значение в кавычках первой ячейкой.
70
+ // Пока разбор брал любую, спек с объявленными процедурами получал свою таблицу кодов
71
+ // прочитанной как список процедур: каждая её строка становилась процедурой, которой в
72
+ // домене нет.
73
+ if (/^\d{3}$/.test(permission.trim())) {
74
+ continue;
75
+ }
67
76
  rows.push({ line: index, name, permission: permission.trim(), short: name.split('.').pop() });
68
77
  }
69
78
 
@@ -40,6 +40,12 @@ skill_for_default() {
40
40
  case "$kind" in
41
41
  edit)
42
42
  case "$target" in
43
+ # Собранное дерево кодом не бывает, и правкой его никто не занимается: путь к
44
+ # артефакту приходит из команды, которая его ЗАПУСКАЕТ. Под артефактом лежат те
45
+ # же имена каталогов, что и под исходником, поэтому ветка стоит первой — любая
46
+ # ниже совпала бы с ним и потребовала правило, которое работе ничего не даёт.
47
+ */node_modules/* | */dist/* | */build/* | */.nx/* | */coverage/*) return 0 ;;
48
+
43
49
  # Надстройка над разложенным текстом — правка того же текста: путь другой, а
44
50
  # предмет тот же. Ветка стоит первой, потому что путь надстройки кончается тем
45
51
  # же именем файла, что и разложенная копия, и ветки ниже разобрали бы её по
@@ -93,10 +99,13 @@ skill_for_default() {
93
99
  # Своё правило линтера кода пишется по тем же соглашениям, что и код под ним.
94
100
  */eslint-rules/*) printf '%s\n' 'typescript-conventions' ;;
95
101
 
96
- # Схема хранилища и её миграции: порядок каталогов лексикографический, а метку
97
- # времени ставит инструмент в момент заведения — цепочка ломается молча и падает
98
- # только накатом с нуля, то есть уже после слияния. Правило живёт при поставке.
99
- */schema.prisma | */prisma/migrations/*) printf '%s\n' 'git-workflow' ;;
102
+ # Схема хранилища, её миграции и настройка клиента: порядок каталогов
103
+ # лексикографический, а метку времени ставит инструмент в момент заведения —
104
+ # цепочка ломается молча и падает только накатом с нуля, то есть уже после
105
+ # слияния. Настройка клиента стоит здесь же: её читает тот же накат, а правилась
106
+ # она без правила под рукой. Правило живёт при поставке.
107
+ */schema.prisma | */prisma/migrations/* | */prisma.config.ts)
108
+ printf '%s\n' 'git-workflow' ;;
100
109
  # Конвейер и образ: проверки решают, что вообще гоняется до слияния, а образ —
101
110
  # что приезжает на прод. И то и другое правилось без единого правила поставки.
102
111
  */.github/workflows/*.yml | */.gitlab-ci.yml | */azure-pipelines*.yml)
@@ -87,8 +87,8 @@ 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 \
91
- check-turn-map check-archive-age check-push-gate; do
90
+ check-glossary check-lib-layers check-reuse check-schema-drift check-states check-state-next \
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
 
@@ -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}"
@@ -222,112 +225,10 @@ rt_is_app_code_default() {
222
225
  esac
223
226
  }
224
227
 
225
- # Пишет ли команда оболочки файл. Успех да, и тогда пути из неё судятся тем же признаком,
226
- # что и путь из вызова инструмента правки.
227
- #
228
- # Гарды подписаны на инструменты правки файла, и этого мало: ту же правку кладут командой —
229
- # перенаправлением, `tee`, `sed -i`, интерпретатором с heredoc. Отбитая правка дважды за один
230
- # заход легла именно так, и не увидел этого никто: в дереве она неотличима от положенной
231
- # инструментом. Разбор — `2026-08-15-guard-denied-shell-wrote-anyway.md`.
232
- #
233
- # Список намеренно широк, и цена этого названа: команда чтения, в которой стоит имя
234
- # интерпретатора, будет отбита наравне с командой правки. Узкий список стоил бы дороже —
235
- # пропущенная форма записи возвращает обход целиком, а найти её можно только промахом.
236
- #
237
- # Перенаправление в пустое устройство и в поток ошибок снимается до разбора: файла оно не
238
- # пишет, а выглядит как перенаправление в файл. Так глушат вывод команды чтения, и без этого
239
- # `grep -rn x libs/ 2>/dev/null` судится наравне с записью — правило требуется на чтение, а
240
- # отбитий, пришедшихся не на правку файла, набирается большинство. Настоящая запись рядом с
241
- # заглушённым потоком остаётся видной: снимается перенаправление, а не команда целиком.
242
- rt_shell_writes_default() {
243
- printf '%s' "$1" \
244
- | sed -E 's#(&|[0-9]*)>>?[[:space:]]*/dev/(null|stderr)##g; s#[0-9]*>&[0-9-]##g' \
245
- | grep -Eq \
246
- '>>?[[:space:]]*[^|&>[:space:]]|\btee\b|\bsed\b[^|]*-i|\bperl\b[^|]*-i|\bpython3?\b|\bnode\b|\bruby\b|\bdd\b[^|]*of=|\bcp\b|\bmv\b|\brm\b|\btouch\b|\btruncate\b|\binstall\b|\bpatch\b|\bgit[[:space:]]+(checkout|restore|apply|stash)\b'
247
- }
248
-
249
- # Пути, названные командой оболочки. Печатает по одному в строке; судит их зовущий.
250
- #
251
- # Разбирать оболочку по-настоящему нечем — здесь и не разбирают: из текста вынимается всё, что
252
- # похоже на путь, и каждое отдаётся признаку. Лишнее он отсеет сам, а пропущенное вернуло бы
253
- # обход. Кавычки снимаются заменой на пробел: путь внутри них тот же самый.
254
- #
255
- # Тело документа на месте путей не даёт: там лежит текст, который команда кладёт в файл, а
256
- # чужой путь, названный в нём словами, требовал бы правила под запись, которой нет. Запись
257
- # замысла так отбивалась трижды подряд, пока пути под каталогом кода не были названы иначе.
258
- # Судится заголовок команды — именно в нём стоит тот путь, куда команда пишет.
259
- #
260
- # Исключение — интерпретатор: ему код приходит телом, и путь записи стоит именно там. Признак
261
- # читается у той строки, которая тело открыла, а не у всей команды: тело принадлежит команде
262
- # своего заголовка. Прежде он читался у всего текста разом, и слово из документа отключало
263
- # вырезание целиком — строка «**Чем проверяется:** `bash projects/…`» в замысле делала запись
264
- # `plan.md` правкой кода приложения. Гард отбивал тем самым запись того файла, отсутствием
265
- # которого он же и отказывает.
266
- rt_shell_paths_default() {
267
- text="$(printf '%s' "$1" | tr "\"'\`" ' ')"
268
- text="$(printf '%s' "$text" | awk '
269
- function trim(s) { sub(/^[ \t]+/, "", s); sub(/[ \t]+$/, "", s); return s }
270
- tag != "" {
271
- if (keep) { print }
272
- if (trim($0) == tag) { tag = ""; keep = 0 }
273
- next
274
- }
275
- {
276
- print
277
- if (match($0, /<<-?[ \t]*[A-Za-z_][A-Za-z0-9_]*/)) {
278
- t = substr($0, RSTART, RLENGTH)
279
- sub(/^<<-?[ \t]*/, "", t)
280
- tag = t
281
- keep = ($0 ~ /(^|[|;&(]|[ \t])(python3?|node|ruby|perl|php|deno|bun|bash|sh|zsh)([ \t]|$)/)
282
- }
283
- }')"
284
-
285
- # Пути берутся только у тех кусков команды, которые пишут. Прежде брались у всей строки
286
- # целиком, и команда чтения, сцепленная с записью, отдавала свои пути как цели записи:
287
- # `python3 <<PY … PY` рядом с `grep -n … projects/…` отбивался за правку кода, которой в нём
288
- # не было. Отбитий, пришедшихся не на правку файла, набиралось большинство, и цену платил
289
- # тот, кто просто читал соседний файл в той же строке.
290
- #
291
- # Кусок — строка верхнего уровня, а внутри неё `;`, `&&` и `||`. Тело heredoc от своей
292
- # команды не отрывается: оно едет вместе с ней одним куском, потому что путь записи
293
- # интерпретатора стоит именно там.
294
- printf '%s' "$text" | awk '
295
- function trim(s) { sub(/^[ \t]+/, "", s); sub(/[ \t]+$/, "", s); return s }
296
- function flush( n, i, part) {
297
- if (chunk == "") { return }
298
- if (tag != "") { part = chunk; gsub(/\n/, " ", part); print part; chunk = ""; return }
299
- n = split(chunk, parts, /;|&&|\|\|/)
300
- for (i = 1; i <= n; i++) { print parts[i] }
301
- chunk = ""
302
- }
303
- tag != "" {
304
- chunk = chunk "\n" $0
305
- if (trim($0) == tag) { flush(); tag = "" }
306
- next
307
- }
308
- {
309
- chunk = $0
310
- if (match($0, /<<-?[ \t]*[A-Za-z_][A-Za-z0-9_]*/)) {
311
- t = substr($0, RSTART, RLENGTH)
312
- sub(/^<<-?[ \t]*/, "", t)
313
- tag = t
314
- next
315
- }
316
- flush()
317
- }
318
- END { if (chunk != "") { part = chunk; gsub(/\n/, " ", part); print part } }
319
- ' \
320
- | while IFS= read -r piece; do
321
- [ -z "$piece" ] && continue
322
- rt_shell_writes "$piece" || continue
323
- printf '%s' "$piece" \
324
- | tr "(),;=" ' ' \
325
- | tr '[:space:]' '\n' \
326
- | grep -E '^[A-Za-z0-9_@.-]*/[A-Za-z0-9_@./-]+$' \
327
- | sed 's|^\./||'
328
- done \
329
- | sort -u
330
- }
228
+ # Разбор команды оболочки пишет ли она и какие пути называет живёт соседним файлом:
229
+ # вдвоём с ним профиль перерастал предел длины, а читают эти две функции порознь.
230
+ # shellcheck disable=SC1090
231
+ [ -f "$(dirname "${BASH_SOURCE[0]}")/shell.sh" ] && . "$(dirname "${BASH_SOURCE[0]}")/shell.sh"
331
232
 
332
233
  # Имя ветки, с которой разрешено открывать заявку на слияние: в имени стоит номер задачи.
333
234
  # Приставка — либо род правки, либо метка очереди работ: обе формы носят номер, а он и нужен.
@@ -372,6 +273,12 @@ RT_TASK_BOT="${RT_TASK_BOT:-}"
372
273
  RT_PULL_TOKEN_VAR="${RT_PULL_TOKEN_VAR:-}"
373
274
  RT_PULL_TOKEN_HINT="${RT_PULL_TOKEN_HINT:-}"
374
275
 
276
+ # Раздел, который тело заявки обязано нести с минуты открытия: решение о слиянии принимается на
277
+ # её странице, где переписки нет вовсе, и сказанного вслух там не остаётся. Умолчание молчит —
278
+ # заголовок пишется языком заявки, а чужих слов пакет не знает: не названный деревом, раздел не
279
+ # судится вовсе.
280
+ RT_PULL_BODY_SECTION="${RT_PULL_BODY_SECTION:-}"
281
+
375
282
  # Команда перевода задачи между колонками очереди работ и имя первой колонки — той, из которой
376
283
  # задача уходит, когда её берут в работу. Имя без умолчания: колонки дерево называет своими
377
284
  # словами, а выдуманное не совпало бы ни с чем и молча выключило бы проверку колонки.
@@ -419,6 +326,21 @@ rt_pull_state_default() {
419
326
  printf '%s' "$state"
420
327
  }
421
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
+
422
344
  # Что в этом дереве считается переизобретением. По строке «образец<таб>чем заменить».
423
345
  # Образцы узкие намеренно: гард сверяет только НОВЫЙ текст, и широкий образец отбивал бы
424
346
  # правку, которая ничего нового не заводит.
@@ -475,5 +397,6 @@ rt_kit_sources_dir() { rt_kit_sources_dir_default "$@"; }
475
397
  rt_qa_decorative() { rt_qa_decorative_default "$@"; }
476
398
  rt_task_state() { rt_task_state_default "$@"; }
477
399
  rt_pull_state() { rt_pull_state_default "$@"; }
400
+ rt_conflicting_pulls() { rt_conflicting_pulls_default "$@"; }
478
401
  rt_report_body() { rt_report_body_default "$@"; }
479
402
  rt_handoff_allowed_cmd() { rt_handoff_allowed_cmd_default "$@"; }
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env bash
2
+ # Разбор команды оболочки: пишет ли она и какие пути называет.
3
+ #
4
+ # Умолчание пакета, часть профиля дерева — грузится из `project.sh`, рядом с которым лежит.
5
+ # Отдельным файлом потому, что вдвоём они перерастают предел длины, а читают их порознь: одному
6
+ # нужен признак записи, другому — цель.
7
+
8
+ # Пишет ли команда оболочки файл. Успех — да, и тогда пути из неё судятся тем же признаком,
9
+ # что и путь из вызова инструмента правки.
10
+ #
11
+ # Гарды подписаны на инструменты правки файла, и этого мало: ту же правку кладут командой —
12
+ # перенаправлением, `tee`, `sed -i`, интерпретатором с heredoc. Отбитая правка дважды за один
13
+ # заход легла именно так, и не увидел этого никто: в дереве она неотличима от положенной
14
+ # инструментом. Разбор — `2026-08-15-guard-denied-shell-wrote-anyway.md`.
15
+ #
16
+ # Список намеренно широк, и цена этого названа: команда чтения, в которой стоит имя
17
+ # интерпретатора, будет отбита наравне с командой правки. Узкий список стоил бы дороже —
18
+ # пропущенная форма записи возвращает обход целиком, а найти её можно только промахом.
19
+ #
20
+ # Перенаправление в пустое устройство и в поток ошибок снимается до разбора: файла оно не
21
+ # пишет, а выглядит как перенаправление в файл. Так глушат вывод команды чтения, и без этого
22
+ # `grep -rn x libs/ 2>/dev/null` судится наравне с записью — правило требуется на чтение, а
23
+ # отбитий, пришедшихся не на правку файла, набирается большинство. Настоящая запись рядом с
24
+ # заглушённым потоком остаётся видной: снимается перенаправление, а не команда целиком.
25
+ #
26
+ # Стрелка снимается там же и по той же причине. `->` и `=>` в оболочке не значат ничего, а знак
27
+ # в них тот же: команда, печатавшая таблицу «путь -> правило», объявлялась пишущей и отдавала
28
+ # все свои пути под суд. Туда же закрывающая скобка комментария разметки.
29
+ #
30
+ # Цель перенаправления сужена до знаков, из которых собирают пути. Строка цитаты разметки —
31
+ # знак и слово через пробел — от записи в файл одним знаком неотличима, и различает их только
32
+ # цель: за настоящим знаком стоит путь, а не слово словами. Цена названа прямо: путь, набранный
33
+ # не латиницей, записью больше не считается — в дереве таких нет ни одного, а появятся, признак
34
+ # придётся расширить.
35
+ rt_shell_writes_default() {
36
+ printf '%s' "$1" \
37
+ | sed -E 's#(&|[0-9]*)>>?[[:space:]]*/dev/(null|stderr)##g; s#[0-9]*>&[0-9-]##g; s#[-=]+>##g' \
38
+ | grep -Eq \
39
+ '>>?[[: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'
40
+ }
41
+
42
+ # Пути, названные командой оболочки. Печатает по одному в строке; судит их зовущий.
43
+ #
44
+ # Разбирать оболочку по-настоящему нечем — здесь и не разбирают: из текста вынимается всё, что
45
+ # похоже на путь, и каждое отдаётся признаку. Лишнее он отсеет сам, а пропущенное вернуло бы
46
+ # обход. Кавычки снимаются заменой на пробел: путь внутри них тот же самый.
47
+ #
48
+ # Тело документа на месте путей не даёт: там лежит текст, который команда кладёт в файл, а
49
+ # чужой путь, названный в нём словами, требовал бы правила под запись, которой нет. Запись
50
+ # замысла так отбивалась трижды подряд, пока пути под каталогом кода не были названы иначе.
51
+ # Судится заголовок команды — именно в нём стоит тот путь, куда команда пишет.
52
+ #
53
+ # Исключение — интерпретатор: ему код приходит телом, и путь записи стоит именно там. Признак
54
+ # читается у той строки, которая тело открыла, а не у всей команды: тело принадлежит команде
55
+ # своего заголовка. Прежде он читался у всего текста разом, и слово из документа отключало
56
+ # вырезание целиком — строка «**Чем проверяется:** `bash projects/…`» в замысле делала запись
57
+ # `plan.md` правкой кода приложения. Гард отбивал тем самым запись того файла, отсутствием
58
+ # которого он же и отказывает.
59
+ rt_shell_paths_default() {
60
+ text="$(printf '%s' "$1" | tr "\"'\`" ' ')"
61
+ text="$(printf '%s' "$text" | awk '
62
+ function trim(s) { sub(/^[ \t]+/, "", s); sub(/[ \t]+$/, "", s); return s }
63
+ tag != "" {
64
+ if (keep) { print }
65
+ if (trim($0) == tag) { tag = ""; keep = 0 }
66
+ next
67
+ }
68
+ {
69
+ print
70
+ if (match($0, /<<-?[ \t]*[A-Za-z_][A-Za-z0-9_]*/)) {
71
+ t = substr($0, RSTART, RLENGTH)
72
+ sub(/^<<-?[ \t]*/, "", t)
73
+ tag = t
74
+ keep = ($0 ~ /(^|[|;&(]|[ \t])(python3?|node|ruby|perl|php|deno|bun|bash|sh|zsh)([ \t]|$)/)
75
+ }
76
+ }')"
77
+
78
+ # Пути берутся только у тех кусков команды, которые пишут. Прежде брались у всей строки
79
+ # целиком, и команда чтения, сцепленная с записью, отдавала свои пути как цели записи:
80
+ # `python3 <<PY … PY` рядом с `grep -n … projects/…` отбивался за правку кода, которой в нём
81
+ # не было. Отбитий, пришедшихся не на правку файла, набиралось большинство, и цену платил
82
+ # тот, кто просто читал соседний файл в той же строке.
83
+ #
84
+ # Кусок — строка верхнего уровня, а внутри неё `;`, `&&` и `||`. Тело heredoc от своей
85
+ # команды не отрывается: оно едет вместе с ней одним куском, потому что путь записи
86
+ # интерпретатора стоит именно там.
87
+ printf '%s' "$text" | awk '
88
+ function trim(s) { sub(/^[ \t]+/, "", s); sub(/[ \t]+$/, "", s); return s }
89
+ function flush( n, i, part) {
90
+ if (chunk == "") { return }
91
+ if (tag != "") { part = chunk; gsub(/\n/, " ", part); print part; chunk = ""; return }
92
+ n = split(chunk, parts, /;|&&|\|\|/)
93
+ for (i = 1; i <= n; i++) { print parts[i] }
94
+ chunk = ""
95
+ }
96
+ tag != "" {
97
+ chunk = chunk "\n" $0
98
+ if (trim($0) == tag) { flush(); tag = "" }
99
+ next
100
+ }
101
+ {
102
+ chunk = $0
103
+ if (match($0, /<<-?[ \t]*[A-Za-z_][A-Za-z0-9_]*/)) {
104
+ t = substr($0, RSTART, RLENGTH)
105
+ sub(/^<<-?[ \t]*/, "", t)
106
+ tag = t
107
+ next
108
+ }
109
+ flush()
110
+ }
111
+ END { if (chunk != "") { part = chunk; gsub(/\n/, " ", part); print part } }
112
+ ' \
113
+ | while IFS= read -r piece; do
114
+ [ -z "$piece" ] && continue
115
+ rt_shell_writes "$piece" || continue
116
+ # Кусок, который пишет одним лишь перенаправлением, отдаёт цель записи, а не всякое
117
+ # похожее на путь слово из своего текста. Имя файла, названное в аргументе или в
118
+ # содержимом, правкой этого файла не является: команда чтения, сцепленная с записью
119
+ # в свой файл, требовала правила по чужому имени и кончала ход — цена такого отказа
120
+ # равна цене настоящего срабатывания, потому что обойти его нечем.
121
+ #
122
+ # Прочие виды записи разбираются по-прежнему: у правки по месту, копирования,
123
+ # переноса и интерпретатора путь стоит в самой команде и не в одном месте.
124
+ if ! printf '%s' "$piece" | grep -Eq '\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'; then
125
+ printf '%s' "$piece" \
126
+ | grep -oE '(>>?[[:space:]]*|\btee\b([[:space:]]+-a)?[[:space:]]+)[A-Za-z0-9_@.~/-]+' \
127
+ | sed -E 's/^(>>?|tee([[:space:]]+-a)?)[[:space:]]*//' \
128
+ | grep -E '^[A-Za-z0-9_@.-]*/[A-Za-z0-9_@./-]+$' \
129
+ | sed 's|^\./||'
130
+ continue
131
+ fi
132
+ printf '%s' "$piece" \
133
+ | tr "(),;=" ' ' \
134
+ | tr '[:space:]' '\n' \
135
+ | grep -E '^[A-Za-z0-9_@.-]*/[A-Za-z0-9_@./-]+$' \
136
+ | sed 's|^\./||'
137
+ done \
138
+ | sort -u
139
+ }
@@ -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'