@rt-tools/agent-kit 0.5.0 → 0.5.1
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 +5 -0
- package/assets/checks/check-doc-paths.mjs +200 -30
- package/assets/checks/check-specs.mjs +42 -7
- package/assets/checks/rt-kit-checks.config.mjs +12 -0
- package/assets/commands/next-session.md +122 -0
- package/assets/defaults/gate-map.sh +21 -1
- package/assets/defaults/project.sh +25 -0
- package/assets/docs/GLOSSARY.md +74 -0
- package/assets/hooks/window-fill-guard.sh +150 -0
- package/assets/laws/code-structure.md +10 -0
- package/assets/laws/delivery.md +8 -1
- package/assets/laws/project-documentation.md +4 -0
- package/assets/laws/work-conduct.md +8 -0
- package/assets/patterns/spec-driven-domain.md +19 -0
- package/assets/patterns/task-flow-close.md +4 -4
- package/assets/patterns/task-flow-handoff.md +115 -0
- package/assets/patterns/task-flow-resume.md +2 -2
- package/assets/patterns/task-flow-start.md +4 -0
- package/assets/rules/doc-style.md +39 -1
- package/assets/rules/git-workflow.azure.md +24 -0
- package/assets/rules/git-workflow.github.md +23 -0
- package/assets/rules/git-workflow.gitlab.md +23 -0
- package/assets/rules/spec-driven.md +8 -0
- package/assets/rules/task-flow.md +46 -0
- 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 +3 -1
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +2 -0
- package/lib/config.js.map +1 -1
- package/lib/hooks-map.d.ts +15 -3
- package/lib/hooks-map.d.ts.map +1 -1
- package/lib/hooks-map.js +47 -11
- package/lib/hooks-map.js.map +1 -1
- package/lib/sync.d.ts.map +1 -1
- package/lib/sync.js +2 -5
- package/lib/sync.js.map +1 -1
- package/package.json +1 -1
- package/rt-tools-agent-kit-0.5.1.tgz +0 -0
- package/rt-tools-agent-kit-0.5.0.tgz +0 -0
package/README.md
CHANGED
|
@@ -260,6 +260,11 @@ npx agent-kit init --laws access,delivery,verifiability
|
|
|
260
260
|
`skill-curator` приезжает и слеш-командой: она собирает сводку о задаче и список загруженных
|
|
261
261
|
правил, без которых разбор выродится в пересказ.
|
|
262
262
|
|
|
263
|
+
Вторая команда, `next-session`, закрывает заход: приводит дерево к главной ветке — переходом на
|
|
264
|
+
неё, если работа шла по правилу и отчёт влит, и вливанием в текущую ветку во всех прочих
|
|
265
|
+
случаях, — снимает влитые локальные ветки, называет невлитые и пишет передачу для следующего
|
|
266
|
+
захода. Незакоммиченная правка её останавливает до первого действия; поставки она не касается.
|
|
267
|
+
|
|
263
268
|
## Как этим пользуются в дереве
|
|
264
269
|
|
|
265
270
|
Всё, что выше, пакет объясняет человеку. Агенту то же самое объясняет скил `agent-kit` —
|
|
@@ -1,19 +1,28 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Проверка того, что
|
|
3
|
+
* Проверка того, что адреса, названные в документации, существуют.
|
|
4
4
|
*
|
|
5
5
|
* Документ, ссылающийся на исчезнувший файл, хуже отсутствующего: он выглядит
|
|
6
6
|
* действующей справкой и уводит читателя в каталог, которого нет. Накапливается
|
|
7
7
|
* это молча — перекладка дерева правит код и ломает текст, а текст никто не
|
|
8
|
-
* собирает.
|
|
9
|
-
* не существовало 80.
|
|
8
|
+
* собирает.
|
|
10
9
|
*
|
|
11
|
-
* Считаются только
|
|
12
|
-
* команды и вывод, где
|
|
13
|
-
* репозитория. Шаблоны (`*`, `<…>`, `{…}`) пропускаются: это форма
|
|
10
|
+
* Считаются только адреса в обратных кавычках и вне блоков кода: в блоках лежат
|
|
11
|
+
* команды и вывод, где путь до собранного — результат сборки, а не файл
|
|
12
|
+
* репозитория. Шаблоны (`*`, `<…>`, `{…}`) пропускаются: это форма адреса, а не адрес.
|
|
14
13
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* Адрес бывает трёх родов, и все три судятся одинаково: укоренённый в дереве путь,
|
|
15
|
+
* голое имя файла и каталог. Каталогами занята половина таблиц «Где это лежит», и
|
|
16
|
+
* проверка, знающая только строку с расширением, их не видит вовсе.
|
|
17
|
+
*
|
|
18
|
+
* Документы, которые по устройству говорят о несуществующем — планы будущего, архив
|
|
19
|
+
* и папки задач, — из проверки выведены. Там же переносимый текст: его адреса
|
|
20
|
+
* принадлежат тому дереву, куда правило ложится, и в этом они примеры, а не ссылки.
|
|
21
|
+
*
|
|
22
|
+
* Вторым заходом сверяется полнота указателя каталога: обзорный документ перечисляет
|
|
23
|
+
* записи таблицей, и читатель ищет по ней, а не обходом. Это обратная сторона той же
|
|
24
|
+
* договорённости — не только адрес из текста ведёт в файл, но и файл назван в тексте,
|
|
25
|
+
* по которому его ищут.
|
|
17
26
|
*
|
|
18
27
|
* Ненулевой код возврата и перечень расхождений.
|
|
19
28
|
*/
|
|
@@ -24,20 +33,47 @@ import { join } from 'node:path';
|
|
|
24
33
|
import { allowlistOf, CONFIG, ROOT } from './rt-kit-checks.config.mjs';
|
|
25
34
|
|
|
26
35
|
const ALLOWLIST = allowlistOf('doc-paths');
|
|
27
|
-
// `worktrees` — копии репозитория под
|
|
28
|
-
// ветки, а проверка ищет
|
|
29
|
-
// красный сквозной прогон на ветке, которая её не заводила.
|
|
36
|
+
// `worktrees` — копии репозитория под каталогом агента: их документы описывают раскладку
|
|
37
|
+
// своей ветки, а проверка ищет адреса в дереве текущей. Одна брошенная копия дала 73
|
|
38
|
+
// расхождения и красный сквозной прогон на ветке, которая её не заводила.
|
|
30
39
|
const SKIPPED_DIRS = CONFIG.skippedDirs;
|
|
31
40
|
/**
|
|
32
|
-
* Архив описывает раскладку, бывшую на момент записи. Править в нём
|
|
41
|
+
* Архив описывает раскладку, бывшую на момент записи. Править в нём адреса — значит
|
|
33
42
|
* переписывать историю задним числом, поэтому он выведен из проверки целиком.
|
|
34
43
|
*/
|
|
35
44
|
const ARCHIVE_DIR = CONFIG.archiveDir;
|
|
36
|
-
/**
|
|
45
|
+
/**
|
|
46
|
+
* Папка задачи описывает ход работы, и снятое она называет по имени: раздел находок
|
|
47
|
+
* перечисляет ровно то, чего в дереве нет. Отличить такое упоминание от ссылки машине
|
|
48
|
+
* нечем, а живёт папка до слияния — поэтому она выведена из проверки, как архив.
|
|
49
|
+
*/
|
|
50
|
+
const TASKS_DIR = CONFIG.tasksDir.endsWith('/') ? CONFIG.tasksDir : `${CONFIG.tasksDir}/`;
|
|
51
|
+
/**
|
|
52
|
+
* Каталоги, чей указатель сверяется с содержимым. Каталог, выведенный из проверки адресов,
|
|
53
|
+
* иначе не судит ничто: запись, приехавшая слиянием соседней ветки, остаётся неназванной, а
|
|
54
|
+
* читатель ищет по указателю. Сверенный руками указатель расходится снова через сутки.
|
|
55
|
+
*/
|
|
56
|
+
const INDEXED_DIRS = (CONFIG.indexedDirs ?? []).map((dir) => (dir.endsWith('/') ? dir : `${dir}/`));
|
|
57
|
+
/**
|
|
58
|
+
* Исходники переносимых текстов: правило, которое ложится в другое дерево, называет адреса
|
|
59
|
+
* того дерева. Разложенная копия узнаётся по шапке, а исходник шапки не несёт — её ставит
|
|
60
|
+
* раскладка, — поэтому его каталог называется настройкой.
|
|
61
|
+
*/
|
|
62
|
+
const PORTABLE_DIRS = (CONFIG.portableDirs ?? []).map((dir) => (dir.endsWith('/') ? dir : `${dir}/`));
|
|
63
|
+
/** Шапка разложенного файла: версия пакета, ресурс и сумма тела. */
|
|
64
|
+
const STAMP_LINE = /rt-kit\s+v\S+\s+·\s+\S+\s+·\s+[0-9a-f]{12}/;
|
|
65
|
+
/** Шапка встаёт первой строкой тела, а тело начинается после вступления скила. */
|
|
66
|
+
const STAMP_LOOKAHEAD = 12;
|
|
67
|
+
/** Расширения, по которым голое имя считается файлом, а не именем сущности */
|
|
37
68
|
const EXTENSIONS = 'ts|mts|cts|js|mjs|cjs|json|jsonc|scss|css|html|proto|conf|ya?ml|sh|md|sql|txt|xml|svg|webp|png|ico|env|Dockerfile|lock';
|
|
38
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Берётся любая строка в кавычках: каталог расширения не несёт, и требовать его в самой
|
|
71
|
+
* выборке значило бы не видеть половину таблиц «Где это лежит». Отсев — в `looksLikePath`.
|
|
72
|
+
*/
|
|
73
|
+
const PATH_IN_BACKTICKS = /`([^`\n]+?)`/g;
|
|
39
74
|
|
|
40
75
|
const problems = [];
|
|
76
|
+
const indexProblems = [];
|
|
41
77
|
const report = (doc, line, path) => problems.push(`${doc}:${line}: нет файла \`${path}\``);
|
|
42
78
|
|
|
43
79
|
function readAllowlist() {
|
|
@@ -71,9 +107,9 @@ function collectDocs(dir = '.') {
|
|
|
71
107
|
|
|
72
108
|
/**
|
|
73
109
|
* Документы, которые в репозиторий не попадут: личный черновик, лежащий в дереве и
|
|
74
|
-
* закрытый
|
|
75
|
-
*
|
|
76
|
-
*
|
|
110
|
+
* закрытый настройкой неотслеживаемого. Проверка судит репозиторий, а не рабочий стол того,
|
|
111
|
+
* кто её запустил: мёртвая ссылка в чужом черновике держала гейт пуша, хотя ни в одну ветку
|
|
112
|
+
* этот файл не едет.
|
|
77
113
|
*/
|
|
78
114
|
function droppedByGit(docs) {
|
|
79
115
|
if (docs.length === 0) {
|
|
@@ -91,12 +127,7 @@ function droppedByGit(docs) {
|
|
|
91
127
|
return new Set((ignored.stdout ?? '').split('\n').filter(Boolean));
|
|
92
128
|
}
|
|
93
129
|
|
|
94
|
-
/**
|
|
95
|
-
* Верхний уровень дерева. Проверяются только адреса, укоренённые в нём: `src/index.ts`
|
|
96
|
-
* в правиле означает «любой файл такого вида», а `services/theme/theme.service.ts` —
|
|
97
|
-
* обрывок чужого пути. Ни то, ни другое адресом не является, и требовать их
|
|
98
|
-
* существования значит ловить форму записи вместо ссылки.
|
|
99
|
-
*/
|
|
130
|
+
/** Верхний уровень дерева: по нему узнаётся адрес, укоренённый в репозитории */
|
|
100
131
|
const ROOTED_IN = new Set(
|
|
101
132
|
readdirSync(ROOT, { withFileTypes: true })
|
|
102
133
|
.filter((entry) => entry.isDirectory() && !SKIPPED_DIRS.includes(entry.name))
|
|
@@ -104,18 +135,138 @@ const ROOTED_IN = new Set(
|
|
|
104
135
|
);
|
|
105
136
|
|
|
106
137
|
/**
|
|
107
|
-
*
|
|
108
|
-
*
|
|
138
|
+
* Дерево спрашивается у системы контроля версий, а не обходом каталогов: каталоги агента и
|
|
139
|
+
* конвейера начинаются с точки, и обход мимо них проходит молча — всё, что в них лежит,
|
|
140
|
+
* читалось бы как несуществующее. Неотслеживаемое берётся вместе с отслеживаемым: файл,
|
|
141
|
+
* заведённый этой же веткой и ещё не добавленный, существует ничуть не меньше.
|
|
142
|
+
*/
|
|
143
|
+
function treeOfRepo() {
|
|
144
|
+
const listed = spawnSync('git', ['ls-files', '--cached', '--others', '--exclude-standard'], {
|
|
145
|
+
cwd: ROOT,
|
|
146
|
+
encoding: 'utf8',
|
|
147
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
148
|
+
});
|
|
149
|
+
const paths = (listed.stdout ?? '').split('\n').filter(Boolean);
|
|
150
|
+
const files = new Set(paths);
|
|
151
|
+
const dirs = new Set();
|
|
152
|
+
const byName = new Map();
|
|
153
|
+
|
|
154
|
+
for (const path of paths) {
|
|
155
|
+
const segments = path.split('/');
|
|
156
|
+
for (let depth = 1; depth < segments.length; depth += 1) {
|
|
157
|
+
dirs.add(segments.slice(0, depth).join('/'));
|
|
158
|
+
}
|
|
159
|
+
byName.set(segments[segments.length - 1], true);
|
|
160
|
+
}
|
|
161
|
+
for (const dir of dirs) {
|
|
162
|
+
byName.set(dir.split('/').pop(), true);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return { files, dirs, byName };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const TREE = treeOfRepo();
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Адрес ли это вообще. Отсекается всё, что описывает форму, а не адрес: шаблоны, сетевые
|
|
172
|
+
* ссылки, имена пакетов, флаги и привязка «путь:символ» — её судит сверка спеков. Дальше
|
|
173
|
+
* кандидат бывает двух родов: укоренённый в дереве и голое имя. Голым именем зовут и файл, и
|
|
174
|
+
* каталог — оба ищутся по дереву, потому что адрес у них один, а написан он коротко.
|
|
109
175
|
*/
|
|
110
176
|
function looksLikePath(candidate) {
|
|
111
177
|
if (/[*<>{}$|\s]|\.\.\.|…/.test(candidate)) {
|
|
112
178
|
return false;
|
|
113
179
|
}
|
|
114
|
-
if (/^(https
|
|
180
|
+
if (/^(https?:|@|~|\/|-)/.test(candidate) || candidate.includes(':')) {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
// Каталог, который проверка не обходит, она и не судит: там чужое, сборка и служебное
|
|
184
|
+
if (SKIPPED_DIRS.some((dir) => candidate.startsWith(`${dir}/`))) {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
// Начинается с точки и стоит без каталога — род файла, а не файл
|
|
188
|
+
if (/^\.[^/]+$/.test(candidate)) {
|
|
115
189
|
return false;
|
|
116
190
|
}
|
|
117
191
|
|
|
118
|
-
return
|
|
192
|
+
return candidate.includes('/') || new RegExp(`\\.(?:${EXTENSIONS})$`).test(candidate);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Есть ли такой адрес в дереве. Укоренённый спрашивается у файловой системы: он назван
|
|
197
|
+
* целиком, и промах в нём — промах. Голое имя ищется по дереву целиком — и среди файлов, и
|
|
198
|
+
* среди каталогов: имя каталога в обзорном документе либы означает каталог рядом, а не
|
|
199
|
+
* каталог в корне.
|
|
200
|
+
*/
|
|
201
|
+
function existsInTree(candidate) {
|
|
202
|
+
const bare = candidate.replace(/\/$/, '');
|
|
203
|
+
|
|
204
|
+
if (ROOTED_IN.has(bare.split('/')[0])) {
|
|
205
|
+
return existsSync(join(ROOT, bare));
|
|
206
|
+
}
|
|
207
|
+
if (TREE.files.has(bare) || TREE.dirs.has(bare)) {
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
if (bare.includes('/')) {
|
|
211
|
+
return [...TREE.files, ...TREE.dirs].some((path) => path.endsWith(`/${bare}`));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return TREE.byName.has(bare);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Переносимый текст: разложенный пакетом — по шапке, его исходник — по каталогу из настройки.
|
|
219
|
+
* Адреса в нём принадлежат тому дереву, куда правило ложится: `libs/common/util` в дереве,
|
|
220
|
+
* которое зовёт свои корни иначе, — не мёртвая ссылка, а пример. Судить их здесь значит
|
|
221
|
+
* краснеть на полтораста строк, ни одна из которых не чинится правкой этого дерева.
|
|
222
|
+
*/
|
|
223
|
+
function isPortable(doc) {
|
|
224
|
+
if (PORTABLE_DIRS.some((dir) => doc.startsWith(dir))) {
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return readFileSync(join(ROOT, doc), 'utf8')
|
|
229
|
+
.split('\n', STAMP_LOOKAHEAD)
|
|
230
|
+
.some((line) => STAMP_LINE.test(line));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Из проверки адресов выведены документы, которые по устройству говорят о несуществующем. */
|
|
234
|
+
const isSkipped = (doc) => doc.startsWith(ARCHIVE_DIR) || doc.startsWith(TASKS_DIR) || isPortable(doc);
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Полнота указателя каталога: у каждой записи каталога есть строка в таблице, у каждой
|
|
238
|
+
* строки — запись. Записью считается первое имя в обратных кавычках строки таблицы: во
|
|
239
|
+
* второй колонке стоит проза, и брать оттуда было бы нечего. Каталог берётся у системы
|
|
240
|
+
* контроля версий той же выборкой, что и дерево: черновик, закрытый настройкой
|
|
241
|
+
* неотслеживаемого, в репозиторий не едет и указателю не нужен.
|
|
242
|
+
*/
|
|
243
|
+
function checkIndex(dir) {
|
|
244
|
+
const index = `${dir}README.md`;
|
|
245
|
+
if (!existsSync(join(ROOT, index))) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const named = new Set(
|
|
250
|
+
readFileSync(join(ROOT, index), 'utf8')
|
|
251
|
+
.split('\n')
|
|
252
|
+
.filter((line) => line.startsWith('|'))
|
|
253
|
+
.map((line) => line.match(PATH_IN_BACKTICKS)?.[0].replaceAll('`', ''))
|
|
254
|
+
.filter((name) => name?.endsWith('.md'))
|
|
255
|
+
);
|
|
256
|
+
const stored = new Set(
|
|
257
|
+
[...TREE.files]
|
|
258
|
+
.filter((path) => path.startsWith(dir) && path.endsWith('.md') && path !== index)
|
|
259
|
+
.map((path) => path.slice(dir.length))
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
[...stored]
|
|
263
|
+
.filter((name) => !named.has(name))
|
|
264
|
+
.sort()
|
|
265
|
+
.forEach((name) => indexProblems.push(`${index}: запись \`${name}\` лежит в каталоге, но в таблице не названа`));
|
|
266
|
+
[...named]
|
|
267
|
+
.filter((name) => !stored.has(name))
|
|
268
|
+
.sort()
|
|
269
|
+
.forEach((name) => indexProblems.push(`${index}: строка \`${name}\` названа в таблице, но записи в каталоге нет`));
|
|
119
270
|
}
|
|
120
271
|
|
|
121
272
|
function checkDoc(doc, allowed) {
|
|
@@ -136,27 +287,46 @@ function checkDoc(doc, allowed) {
|
|
|
136
287
|
if (!looksLikePath(candidate) || allowed.has(candidate)) {
|
|
137
288
|
continue;
|
|
138
289
|
}
|
|
139
|
-
if (!
|
|
290
|
+
if (!existsInTree(candidate)) {
|
|
140
291
|
report(doc, index + 1, candidate);
|
|
141
292
|
}
|
|
142
293
|
}
|
|
143
294
|
});
|
|
144
295
|
}
|
|
145
296
|
|
|
297
|
+
/** Расхождение указателя печатается своим списком: чинится оно строкой в таблице, а не молчанием. */
|
|
298
|
+
function reportIndex() {
|
|
299
|
+
if (indexProblems.length === 0) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
console.error(`\nуказатель разошёлся с каталогом, расхождений ${indexProblems.length}\n`);
|
|
304
|
+
indexProblems.forEach((problem) => console.error(` ${problem}`));
|
|
305
|
+
console.error(
|
|
306
|
+
'\nЗапись называется в таблице указателя тем же изменением, которым кладётся:\nчитатель ищет по указателю, а не обходом каталога.'
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
146
310
|
const allowlist = readAllowlist();
|
|
147
311
|
const allowedPaths = new Set(allowlist.paths);
|
|
148
|
-
const collected = collectDocs().filter((doc) => !allowlist.files.includes(doc) && !doc
|
|
312
|
+
const collected = collectDocs().filter((doc) => !allowlist.files.includes(doc) && !isSkipped(doc));
|
|
149
313
|
const dropped = droppedByGit(collected);
|
|
150
314
|
const docs = collected.filter((doc) => !dropped.has(doc));
|
|
151
315
|
|
|
152
316
|
docs.forEach((doc) => checkDoc(doc, allowedPaths));
|
|
317
|
+
INDEXED_DIRS.forEach((dir) => checkIndex(dir));
|
|
153
318
|
|
|
154
319
|
if (problems.length > 0) {
|
|
155
320
|
console.error(`check-doc-paths: расхождений ${problems.length}\n`);
|
|
156
321
|
problems.forEach((problem) => console.error(` ${problem}`));
|
|
157
322
|
console.error(
|
|
158
|
-
`\nЛибо
|
|
323
|
+
`\nЛибо адрес устарел и его надо поправить, либо документ описывает ещё не созданное —\nтогда он вносится в ${ALLOWLIST}.`
|
|
159
324
|
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
reportIndex();
|
|
328
|
+
|
|
329
|
+
if (problems.length > 0 || indexProblems.length > 0) {
|
|
160
330
|
process.exit(1);
|
|
161
331
|
}
|
|
162
332
|
|
|
@@ -62,14 +62,20 @@ const TEST_ROOTS = CONFIG.sourceRoots;
|
|
|
62
62
|
const SOURCE_ROOTS = [...CONFIG.sourceRoots, ...(CONFIG.schemaFile ? [CONFIG.schemaFile.split('/')[0]] : [])];
|
|
63
63
|
const SKIPPED_DIRS = CONFIG.skippedDirs;
|
|
64
64
|
|
|
65
|
-
/**
|
|
66
|
-
|
|
65
|
+
/**
|
|
66
|
+
* `### SC-BK-03 — заявка на занятые даты`
|
|
67
|
+
*
|
|
68
|
+
* Номер принимается от одной цифры до трёх. Заголовок, не подошедший под шаблон, сценария не
|
|
69
|
+
* заводит и отказа не даёт: дерево, пронумеровавшее сценарии с единицы, теряло бы первые
|
|
70
|
+
* девять из них молча — ни в покрытии, ни в долгах, при зелёной сверке.
|
|
71
|
+
*/
|
|
72
|
+
const SCENARIO_HEADING = /^###\s+(SC-([A-Z]{2,4})-(\d{1,3}))\s+—\s+(.+?)\s*$/;
|
|
67
73
|
/** Отметка осознанно непокрытого сценария; причина обязательна */
|
|
68
74
|
const UNCOVERED = /^Не покрыто:\s*\S/;
|
|
69
75
|
/** Тест есть, но проверяет не всё обещанное или идёт другим путём */
|
|
70
76
|
const PARTIAL = /^Покрытие:\s*частичное\s*—\s*\S/;
|
|
71
|
-
/** Упоминание сценария в заголовке
|
|
72
|
-
const SCENARIO_REFERENCE = /\bSC-[A-Z]{2,4}-\d{
|
|
77
|
+
/** Упоминание сценария в заголовке теста; номер той же длины, что и в заголовке сценария */
|
|
78
|
+
const SCENARIO_REFERENCE = /\bSC-[A-Z]{2,4}-\d{1,3}\b/g;
|
|
73
79
|
/** Строка обещания сценария; её продолжения идут с отступом */
|
|
74
80
|
const PROMISE = /^Тогда\s+\S/;
|
|
75
81
|
/**
|
|
@@ -229,7 +235,34 @@ function ruleHeadOf(bulletText) {
|
|
|
229
235
|
* Одно слово в двух смыслах развели именно здесь: «правило» — слой между законом и скилом,
|
|
230
236
|
* а внутри закона живут статьи.
|
|
231
237
|
*/
|
|
232
|
-
|
|
238
|
+
/**
|
|
239
|
+
* Строки таблицы привязок компаньона.
|
|
240
|
+
*
|
|
241
|
+
* Компаньон правила держит три таблицы: чем вещи правила названы в этом дереве, где лежат
|
|
242
|
+
* механизмы и где исполняется каждая статья. Привязки — только третья, и берётся она по имени
|
|
243
|
+
* раздела, а не по месту в файле. Пока читался весь файл, строки первых двух попадали в список
|
|
244
|
+
* наравне с настоящими и тут же объявлялись расхождением: статьи с таким текстом в правиле нет
|
|
245
|
+
* и быть не может. Две трети перечня в дереве были ими, и правильно дописанная строка «Где это
|
|
246
|
+
* лежит» отвечала отказом.
|
|
247
|
+
*
|
|
248
|
+
* У компаньона спека домена раздела нет: там таблица одна, и сужать нечего — такой зовёт без
|
|
249
|
+
* имени раздела. У правила раздел стоит в образце компаньона, поэтому его отсутствие — отказ:
|
|
250
|
+
* молча прочесть вместо него весь файл значило бы вернуть тот же дефект.
|
|
251
|
+
*/
|
|
252
|
+
function rowsOfMap(specFile, mapFile, mapHeading) {
|
|
253
|
+
const text = read(mapFile);
|
|
254
|
+
if (!mapHeading) {
|
|
255
|
+
return text.split('\n');
|
|
256
|
+
}
|
|
257
|
+
const section = sectionOf(text, mapHeading);
|
|
258
|
+
if (!section.length) {
|
|
259
|
+
report(mapFile, `нет раздела \`${mapHeading}\` — привязкам правила негде лежать`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return section;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function checkRuleImplementation(specFile, text, mapFile, heading = '## Правила', mapHeading = '') {
|
|
233
266
|
const bullets = bulletsOf(sectionOf(text, heading));
|
|
234
267
|
if (!bullets.length) {
|
|
235
268
|
report(specFile, `в разделе \`${heading}\` нет ни одного пункта`);
|
|
@@ -244,7 +277,7 @@ function checkRuleImplementation(specFile, text, mapFile, heading = '## Прав
|
|
|
244
277
|
}
|
|
245
278
|
|
|
246
279
|
const rows = new Map();
|
|
247
|
-
for (const line of
|
|
280
|
+
for (const line of rowsOfMap(specFile, mapFile, mapHeading)) {
|
|
248
281
|
const cells = line.match(/^\|([^|]+)\|([^|]*)\|\s*$/);
|
|
249
282
|
if (!cells) {
|
|
250
283
|
continue;
|
|
@@ -907,6 +940,8 @@ for (const file of walk(CONSTITUTION_DIR, (name) => name.endsWith('.md'))) {
|
|
|
907
940
|
// Правило — скил с `kind: rule` в шапке. Оно и знает о проекте: имена, пути, связи. Привязка
|
|
908
941
|
// его утверждений к коду живёт в `implementation.md` рядом со скилом.
|
|
909
942
|
const RULE_HEADING = '## Как закон применяется здесь';
|
|
943
|
+
/** Раздел компаньона правила, где лежат привязки; остальные его таблицы называют имена дерева. */
|
|
944
|
+
const MAP_HEADING = '## Где исполняются статьи';
|
|
910
945
|
|
|
911
946
|
/**
|
|
912
947
|
* Шапка скила — первый блок между `---`. Читается только она: паттерн, который учит заводить
|
|
@@ -953,7 +988,7 @@ for (const file of walk('.claude/skills', (name) => name === 'SKILL.md')) {
|
|
|
953
988
|
} else {
|
|
954
989
|
ruled.add(law);
|
|
955
990
|
}
|
|
956
|
-
checkRuleImplementation(file, text, `${dirname(file)}/implementation.md`, RULE_HEADING);
|
|
991
|
+
checkRuleImplementation(file, text, `${dirname(file)}/implementation.md`, RULE_HEADING, MAP_HEADING);
|
|
957
992
|
|
|
958
993
|
const name = nameOf(head);
|
|
959
994
|
if (name && name !== file.slice('.claude/skills/'.length, -'/SKILL.md'.length)) {
|
|
@@ -36,6 +36,18 @@ const DEFAULTS = {
|
|
|
36
36
|
docsDir: 'docs',
|
|
37
37
|
/** Отложенное: про него проверки молчат — оно описывает прошлое, а не дерево. */
|
|
38
38
|
archiveDir: 'docs/archive/',
|
|
39
|
+
/**
|
|
40
|
+
* Каталоги, чей указатель сверяется с содержимым: обзорный документ в них перечисляет
|
|
41
|
+
* записи таблицей, и читатель ищет по ней, а не обходом. Пусто — сверки указателя нет.
|
|
42
|
+
*/
|
|
43
|
+
indexedDirs: ['docs/archive/'],
|
|
44
|
+
/**
|
|
45
|
+
* Каталоги, где лежат исходники переносимых текстов. Такой текст называет адреса того
|
|
46
|
+
* дерева, куда он ложится, а не того, где написан, — и сверять его с этим деревом значит
|
|
47
|
+
* краснеть на каждый пример. Разложенную копию проверка узнаёт по шапке сама; сюда
|
|
48
|
+
* вносится только исходник. Пусто — переносимых текстов дерево не держит.
|
|
49
|
+
*/
|
|
50
|
+
portableDirs: [],
|
|
39
51
|
/** Где лежат спеки доменов; пусто — их в дереве нет, и сверка спеков не запускается. */
|
|
40
52
|
specsDir: 'docs/specs',
|
|
41
53
|
tasksDir: 'docs/tasks',
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Закрытие захода — главная ветка подтянута, влитые ветки сняты, передача написана
|
|
3
|
+
argument-hint: '[пусто | <что дописать в передачу от себя>]'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Закрой заход: приведи дерево к главной ветке, убери влитые ветки и напиши передачу для
|
|
7
|
+
следующего захода. Дописка владельца к передаче: `$ARGUMENTS`
|
|
8
|
+
|
|
9
|
+
Вызывается **последним действием захода** — после того, как работа закоммичена, а отчёт открыт
|
|
10
|
+
или влит. Команда ничего не мержит, не пушит и не открывает: закрытие захода — уборка, а не
|
|
11
|
+
поставка.
|
|
12
|
+
|
|
13
|
+
## 1. Прочитай профиль дерева
|
|
14
|
+
|
|
15
|
+
Имя главной ветки, каталог папок задач и каталог передачи у каждого дерева свои:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
for profile in .claude/rt-kit/defaults/project.sh .claude/rt-kit/project.sh; do
|
|
19
|
+
[ -f "$profile" ] && . "$profile"
|
|
20
|
+
done
|
|
21
|
+
printf 'главная: %s · задачи: %s · передача: %s\n' \
|
|
22
|
+
"${RT_MAIN_BRANCH:-main}" "${RT_TASKS_DIR:-docs/tasks}" "${RT_HANDOFF_DIR:-.claude/handoff}"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Зашивать эти имена в команду нельзя: в первом же дереве, которое зовёт главную ветку иначе,
|
|
26
|
+
уборка уедет не туда.
|
|
27
|
+
|
|
28
|
+
## 2. Остановись, если в дереве есть незакоммиченное
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
git status --short
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Непустой вывод — конец команды. Назови файлы владельцу и не трогай ни веток, ни главной: смена
|
|
35
|
+
ветки уносит правку за собой или отбивается на полпути, а решает, что с ней делать, владелец.
|
|
36
|
+
|
|
37
|
+
Неотслеживаемый файл — тоже незакоммиченное. Скажи о нём отдельной строкой: он мог остаться от
|
|
38
|
+
работы, которую бросили.
|
|
39
|
+
|
|
40
|
+
## 3. Пойми, по правилу ли ведётся работа
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
git fetch --prune --quiet
|
|
44
|
+
branch="$(git branch --show-current)"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Работа идёт **по правилу**, если имя ветки несёт номер задачи — это `rt_task_branch_ok` из
|
|
48
|
+
профиля — или если в каталоге папок задач лежит папка с именем ветки. Отчёт **влит**, когда
|
|
49
|
+
коммиты ветки уже есть в удалённой главной:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
git merge-base --is-ancestor HEAD "origin/${RT_MAIN_BRANCH:-main}" && echo влит || echo 'не влит'
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## 4. Приведи дерево к главной ветке
|
|
56
|
+
|
|
57
|
+
- **Работа по правилу и отчёт влит** — задача закрыта, ветка больше не нужна:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
git switch "${RT_MAIN_BRANCH:-main}" && git pull --ff-only
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
- **Всё остальное** — работа не кончилась, и ветка остаётся местом, где она продолжится:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
git merge "origin/${RT_MAIN_BRANCH:-main}"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Конфликт разбирается сейчас, а не в начале следующего захода: назови его владельцу и
|
|
70
|
+
останови команду до его решения.
|
|
71
|
+
|
|
72
|
+
## 5. Убери ветки
|
|
73
|
+
|
|
74
|
+
Снимаются только влитые в главную: их коммиты есть в ней, и восстанавливать нечего.
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
git branch --merged "${RT_MAIN_BRANCH:-main}" \
|
|
78
|
+
| grep -vE "^\*|^\s*${RT_MAIN_BRANCH:-main}$" \
|
|
79
|
+
| xargs -r git branch -d
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Невлитую ветку **не сноси**. Назови её владельцу вместе с числом коммитов, которых нет в
|
|
83
|
+
главной, — по ним видно, что именно потеряется, если её снести:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
for b in $(git branch --no-merged "${RT_MAIN_BRANCH:-main}" --format='%(refname:short)'); do
|
|
87
|
+
printf '%s: %s коммитов мимо главной\n' "$b" "$(git rev-list --count "${RT_MAIN_BRANCH:-main}..$b")"
|
|
88
|
+
done
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Мёртвые ссылки на удалённые ветки снял `git fetch --prune` шагом 3.
|
|
92
|
+
|
|
93
|
+
## 6. Напиши передачу
|
|
94
|
+
|
|
95
|
+
Что в ней стоит и в какой форме — паттерн `task-flow-handoff`; здесь только место и порядок.
|
|
96
|
+
Файл один на ветку и лежит вне истории дерева:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
mkdir -p "${RT_HANDOFF_DIR:-.claude/handoff}"
|
|
100
|
+
# файл — ${RT_HANDOFF_DIR:-.claude/handoff}/<ветка>.md
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Имя берётся от той ветки, в которой шла работа, — не от той, куда команда перешла шагом 4.
|
|
104
|
+
|
|
105
|
+
К тому, что требует паттерн, эта команда добавляет своё: что она убрала — снятые ветки,
|
|
106
|
+
состояние главной, оставшееся невлитым. Следующий заход начинается ровно с этого.
|
|
107
|
+
|
|
108
|
+
Заход, кончившийся ничем, передачу пишет тоже: «пробовали так — не вышло, потому что» стоит
|
|
109
|
+
дороже пустого файла. Дописку владельца из `$ARGUMENTS` вставь своим разделом, не пересказывая.
|
|
110
|
+
|
|
111
|
+
## 7. Отдай итог
|
|
112
|
+
|
|
113
|
+
Последней строкой — путь к передаче: владелец вставляет её в новый заход одной вставкой. Перед
|
|
114
|
+
ней: что стало с главной веткой, какие ветки сняты, какие остались невлитыми. Содержание
|
|
115
|
+
передачи не пересказывай — владелец её и так прочитает.
|
|
116
|
+
|
|
117
|
+
## Чего команда не делает
|
|
118
|
+
|
|
119
|
+
- не мержит отчёт и не пушит: это поставка, и вслепую она не делается;
|
|
120
|
+
- не сносит невлитую ветку и не трогает папку задачи;
|
|
121
|
+
- не коммитит передачу — она лежит вне дерева намеренно, иначе рядом с ходом работы заводится
|
|
122
|
+
вторая запись об одном и том же.
|
|
@@ -51,7 +51,15 @@ skill_for_default() {
|
|
|
51
51
|
case "$kind" in
|
|
52
52
|
edit)
|
|
53
53
|
case "$target" in
|
|
54
|
-
#
|
|
54
|
+
# Правило и паттерн — такая же договорённость, как спек: обязательные разделы,
|
|
55
|
+
# утверждение с привязкой, граница между статьёй закона и утверждением правила.
|
|
56
|
+
# Ветка стоит раньше общего исключения и раньше `*.md`: под исключением текст
|
|
57
|
+
# правила переписывался без единого требования, а `*.md` увёл бы его в правило
|
|
58
|
+
# формулировок — оно про слова, не про устройство.
|
|
59
|
+
*/.claude/skills/*.md) printf '%s\n' 'spec-driven' ;;
|
|
60
|
+
|
|
61
|
+
# Остальные файлы самого агента правятся без правила: правило на них — это оно
|
|
62
|
+
# само.
|
|
55
63
|
*/.claude/skills/* | */.claude/agents/* | */.claude/commands/* | */.claude/workflows/*) return 0 ;;
|
|
56
64
|
|
|
57
65
|
# Тексты проекта. Спек держит устройство домена, закон — договорённость,
|
|
@@ -61,6 +69,18 @@ skill_for_default() {
|
|
|
61
69
|
*/docs/constitution/*) printf '%s\n' 'spec-driven' ;;
|
|
62
70
|
*.md) printf '%s\n' 'doc-style' ;;
|
|
63
71
|
|
|
72
|
+
# Конфиги линтеров — то же самое, только запреты в них исполняемые: они и есть
|
|
73
|
+
# исполнение правил про типы и про оформление, а комментарии в них пересказывают
|
|
74
|
+
# эти правила поимённо. Правились без единого правила под рукой.
|
|
75
|
+
*/eslint.config.mjs | */eslint.config.js) printf '%s\n' 'typescript-conventions' ;;
|
|
76
|
+
*/stylelint.config.js | */stylelint.config.mjs) printf '%s\n' 'styling-bem' ;;
|
|
77
|
+
|
|
78
|
+
# Проверка повторов исполняет утверждения правила об общем коде и требуется
|
|
79
|
+
# только им: правило о раскладке либ говорит про неё одной строкой с отсылкой,
|
|
80
|
+
# а привязки её признаков стоят при общем коде. Два отказа подряд на правку двух
|
|
81
|
+
# строк комментария стоят захода, а второе прочитанное правило не пригождается.
|
|
82
|
+
*/tools/check-dupes.mjs | */tools/dupes-allowlist.json) printf '%s\n' 'shared-code' ;;
|
|
83
|
+
|
|
64
84
|
# Поставка: состав зависимостей — это то, что приезжает на прод. Правка
|
|
65
85
|
# скриптов зависимостью не является, и правило про версии на неё не вступает.
|
|
66
86
|
# Оговорка: удаление зависимости приходит правкой без номера версии и сюда не
|
|
@@ -77,6 +77,30 @@ RT_TASKS_DIR="${RT_TASKS_DIR:-docs/tasks}"
|
|
|
77
77
|
# удалить проще, чем разобрать, а слова владельца больше нигде не записаны.
|
|
78
78
|
RT_ARCHIVE_DIR="${RT_ARCHIVE_DIR:-docs/archive}"
|
|
79
79
|
|
|
80
|
+
# Размер окна захода в токенах и пороги стража. Пусто — стража нет: считать долю не от чего, а
|
|
81
|
+
# выведенный из записи захода размер врал бы — модель записана там без пометки о расширенном
|
|
82
|
+
# окне. Дерево задаёт его в настройке агента, переменной окружения того же имени.
|
|
83
|
+
RT_WINDOW_TOKENS="${RT_WINDOW_TOKENS:-}"
|
|
84
|
+
RT_WINDOW_WARN_PCT="${RT_WINDOW_WARN_PCT:-40}"
|
|
85
|
+
RT_WINDOW_STOP_PCT="${RT_WINDOW_STOP_PCT:-50}"
|
|
86
|
+
|
|
87
|
+
# Куда кладётся передача захода. Вне дерева: состояние работы живёт в ходе работы и коммитится,
|
|
88
|
+
# а передача его пересказывает для вставки в новый заход и в историю не едет.
|
|
89
|
+
RT_HANDOFF_DIR="${RT_HANDOFF_DIR:-.claude/handoff}"
|
|
90
|
+
|
|
91
|
+
# Команды, которые проходят после порога остановки: ими заход закрывается. Отбить их значило бы
|
|
92
|
+
# отобрать у него единственный способ закончиться. Вызов считается по началу строки или сразу за
|
|
93
|
+
# разделителем — упоминание команды в тексте командой не является.
|
|
94
|
+
rt_handoff_allowed_cmd_default() {
|
|
95
|
+
case "$1" in
|
|
96
|
+
git\ * | *[\;\&\|]\ *git\ * | *\$\(git\ *) return 0 ;;
|
|
97
|
+
gh\ * | */gh\ * | glab\ * | */glab\ * | az\ * | */az\ *) return 0 ;;
|
|
98
|
+
*task:move* | *check:* | mkdir\ -p\ * | cat\ * | ls\ *) return 0 ;;
|
|
99
|
+
esac
|
|
100
|
+
|
|
101
|
+
return 1
|
|
102
|
+
}
|
|
103
|
+
|
|
80
104
|
# Где лежат тексты, которые читают до вопроса владельцу: законы, правила и договорённости о
|
|
81
105
|
# продукте. По ним гард разговора судит, читалось ли за ход хоть что-то, и их же называет в
|
|
82
106
|
# подсказке. Пусто у законов и правил разом — дерево этого требования не получает: читать
|
|
@@ -202,3 +226,4 @@ rt_is_app_code() { rt_is_app_code_default "$@"; }
|
|
|
202
226
|
rt_qa_decorative() { rt_qa_decorative_default "$@"; }
|
|
203
227
|
rt_task_state() { rt_task_state_default "$@"; }
|
|
204
228
|
rt_report_body() { rt_report_body_default "$@"; }
|
|
229
|
+
rt_handoff_allowed_cmd() { rt_handoff_allowed_cmd_default "$@"; }
|