@rt-tools/agent-kit 0.8.2 → 0.8.3
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.
- package/README.md +1 -0
- package/assets/agents/rules-reviewer.md +83 -0
- package/assets/checks/check-dupes.mjs +66 -6
- package/assets/checks/check-specs.mjs +100 -15
- package/assets/checks/rt-kit-checks.config.mjs +9 -0
- package/assets/commands/feedback.md +95 -0
- package/assets/commands/rules-review.md +98 -0
- package/assets/commands/skill-curator.md +39 -22
- package/assets/docs/GLOSSARY.md +21 -20
- package/assets/hooks/reuse-first-guard.sh +16 -2
- package/assets/hooks/skill-gate.sh +1 -1
- package/assets/hooks/task-flow-guard.sh +16 -2
- package/assets/hooks/waiting-turn-guard.sh +87 -0
- package/assets/laws/delivery.md +28 -0
- package/assets/laws/project-documentation.md +18 -0
- package/assets/laws/work-conduct.md +16 -0
- package/assets/patterns/git-workflow-commit.azure.md +74 -2
- package/assets/patterns/git-workflow-commit.github.md +75 -2
- package/assets/patterns/git-workflow-commit.gitlab.md +75 -4
- package/assets/patterns/git-workflow-docker.md +30 -0
- package/assets/patterns/task-flow-close.md +154 -47
- package/assets/patterns/task-flow-handoff.md +1 -1
- package/assets/patterns/task-flow-resume.md +3 -3
- package/assets/patterns/task-flow-start.md +32 -5
- package/assets/rules/angular-patterns.md +22 -0
- package/assets/rules/api-layer.md +25 -0
- package/assets/rules/browser-verification.md +32 -0
- package/assets/rules/component-structure.md +21 -0
- package/assets/rules/dependencies.md +22 -0
- package/assets/rules/doc-style.md +24 -0
- package/assets/rules/entity-conventions.needs-admin.md +21 -0
- package/assets/rules/entity-models.md +21 -0
- package/assets/rules/git-workflow.azure.md +50 -1
- package/assets/rules/git-workflow.github.md +48 -2
- package/assets/rules/git-workflow.gitlab.md +49 -1
- package/assets/rules/lib-layers.md +25 -0
- package/assets/rules/lists.md +27 -0
- package/assets/rules/navigation.md +21 -0
- package/assets/rules/observability.needs-app.md +23 -0
- package/assets/rules/permissions.md +23 -0
- package/assets/rules/platform-access.md +21 -0
- package/assets/rules/reuse-first.md +20 -0
- package/assets/rules/seo.md +19 -0
- package/assets/rules/shared-code.md +19 -0
- package/assets/rules/spec-driven.md +32 -0
- package/assets/rules/styling-bem.md +19 -0
- package/assets/rules/task-flow.md +107 -17
- package/assets/rules/testing.md +31 -0
- package/assets/rules/translations.md +21 -0
- package/assets/rules/typescript-conventions.md +21 -0
- package/assets/samples/specs/_template/spec.md +83 -0
- package/assets/samples/tasks/_template/grill.md +28 -0
- package/assets/samples/tasks/_template/plan.md +39 -0
- package/assets/samples/tasks/_template/progress.md +23 -0
- package/assets/skills/agent-kit.md +16 -2
- package/assets/templates/rule.md +31 -2
- package/lib/commands.d.ts.map +1 -1
- package/lib/commands.js +1 -0
- package/lib/commands.js.map +1 -1
- package/lib/config.d.ts +10 -1
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +4 -0
- package/lib/config.js.map +1 -1
- package/package.json +1 -1
- package/rt-tools-agent-kit-0.8.3.tgz +0 -0
- package/rt-tools-agent-kit-0.8.2.tgz +0 -0
package/README.md
CHANGED
|
@@ -63,6 +63,7 @@ npx agent-kit sync --check
|
|
|
63
63
|
| `agents`, `commands`, `workflows` | `.claude/` | роли, слеш-команды и многошаговые прогоны |
|
|
64
64
|
| `checks` | `tools/` | проверки, которые зовёт гейт |
|
|
65
65
|
| `templates` | `.claude/rt-kit/templates/` | формы правила, паттерна, компаньона и надстроек |
|
|
66
|
+
| `samples` | `docs/` | образцы, которые копируют в рабочий файл: папка задачи, спек домена |
|
|
66
67
|
|
|
67
68
|
Правило, паттерн и скил ложатся одинаково — все три скилы; различает их `kind` во вступлении
|
|
68
69
|
файла. Скил без закона стоит рядом с лестницей, а не в ней: он не про то, что должно быть верно
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rules-reviewer
|
|
3
|
+
description: Читает семью текстов слоя правил целиком — закон, все правила под ним и все паттерны при них — и ищет то, чего не считает машина: два текста, говорящих об одном разное, и случай, которого не назвал ни один. Файлов не правит. Использовать перед выпуском новой редакции пакета правил и после правки закона или правила.
|
|
4
|
+
tools: Read, Grep, Glob, Bash
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Ты читаешь семью текстов слоя правил и ищешь расхождения смысла. Отвечаешь **по-русски**.
|
|
8
|
+
|
|
9
|
+
Твой результат — список находок. Не правки: ты ничего не меняешь.
|
|
10
|
+
|
|
11
|
+
Промах в этих текстах уезжает ко всем деревьям разом и находит его тот, кто пошёл за правилом и
|
|
12
|
+
сделал не то. Считаемое — недостающий раздел, правило без паттерна, имя соседа, которому ничего
|
|
13
|
+
не отвечает — уже ловит проверка. Тебе остаётся то, что видно только чтением.
|
|
14
|
+
|
|
15
|
+
## Чего делать нельзя
|
|
16
|
+
|
|
17
|
+
- **Никаких git-команд вообще**, включая `status` и `diff`. Историю ведёт главный агент.
|
|
18
|
+
- Ничего не править: ни закон, ни правило, ни паттерн. Ты возвращаешь находки.
|
|
19
|
+
- Не пересказывать найденное своими словами: находка без дословной цитаты не проверяется ничем,
|
|
20
|
+
и человек не отличит настоящее расхождение от твоего прочтения.
|
|
21
|
+
- Не считать того, что уже считает проверка полноты текстов. Повтор её вывода вытесняет из
|
|
22
|
+
ответа то, ради чего тебя и звали.
|
|
23
|
+
- Не судить о дереве, в котором ты запущен. Тексты пакета переносятся, и что из них разложено
|
|
24
|
+
здесь — не их предмет.
|
|
25
|
+
|
|
26
|
+
## По чему идёшь
|
|
27
|
+
|
|
28
|
+
Семья — это **один закон, все правила под ним и все паттерны при этих правилах**. Имя закона
|
|
29
|
+
тебе даёт вызывающий. Пакет целиком не читается: текстов в нём больше десяти тысяч строк, и
|
|
30
|
+
прочитанные разом они дают крошку по каждому файлу вместо находок.
|
|
31
|
+
|
|
32
|
+
Правило принадлежит закону по полю `law:` в его шапке, паттерн правилу — по полю `rule:`.
|
|
33
|
+
Приставка имени ненадёжна: её несут не все.
|
|
34
|
+
|
|
35
|
+
**Все виды правила читаются, а не выбранный.** У одного ресурса бывает несколько редакций —
|
|
36
|
+
`git-workflow.github`, `git-workflow.gitlab`, `git-workflow.azure`. Дерево раскладывает одну, и
|
|
37
|
+
остальные не читает никто: разойдясь, они молчат до первого дерева, выбравшего другую.
|
|
38
|
+
|
|
39
|
+
**Суффикс требования видом не является.** `entity-conventions.needs-admin`,
|
|
40
|
+
`observability.needs-app` — правила, которые дерево берёт, только объявив нужную черту. В поле
|
|
41
|
+
`rule:` у паттерна стоит голое имя, без обоих суффиксов: собранная по имени с суффиксом семья
|
|
42
|
+
приходит без паттернов, и пустота эта выглядит как их отсутствие. Пришедшая к тебе семья без
|
|
43
|
+
единого паттерна — повод сказать об этом, а не молча разобрать что дали.
|
|
44
|
+
|
|
45
|
+
Ищешь два рода находок, и они разные.
|
|
46
|
+
|
|
47
|
+
**Расхождение — два места, говорящих об одном разное.** Правило требует того, что паттерн при
|
|
48
|
+
соседнем правиле запрещает. Закон называет одно число, правило — другое. Паттерн показывает
|
|
49
|
+
приём, который правило объявило отвергнутым. Сюда же — одно понятие под двумя именами и одно имя
|
|
50
|
+
над двумя понятиями.
|
|
51
|
+
|
|
52
|
+
**Пробел — случай, которого не назвал ни один текст семьи.** Статья закона, под которую ни в
|
|
53
|
+
одном правиле нет ни строки. Развилка, у которой описана одна ветка из двух. Отказ, о котором
|
|
54
|
+
сказано, что он бывает, и не сказано, что делать. Раздел «Чего из закона здесь нет», который
|
|
55
|
+
молчит о том, чего в дереве действительно нет.
|
|
56
|
+
|
|
57
|
+
**Граф хода — такое же место расхождения, как проза.** Он стоит в правиле разделом «Ход»
|
|
58
|
+
блоком `mermaid` и изображает тот же ход, что описан ниже словами: ветка графа, которой в прозе нет, и статья, до
|
|
59
|
+
которой по графу не дойти, — расхождение того же рода, что и два текста об одном. Правится он тем
|
|
60
|
+
же изменением, что и проза, и разойдясь, обе стороны читаются как действующие.
|
|
61
|
+
|
|
62
|
+
Три места, где расхождения заводятся чаще прочего, — проверь каждое:
|
|
63
|
+
|
|
64
|
+
- **«Чего из закона здесь нет»** — его не читает ни одна сверка, и неправда живёт в нём сколько
|
|
65
|
+
угодно: раздел говорит об отсутствии механизма, а механизм давно заведён, и заметить это может
|
|
66
|
+
только тот, кто пошёл его искать.
|
|
67
|
+
- **Числа** — счёт правил, статей, шагов, строк. Они стареют без единой правки рядом.
|
|
68
|
+
- **Ловушки** — их пишут по случаю и не перечитывают: приём, который они запрещают, мог с тех
|
|
69
|
+
пор стать рабочим.
|
|
70
|
+
|
|
71
|
+
## Что возвращаешь
|
|
72
|
+
|
|
73
|
+
Список находок, самая дорогая первой. Роды не смешивай — сперва расхождения, потом пробелы.
|
|
74
|
+
|
|
75
|
+
У **расхождения**: имена обоих ресурсов, обе цитаты дословно, и одной фразой — чем именно они
|
|
76
|
+
расходятся и что исполнитель сделает не так, пойдя за той или другой.
|
|
77
|
+
|
|
78
|
+
У **пробела**: имя ресурса, в котором его недостаёт, цитата места, где он должен был стоять
|
|
79
|
+
(или имя раздела, если места нет вовсе), и что случится, когда этот случай наступит. Второй
|
|
80
|
+
цитаты у пробела не бывает — не выдумывай её.
|
|
81
|
+
|
|
82
|
+
Находок нет — так и скажи. Пустой ответ дешевле выдуманного: по выдуманному правят настоящие
|
|
83
|
+
тексты.
|
|
@@ -36,8 +36,9 @@
|
|
|
36
36
|
*
|
|
37
37
|
* Ненулевой код возврата и перечень расхождений.
|
|
38
38
|
*/
|
|
39
|
-
import {
|
|
40
|
-
import {
|
|
39
|
+
import { createRequire } from 'node:module';
|
|
40
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
41
|
+
import { dirname, join } from 'node:path';
|
|
41
42
|
|
|
42
43
|
import { allowlistOf, CONFIG, ROOT } from './rt-kit-checks.config.mjs';
|
|
43
44
|
|
|
@@ -76,6 +77,56 @@ function collectFiles(dir) {
|
|
|
76
77
|
return files;
|
|
77
78
|
}
|
|
78
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Каталог объявлений внешнего пакета — разрешением модуля, а не путём в `node_modules`.
|
|
82
|
+
*
|
|
83
|
+
* Точек разрешения несколько: корень дерева и каждый его подпроект, объявивший этот пакет
|
|
84
|
+
* зависимостью. Пакет подпроекта в корне не лежит вовсе, и разрешение от корня его не находит;
|
|
85
|
+
* менеджер при этом вправе держать рядом несколько версий сразу, и обход хранилища по образцу
|
|
86
|
+
* пути выбрал бы ту, которую никто не ставит.
|
|
87
|
+
*
|
|
88
|
+
* Не нашлось — `null`, и внешние наборы просто не считаются: дерево без этого пакета должно
|
|
89
|
+
* получать сверку своих повторов, а не отказ чтения каталога.
|
|
90
|
+
*/
|
|
91
|
+
function resolveExternalDir({ package: name, dir }) {
|
|
92
|
+
for (const from of [ROOT, ...holdersOf(name)]) {
|
|
93
|
+
try {
|
|
94
|
+
const manifest = createRequire(join(from, 'package.json')).resolve(`${name}/package.json`);
|
|
95
|
+
const found = join(dirname(manifest), dir);
|
|
96
|
+
if (existsSync(found)) {
|
|
97
|
+
return found;
|
|
98
|
+
}
|
|
99
|
+
} catch {
|
|
100
|
+
// Эта точка пакета не видит — пробуется следующая.
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Подпроекты, объявившие пакет зависимостью: их манифесты и есть точки разрешения. */
|
|
108
|
+
function holdersOf(name) {
|
|
109
|
+
const found = [];
|
|
110
|
+
for (const root of SOURCE_ROOTS) {
|
|
111
|
+
if (!existsSync(join(ROOT, root))) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
for (const entry of readdirSync(join(ROOT, root), { withFileTypes: true })) {
|
|
115
|
+
const manifest = join(ROOT, root, entry.name, 'package.json');
|
|
116
|
+
if (!entry.isDirectory() || !existsSync(manifest)) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const declared = JSON.parse(readFileSync(manifest, 'utf8'));
|
|
120
|
+
const fields = [declared.dependencies, declared.peerDependencies, declared.devDependencies];
|
|
121
|
+
if (fields.some((field) => field?.[name])) {
|
|
122
|
+
found.push(join(ROOT, root, entry.name));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return found;
|
|
128
|
+
}
|
|
129
|
+
|
|
79
130
|
/** Корень либы: путь до каталога `src`. Повтор внутри одной либы повтором не считается */
|
|
80
131
|
function libOf(path) {
|
|
81
132
|
const parts = path.split('/');
|
|
@@ -116,8 +167,13 @@ const MIN_TABLE_PAIRS = 2;
|
|
|
116
167
|
/**
|
|
117
168
|
* Пакеты, чьи наборы считаются наравне с либами. Своё перечисление под уже
|
|
118
169
|
* объявленный там набор — такая же копия, как и между двумя либами.
|
|
170
|
+
*
|
|
171
|
+
* Имя пакета и каталог внутри него объявляет дерево; путь в `node_modules` здесь не
|
|
172
|
+
* зашивается. Пакет, объявленный зависимостью подпроекта, в корневом `node_modules` не лежит
|
|
173
|
+
* вовсе — менеджер держит его в своём хранилище, — и проверка кончалась отказом чтения
|
|
174
|
+
* каталога, не дойдя до сверки ни разу.
|
|
119
175
|
*/
|
|
120
|
-
const EXTERNAL_ENUM_SOURCES = [
|
|
176
|
+
const EXTERNAL_ENUM_SOURCES = CONFIG.externalEnums ?? [];
|
|
121
177
|
|
|
122
178
|
const exportsByName = new Map();
|
|
123
179
|
const settingsByName = new Map();
|
|
@@ -188,10 +244,14 @@ for (const path of SOURCE_ROOTS.flatMap((root) => collectFiles(root))) {
|
|
|
188
244
|
collectEnums(text, lib);
|
|
189
245
|
}
|
|
190
246
|
|
|
191
|
-
for (const
|
|
192
|
-
|
|
247
|
+
for (const source of EXTERNAL_ENUM_SOURCES) {
|
|
248
|
+
const dir = resolveExternalDir(source);
|
|
249
|
+
if (!dir) {
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
193
253
|
if (entry.isFile() && entry.name.endsWith('.d.ts')) {
|
|
194
|
-
collectEnums(readFileSync(join(
|
|
254
|
+
collectEnums(readFileSync(join(dir, entry.name), 'utf8'), `${source.package}/${source.dir}`);
|
|
195
255
|
}
|
|
196
256
|
}
|
|
197
257
|
}
|
|
@@ -93,8 +93,32 @@ const E2E_ROOTS = CONFIG.e2eRoots;
|
|
|
93
93
|
* выглядит как правило без якоря. Заглавные и десять букв нужны ради `api.Dockerfile`:
|
|
94
94
|
* без них правило про режим исполнения образа считалось правилом с пустой привязкой,
|
|
95
95
|
* а привязать его больше не к чему — режим объявлен ровно там.
|
|
96
|
+
*
|
|
97
|
+
* Символ — любая буква, а не только латинская: тексты, которые исполняет модель, написаны
|
|
98
|
+
* своим языком, и латиницей в них называется ровно то, что утверждения не держит — имя поля
|
|
99
|
+
* шапки, имя инструмента. Привязанное к имени поля утверждение остаётся зелёным, когда текст
|
|
100
|
+
* переписан целиком. Алфавит не перечисляется диапазонами: перечисленные молча не покрывают
|
|
101
|
+
* соседнего, и промах выглядит отсутствием привязки. Путь при этом остаётся латинским — он
|
|
102
|
+
* адрес в дереве, а не слово текста.
|
|
103
|
+
*/
|
|
104
|
+
const ANCHOR = /`([\w./-]+\.[A-Za-z]{2,10}):(\p{L}[\p{L}\p{N}_-]*|_[\w-]*)`/gu;
|
|
105
|
+
/**
|
|
106
|
+
* Явный вердикт вместо якоря: статья, которой в дереве исполняться негде. Так бывает
|
|
107
|
+
* законно — правило говорит о службе, которой дерево не держит, или о движении человека,
|
|
108
|
+
* до которого проверке не дотянуться: кнопку слияния нажимают в браузере, где хуков нет
|
|
109
|
+
* вовсе. Якорь такой статье можно поставить только в файл, который её не исполняет, —
|
|
110
|
+
* проверка примет, а читателю совратёт.
|
|
111
|
+
*
|
|
112
|
+
* Принимается вердикт с причиной, а не одно слово: пустой он становится способом закрыть
|
|
113
|
+
* любую строку, и таблица за месяц превращается в список отговорок. Порог длины — та же
|
|
114
|
+
* мера, что у обхода гарда документов: причина короче его причиной не считается.
|
|
115
|
+
*
|
|
116
|
+
* Конец слова ищется отрицательным просмотром, а не `\b`: границей слова JavaScript знает
|
|
117
|
+
* только латиницу, и после кириллической буквы её нет вовсе — вердикт не опознавался ни
|
|
118
|
+
* разу.
|
|
96
119
|
*/
|
|
97
|
-
const
|
|
120
|
+
const VERDICT = /^\s*(?:\*\*)?Не (?:исполняется|применимо|проверяется)(?![\p{L}\p{N}_])/u;
|
|
121
|
+
const VERDICT_MIN = 40;
|
|
98
122
|
/** Строка шапки, объявляющая либы, чьи процедуры домен обслуживает */
|
|
99
123
|
const PROCEDURE_ROOTS = /^\*\*Процедуры:\*\*\s*(.+)$/;
|
|
100
124
|
const BACKTICKED = /`([^`]+)`/g;
|
|
@@ -204,9 +228,19 @@ function bulletsOf(lines) {
|
|
|
204
228
|
|
|
205
229
|
const escapeForRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\-]/g, '\\$&');
|
|
206
230
|
|
|
207
|
-
/**
|
|
231
|
+
/**
|
|
232
|
+
* Символ ищется как слово: подстрока дала бы ложное совпадение на префиксе.
|
|
233
|
+
*
|
|
234
|
+
* Границы слова считаются буквой любого алфавита, а не `\b`: он в JavaScript знает буквой
|
|
235
|
+
* только латиницу, и `\bСемья\b` не совпадает ни разу — привязка на русском слове читалась
|
|
236
|
+
* как ведущая в файл, где этого слова нет, при том что слово стоит там первой же строкой.
|
|
237
|
+
*/
|
|
208
238
|
function fileHasSymbol(path, symbol) {
|
|
209
|
-
|
|
239
|
+
// Дефис здесь не экранируется: вне класса символов он ничего не значит, а под флагом `u`
|
|
240
|
+
// лишнее экранирование — уже отказ разбора. Общий экранировщик его защищает, потому что
|
|
241
|
+
// рассчитан и на класс тоже, и `task-flow` роняло всю сверку целиком.
|
|
242
|
+
const word = symbol.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
243
|
+
return new RegExp(`(?<![\\p{L}\\p{N}_])${word}(?![\\p{L}\\p{N}_])`, 'u').test(read(path));
|
|
210
244
|
}
|
|
211
245
|
|
|
212
246
|
/**
|
|
@@ -287,7 +321,12 @@ function checkRuleImplementation(specFile, text, mapFile, heading = '## Прав
|
|
|
287
321
|
if (!head || head === 'Правило' || head === 'Статья' || /^-+$/.test(head)) {
|
|
288
322
|
continue;
|
|
289
323
|
}
|
|
290
|
-
|
|
324
|
+
const cell = cells[2].trim();
|
|
325
|
+
rows.set(head, {
|
|
326
|
+
anchors: [...cells[2].matchAll(ANCHOR)],
|
|
327
|
+
verdict: VERDICT.test(cell) && cell.length >= VERDICT_MIN,
|
|
328
|
+
used: false,
|
|
329
|
+
});
|
|
291
330
|
}
|
|
292
331
|
|
|
293
332
|
for (const bullet of bullets) {
|
|
@@ -301,13 +340,17 @@ function checkRuleImplementation(specFile, text, mapFile, heading = '## Прав
|
|
|
301
340
|
report(
|
|
302
341
|
mapFile,
|
|
303
342
|
`правило без привязки: «${head.slice(0, 60)}…» — допиши строку с \`файл:символ\`, ` +
|
|
304
|
-
'либо перенеси правило в «Открытые вопросы» как Q-N'
|
|
343
|
+
'вердиктом «Не исполняется» с причиной либо перенеси правило в «Открытые вопросы» как Q-N'
|
|
305
344
|
);
|
|
306
345
|
continue;
|
|
307
346
|
}
|
|
308
347
|
row.used = true;
|
|
309
|
-
if (!row.anchors.length) {
|
|
310
|
-
report(
|
|
348
|
+
if (!row.anchors.length && !row.verdict) {
|
|
349
|
+
report(
|
|
350
|
+
mapFile,
|
|
351
|
+
`у правила «${head.slice(0, 60)}…» пустая привязка — поставь \`файл:символ\` ` +
|
|
352
|
+
'либо вердикт «Не исполняется», «Не применимо», «Не проверяется» с причиной'
|
|
353
|
+
);
|
|
311
354
|
}
|
|
312
355
|
for (const [, path, symbol] of row.anchors) {
|
|
313
356
|
if (!exists(path)) {
|
|
@@ -883,9 +926,11 @@ for (const domain of domains) {
|
|
|
883
926
|
|
|
884
927
|
const found = walk(base, (name) => name === 'scenarios.md').flatMap(parseScenarios);
|
|
885
928
|
|
|
886
|
-
// Префикс
|
|
887
|
-
//
|
|
888
|
-
//
|
|
929
|
+
// Префикс принадлежит домену вместе с его поддоменами, а не отдельному каталогу. Домен
|
|
930
|
+
// делится тогда, когда его спек перерос предел длины, и сценарии переезжают в поддомены
|
|
931
|
+
// прежними: номер — единственное, чем сценарий связан с заголовком теста, и своя нумерация
|
|
932
|
+
// у каждого поддомена означала бы пересчёт всех номеров разом. Два префикса в одном спеке
|
|
933
|
+
// по-прежнему означают, что предмет описан дважды.
|
|
889
934
|
const prefixesOf = new Map();
|
|
890
935
|
for (const scenario of found) {
|
|
891
936
|
const dir = dirname(scenario.file);
|
|
@@ -906,11 +951,11 @@ for (const domain of domains) {
|
|
|
906
951
|
}
|
|
907
952
|
for (const prefix of prefixes) {
|
|
908
953
|
const owner = prefixOwners.get(prefix);
|
|
909
|
-
if (owner && owner !==
|
|
954
|
+
if (owner && owner !== base) {
|
|
910
955
|
report(dir, `префикс \`SC-${prefix}\` уже занят — \`${owner}\`; по номеру не видно, чей сценарий`);
|
|
911
956
|
continue;
|
|
912
957
|
}
|
|
913
|
-
prefixOwners.set(prefix,
|
|
958
|
+
prefixOwners.set(prefix, base);
|
|
914
959
|
}
|
|
915
960
|
}
|
|
916
961
|
|
|
@@ -1008,12 +1053,52 @@ const isProposedLaw = (file) => /^\*\*Статус:\*\*\s*предложен/m.t
|
|
|
1008
1053
|
.filter(([law, file]) => !ruled.has(law) && !isProposedLaw(file))
|
|
1009
1054
|
.forEach(([, file]) => report(file, 'у закона нет ни одного правила — заведи скил с `law:` на него'));
|
|
1010
1055
|
|
|
1056
|
+
/**
|
|
1057
|
+
* Имена паттернов, которые дерево при раскладке пропустило: ключ `skip` в настройке проекта.
|
|
1058
|
+
*
|
|
1059
|
+
* Пропуск — выбор дерева, а не забытая работа: правило о процедурах бэкенда ложится и в дерево,
|
|
1060
|
+
* где бэкенда нет вовсе. Требовать там паттерн значит требовать завести файл, которому нечего
|
|
1061
|
+
* сказать, — и единственным способом позеленеть становится снятие пропуска.
|
|
1062
|
+
*/
|
|
1063
|
+
const skippedPatterns = () => {
|
|
1064
|
+
const path = '.claude/rt-kit.json';
|
|
1065
|
+
if (!exists(path)) {
|
|
1066
|
+
return new Set();
|
|
1067
|
+
}
|
|
1068
|
+
try {
|
|
1069
|
+
const skip = JSON.parse(read(path)).skip ?? [];
|
|
1070
|
+
|
|
1071
|
+
return new Set(skip.map((resource) => resource.match(/^patterns\/(.+)\.md$/)?.[1]).filter(Boolean));
|
|
1072
|
+
} catch {
|
|
1073
|
+
return new Set();
|
|
1074
|
+
}
|
|
1075
|
+
};
|
|
1076
|
+
|
|
1077
|
+
/**
|
|
1078
|
+
* Раздел «Паттерны» самого правила — единственное место, где связь видна без файла паттерна:
|
|
1079
|
+
* пропущенного файла в дереве нет, и поле `rule:` в нём спросить не у кого.
|
|
1080
|
+
*/
|
|
1081
|
+
// Флага `m` здесь нет намеренно: с ним `$` означает конец строки, и раздел кончается на первом
|
|
1082
|
+
// же переводе строки — пустым. Начало заголовка поэтому ищется своей парой, а не якорем.
|
|
1083
|
+
const PATTERNS_HEADING = /(?:^|\n)## Паттерны\n([\s\S]*?)(?=\n## |$)/;
|
|
1084
|
+
const patternsNamedBy = (text) => [...(text.match(PATTERNS_HEADING)?.[1] ?? '').matchAll(/^-\s+`([\w-]+)`/gm)].map(([, found]) => found);
|
|
1085
|
+
|
|
1086
|
+
const skipped = skippedPatterns();
|
|
1087
|
+
|
|
1011
1088
|
for (const file of walk('.claude/skills', (name) => name === 'SKILL.md')) {
|
|
1012
|
-
const
|
|
1089
|
+
const text = read(file);
|
|
1090
|
+
const head = frontMatterOf(text);
|
|
1013
1091
|
const name = nameOf(head);
|
|
1014
|
-
if (
|
|
1015
|
-
|
|
1092
|
+
if (!/^kind:\s*rule\s*$/m.test(head) || !name || patterned.has(name)) {
|
|
1093
|
+
continue;
|
|
1016
1094
|
}
|
|
1095
|
+
|
|
1096
|
+
const named = patternsNamedBy(text);
|
|
1097
|
+
if (named.length > 0 && named.every((pattern) => skipped.has(pattern))) {
|
|
1098
|
+
continue;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
report(file, 'у правила нет ни одного паттерна — заведи скил с `rule:` на него');
|
|
1017
1102
|
}
|
|
1018
1103
|
|
|
1019
1104
|
checkTracedAnchors();
|
|
@@ -71,6 +71,15 @@ const DEFAULTS = {
|
|
|
71
71
|
* числе на каждой правке, а не о длине файла.
|
|
72
72
|
*/
|
|
73
73
|
fileSizeLimit: 500,
|
|
74
|
+
/**
|
|
75
|
+
* Внешние пакеты, чьи перечисления считаются наравне с либами: своё перечисление под уже
|
|
76
|
+
* объявленный там набор — такая же копия, как и между двумя либами. Каждая запись — имя
|
|
77
|
+
* пакета и каталог объявлений внутри него; каталог ищется разрешением модуля, а не путём в
|
|
78
|
+
* `node_modules`: пакет, объявленный зависимостью подпроекта, в корне дерева не лежит вовсе,
|
|
79
|
+
* и зашитый путь на такой раскладке верным не бывает никогда. Пусто — внешние наборы не
|
|
80
|
+
* считаются.
|
|
81
|
+
*/
|
|
82
|
+
externalEnums: [],
|
|
74
83
|
/** Корни сквозных тестов; пусто — их в дереве нет. */
|
|
75
84
|
e2eRoots: ['apps/site-e2e', 'apps/admin-e2e'],
|
|
76
85
|
/** Корни бэкенда: у него нет ни компонентов, ни шаблонов, и часть признаков к нему не применяется. */
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Слово о слое правил, сказанное посреди работы, ложится блоком в файл предложений
|
|
3
|
+
argument-hint: '<что мешает, чего не хватило, что сработало не так>'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Положи слово пользователя блоком в файл предложений. Слово: `$ARGUMENTS`
|
|
7
|
+
|
|
8
|
+
Зовётся **посреди работы**, а не после неё: то, обо что споткнулись час назад, к разбору закрытой
|
|
9
|
+
задачи уже забыто, а сама реплика живёт до конца сессии и умирает вместе с ней. Разбор закрытой
|
|
10
|
+
задачи смотрит на загруженное и на ход работы; реплик он не видит вовсе.
|
|
11
|
+
|
|
12
|
+
Команда ничего не отправляет. Она кладёт блок на диск, а увозит его обычная отправка, позванная
|
|
13
|
+
отдельно. Скажи об этом пользователю последней строкой — иначе положенное читается как
|
|
14
|
+
отправленное, и он ждёт ответа, которого никто не посылал.
|
|
15
|
+
|
|
16
|
+
## 1. Пойми, о чём слово
|
|
17
|
+
|
|
18
|
+
Слово пользователя — проза: «вот это правило мешает», «гейт требует не то», «этого в правилах
|
|
19
|
+
нет вовсе». Твоё дело — перевести её в три вещи:
|
|
20
|
+
|
|
21
|
+
- **адрес** — куда правка идёт;
|
|
22
|
+
- **ресурс** — что именно правится;
|
|
23
|
+
- **готовый текст** — ровно то, что вставить.
|
|
24
|
+
|
|
25
|
+
Адрес один из трёх, и выбирается он не по удобству:
|
|
26
|
+
|
|
27
|
+
пакет — правка ресурса @rt-tools/agent-kit; верна любому дереву и уезжает наружу
|
|
28
|
+
компаньон — implementation.md рядом с правилом: имена этого дерева и привязка статей
|
|
29
|
+
дерево — надстройка этого дерева; наружу не уезжает никогда
|
|
30
|
+
|
|
31
|
+
Ресурс называется идентификатором пакета — `rules/styling-bem.md`, `hooks/skill-gate.sh`,
|
|
32
|
+
`patterns/git-workflow-commit.md`, — а у адресов «компаньон» и «дерево» путём в дереве.
|
|
33
|
+
|
|
34
|
+
**Непонятный адрес спрашивается, а не назначается по догадке.** Неверный адрес уводит правку в
|
|
35
|
+
чужой репозиторий: сказанное о своём дереве уезжает всем, а сказанное обо всех остаётся лежать
|
|
36
|
+
дома. Пока пользователь не ответил, в файл не записывается ничего.
|
|
37
|
+
|
|
38
|
+
Спрашивать не надо, когда адрес виден из самого слова: речь о правиле, которое ты только что
|
|
39
|
+
грузил, — это `пакет`; речь об именах, путях и командах этого дерева — `компаньон` или `дерево`.
|
|
40
|
+
|
|
41
|
+
## 2. Найди файл сегодняшнего дня
|
|
42
|
+
|
|
43
|
+
Блок ложится туда же, куда его кладёт разбор закрытой задачи: у них один адресат и один формат, а
|
|
44
|
+
второй файл рядом означал бы, что отправка читает два места, а пользователь не помнит, в каком
|
|
45
|
+
лежит его слово.
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
ls .claude/rt-kit/proposals/$(date +%F)-*.md 2>/dev/null
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Нашёлся — дописывай в него. Не нашёлся — заведи с образца, назвав по ветке:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
mkdir -p .claude/rt-kit/proposals
|
|
55
|
+
cp .claude/rt-kit/templates/proposal.md \
|
|
56
|
+
".claude/rt-kit/proposals/$(date +%F)-$(git branch --show-current).md"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
У свежего файла шапка образца остаётся, а незаполненный образец блока — `rules/<правило>.md` со
|
|
60
|
+
скобками — заменяется твоим блоком: отправка такой образец пропускает, но лежит он молчаливым
|
|
61
|
+
мусором.
|
|
62
|
+
|
|
63
|
+
## 3. Напиши блок
|
|
64
|
+
|
|
65
|
+
Форма заголовка — не украшение: по ней отправка отбирает то, что уезжает наружу. Блок без адреса
|
|
66
|
+
в заголовке не уедет никуда и останется лежать молча.
|
|
67
|
+
|
|
68
|
+
```markdown
|
|
69
|
+
## <адрес> · <ресурс>
|
|
70
|
+
|
|
71
|
+
- **место:** раздел «<заголовок>», в конец
|
|
72
|
+
- **повод:** что в этой работе пошло не так без этого правила
|
|
73
|
+
|
|
74
|
+
> Готовый текст правки — ровно то, что вставить, в стиле соседних правил: по-русски,
|
|
75
|
+
> утверждением, без воды.
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Повод пишется от случая, а не от желания: «здесь было неудобно» правилом не становится. Слово
|
|
79
|
+
пользователя пересказывается его смыслом, а не твоими выводами о том, как надо было бы.
|
|
80
|
+
|
|
81
|
+
**Адреса этого дерева в тексте блока не бывает** — ни пути, ни имени корня, ни имени чужого
|
|
82
|
+
репозитория: файл уезжает в чужой репозиторий целиком. Найденный адрес отбивает отправку с
|
|
83
|
+
номером строки, и это проверка, а не напоминание. Правь текст, а не обходи её.
|
|
84
|
+
|
|
85
|
+
## 4. Скажи, что вышло
|
|
86
|
+
|
|
87
|
+
Одной строкой: в какой файл лёг блок, сколько блоков в нём теперь и чем он уедет.
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
npx agent-kit propose --dry-run # что уехало бы
|
|
91
|
+
npx agent-kit propose # отправить груз в приём
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Отправка увозит блоки с адресом «пакет» и метит их отправленными; блоки «компаньон» и «дерево»
|
|
95
|
+
остаются лежать — их правит тот, кто работает в этом дереве.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Смысловое ревью семьи текстов слоя правил — закон, его правила и паттерны при них
|
|
3
|
+
argument-hint: '<имя закона>'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Прогони смысловое ревью одной семьи текстов пакета правил. Семья: `$ARGUMENTS`
|
|
7
|
+
|
|
8
|
+
Зовётся **в репозитории самого пакета**, а не в дереве, где он стоит: судятся исходные тексты
|
|
9
|
+
ресурсов, и лежат они только здесь. В чужом дереве лежит разложенная копия выбранных ресурсов, а
|
|
10
|
+
не семья целиком, и путей, по которым команда ходит, в нём нет вовсе: обход по ним вернёт
|
|
11
|
+
пустоту, неотличимую от «такого закона не бывает». Каталог ресурсов ниже назван так, как он
|
|
12
|
+
зовётся в репозитории пакета.
|
|
13
|
+
|
|
14
|
+
Считаемое ловит проверка полноты текстов — недостающий раздел, правило без паттерна, имя соседа,
|
|
15
|
+
которому в наборе ничего не отвечает. Здесь ищется то, чего она не считает: два текста,
|
|
16
|
+
говорящих об одном разное, и случай, которого не назвал ни один. Ответ роли не повторяется от
|
|
17
|
+
запуска к запуску, поэтому в гейт он не идёт и ветку не отбивает.
|
|
18
|
+
|
|
19
|
+
## 1. Пойми, какая семья
|
|
20
|
+
|
|
21
|
+
Семья зовётся именем закона — без пути и без расширения: `work-conduct`, `delivery`,
|
|
22
|
+
`project-documentation`.
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
ls projects/agent-kit/assets/laws/*.md projects/agent-kit/assets/laws/*/*.md
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Имени нет** — назови человеку перечень имён и остановись. Догадываться по похожести нельзя:
|
|
29
|
+
ревью уйдёт на чужую семью, и его находки человек примет за находки о своей.
|
|
30
|
+
|
|
31
|
+
**Названо несколько** — гони по одной, по очереди, и ответы не смешивай: находка семьи читается
|
|
32
|
+
вместе с её законом, а сваленные в кучу они теряют адрес.
|
|
33
|
+
|
|
34
|
+
**Довода нет вовсе** — потребуй имя и напечатай перечень. Умолчания здесь нет: «первый
|
|
35
|
+
попавшийся закон» даёт прогон, неотличимый от осмысленного.
|
|
36
|
+
|
|
37
|
+
**Закон лежит в слое приложения** — это законный случай, а не промах: у денег, локалей и доступа
|
|
38
|
+
семья такая же. Читается он оттуда же, где лежит.
|
|
39
|
+
|
|
40
|
+
## 2. Собери семью
|
|
41
|
+
|
|
42
|
+
Правило принадлежит закону полем `law:`, паттерн правилу — полем `rule:`. Приставка имени
|
|
43
|
+
ненадёжна: её несут не все паттерны.
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
LAW=<имя закона>
|
|
47
|
+
grep -l "^law: $LAW\$" projects/agent-kit/assets/rules/*.md
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Дальше по каждому найденному правилу — его паттерны. Имя правила для поиска берётся голым: без
|
|
51
|
+
вида и без требования — оба стоят суффиксами в имени файла, а поле `rule:` у паттерна несёт
|
|
52
|
+
только само имя.
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
RULE=<имя правила: первое слово имени файла, до первой точки>
|
|
56
|
+
grep -l "^rule: $RULE\$" projects/agent-kit/assets/patterns/*.md
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**Виды берутся все.** `git-workflow.github`, `git-workflow.gitlab`, `git-workflow.azure` — три
|
|
60
|
+
редакции одного правила: дерево раскладывает одну, а расходятся они молча.
|
|
61
|
+
|
|
62
|
+
**Суффикс требования — не вид.** `entity-conventions.needs-admin`, `observability.needs-app` —
|
|
63
|
+
это правила, которые дерево берёт, только когда объявило нужную черту. Имя с этим суффиксом в
|
|
64
|
+
поле `rule:` не стоит ни у одного паттерна: поиск по нему возвращает пустоту, и семья уезжает в
|
|
65
|
+
ревью без паттернов вовсе — молча, потому что пустой ответ выглядит как «паттернов нет».
|
|
66
|
+
|
|
67
|
+
**У закона нет ни одного правила** — скажи это человеку и остановись. Читать один закон нечем:
|
|
68
|
+
расхождение живёт между двумя текстами, а пробел уровня статьи без правил под ней — не находка
|
|
69
|
+
ревью, а отсутствие целого слоя. Сам по себе такой закон стоит разговора: его находит и проверка
|
|
70
|
+
полноты текстов, и она же скажет, сколько их.
|
|
71
|
+
|
|
72
|
+
## 3. Запусти роль
|
|
73
|
+
|
|
74
|
+
Инструментом `Agent`, `subagent_type: 'rules-reviewer'`. В промпт — имя закона и полный список
|
|
75
|
+
путей: закон, все его правила со всеми видами, все паттерны при них. Список собираешь ты: роль
|
|
76
|
+
git-команд не зовёт и историю не читает.
|
|
77
|
+
|
|
78
|
+
Одна роль на семью. Веер из нескольких ролей со сведением ответов здесь не заводится: взгляд на
|
|
79
|
+
предмет один — два текста об одном говорят разное, — а сведение превращает дословные цитаты в
|
|
80
|
+
пересказ.
|
|
81
|
+
|
|
82
|
+
## 4. Покажи находки человеку
|
|
83
|
+
|
|
84
|
+
**Как есть.** Роль возвращает цитаты дословно, и пересказ их портит: по пересказу человек не
|
|
85
|
+
отличит настоящее расхождение от прочтения роли.
|
|
86
|
+
|
|
87
|
+
Порядок сохрани: сперва расхождения — у каждого два места и две цитаты, — потом пробелы, у
|
|
88
|
+
которых второго места нет.
|
|
89
|
+
|
|
90
|
+
По каждой находке скажи своё: согласен или нет и почему. Правку не вноси — тексты правил
|
|
91
|
+
действуют на все будущие сессии всех деревьев, и решает по ним человек.
|
|
92
|
+
|
|
93
|
+
Находка, с которой человек согласился, идёт дальше двумя путями, и выбирает он:
|
|
94
|
+
|
|
95
|
+
- **правится тут же** — если это правка текста пакета и она укладывается в текущую работу;
|
|
96
|
+
- **уходит задачей** в очередь работ — если тянет за собой код, раскладку или другой закон.
|
|
97
|
+
|
|
98
|
+
Пустой ответ роли — тоже результат: скажи, что находок нет, и не выдумывай их из вежливости.
|