@rt-tools/agent-kit 0.5.0 → 0.5.2
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 +73 -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/agent-kit-digest.md +83 -0
- package/assets/commands/next-session.md +122 -0
- package/assets/commands/skill-curator.md +33 -1
- package/assets/defaults/gate-map.sh +23 -1
- package/assets/defaults/project.sh +37 -1
- package/assets/docs/GLOSSARY.md +77 -0
- package/assets/hooks/docs-guard.sh +18 -1
- package/assets/hooks/git-guard-delivery.sh +30 -3
- package/assets/hooks/git-guard-main.sh +8 -0
- package/assets/hooks/git-guard-push-tests.sh +8 -1
- package/assets/hooks/grill-gate.sh +38 -16
- package/assets/hooks/lint-after-edit.sh +10 -3
- package/assets/hooks/observe.sh +90 -0
- package/assets/hooks/postmortem-guard.sh +92 -0
- package/assets/hooks/profile-check.sh +43 -0
- package/assets/hooks/qa-dataid-guard.sh +9 -2
- package/assets/hooks/reuse-first-guard.sh +9 -2
- package/assets/hooks/skill-gate.sh +15 -0
- package/assets/hooks/skill-loaded.sh +7 -0
- package/assets/hooks/task-context-load.sh +16 -2
- package/assets/hooks/task-flow-guard.sh +9 -2
- package/assets/hooks/window-fill-guard.sh +157 -0
- package/assets/laws/code-structure.md +10 -0
- package/assets/laws/delivery.md +8 -1
- package/assets/laws/project-documentation.md +9 -0
- package/assets/laws/verifiability.md +9 -0
- package/assets/laws/work-conduct.md +47 -0
- package/assets/patterns/git-workflow-commit.azure.md +16 -12
- package/assets/patterns/git-workflow-commit.github.md +16 -12
- package/assets/patterns/git-workflow-commit.gitlab.md +16 -12
- 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 +14 -2
- package/assets/patterns/task-flow-start.md +40 -2
- package/assets/rules/doc-style.md +39 -1
- package/assets/rules/git-workflow.azure.md +39 -0
- package/assets/rules/git-workflow.github.md +38 -0
- package/assets/rules/git-workflow.gitlab.md +38 -0
- package/assets/rules/spec-driven.md +14 -0
- package/assets/rules/task-flow.md +70 -3
- package/assets/skills/agent-kit.md +32 -0
- package/assets/templates/postmortem.md +32 -0
- package/assets/templates/proposal.md +39 -0
- package/bin/agent-kit.d.ts.map +1 -1
- package/bin/agent-kit.js +50 -1
- package/bin/agent-kit.js.map +1 -1
- package/lib/catalog.d.ts +33 -0
- package/lib/catalog.d.ts.map +1 -1
- package/lib/catalog.js +55 -1
- package/lib/catalog.js.map +1 -1
- package/lib/commands.d.ts +41 -0
- package/lib/commands.d.ts.map +1 -1
- package/lib/commands.js +315 -3
- package/lib/commands.js.map +1 -1
- package/lib/config.d.ts +11 -1
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +6 -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/observations.d.ts +72 -0
- package/lib/observations.d.ts.map +1 -0
- package/lib/observations.js +126 -0
- package/lib/observations.js.map +1 -0
- package/lib/proposals.d.ts +48 -0
- package/lib/proposals.d.ts.map +1 -0
- package/lib/proposals.js +111 -0
- package/lib/proposals.js.map +1 -0
- package/lib/submit.d.ts +24 -0
- package/lib/submit.d.ts.map +1 -0
- package/lib/submit.js +26 -0
- package/lib/submit.js.map +1 -0
- package/lib/sync.d.ts +9 -1
- package/lib/sync.d.ts.map +1 -1
- package/lib/sync.js +4 -6
- package/lib/sync.js.map +1 -1
- package/package.json +1 -1
- package/rt-tools-agent-kit-0.5.2.tgz +0 -0
- package/rt-tools-agent-kit-0.5.0.tgz +0 -0
package/README.md
CHANGED
|
@@ -26,6 +26,7 @@ npx agent-kit init # спросить законы галочками и з
|
|
|
26
26
|
npx agent-kit sync # разложить выбранное в docs/constitution/
|
|
27
27
|
npx agent-kit doctor # что разложено, что отстало, чего не хватает
|
|
28
28
|
npx agent-kit adopt # отдать пакету файлы, лежащие на его путях не от него
|
|
29
|
+
npx agent-kit stats # чем пользовались, чем ни разу, обо что спотыкались
|
|
29
30
|
```
|
|
30
31
|
|
|
31
32
|
`init` называет и то, чего пакет ждёт от дерева: значения дырок, которые придётся вписать в
|
|
@@ -238,6 +239,73 @@ npx agent-kit init --laws access,delivery,verifiability
|
|
|
238
239
|
|
|
239
240
|
Отказ хотя бы по одному файлу не пишет ничего: половина разложенного хуже целого.
|
|
240
241
|
|
|
242
|
+
## Наблюдения и сводка
|
|
243
|
+
|
|
244
|
+
Правило, которого никто не открывает, не действует, — и узнать об этом было нечем. Гарды пишут
|
|
245
|
+
наблюдения: правило загружено, гейт отбил правку, гард отказал. Строка несёт имя ресурса пакета,
|
|
246
|
+
род события, род правки, версию и признак сессии; путей дерева, имён его доменов и имени его
|
|
247
|
+
самого в ней нет — значение со слэшем не пишется вовсе.
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
npx agent-kit stats --days 3 # за отрезок; без довода — за три дня
|
|
251
|
+
npx agent-kit stats --json # то же машиночитаемо
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Самая ценная строка сводки — не «чем пользовались», а **что разложено и не загружено ни разу**:
|
|
255
|
+
чем пользуются, видно и по работе, а мёртвый ресурс ничем себя не выдаёт.
|
|
256
|
+
|
|
257
|
+
Наблюдения лежат в `.claude/rt-kit/observations/` файлом на день и снимаются через тридцать
|
|
258
|
+
дней. Запись выключается ключом `"observe": false` в конфиге — целиком, а не частями. Каталог
|
|
259
|
+
просится в список игнорируемого; вписывает его проект — в чужие файлы дерева пакет не пишет.
|
|
260
|
+
|
|
261
|
+
## Предложения обратно в пакет
|
|
262
|
+
|
|
263
|
+
Разбор закрытой задачи и раньше ставил каждому предложению адрес — «пакет», «компаньон» или
|
|
264
|
+
«дерево», — но результат оставался в переписке, и правки переносили руками. Теперь предложения
|
|
265
|
+
ложатся файлом в `.claude/rt-kit/proposals/`, блоком на предложение:
|
|
266
|
+
|
|
267
|
+
```markdown
|
|
268
|
+
## пакет · rules/styling-bem.md
|
|
269
|
+
|
|
270
|
+
- **место:** раздел «Ловушки», в конец
|
|
271
|
+
- **повод:** что в этой задаче пошло не так без этого правила
|
|
272
|
+
|
|
273
|
+
> Готовый текст правки.
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
```bash
|
|
277
|
+
npx agent-kit propose --dry-run # что уехало бы
|
|
278
|
+
npx agent-kit propose # завести записи в очереди работ пакета
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Наружу уезжает **только адрес «пакет»** и вместе с ним — сводка наблюдений: без цифр
|
|
282
|
+
предложение читается как мнение. Отправленное помечается ссылкой в том же файле и второй раз не
|
|
283
|
+
уезжает. Куда отправлять, пакет берёт из своего манифеста, а не из зашитого в код адреса.
|
|
284
|
+
|
|
285
|
+
Перед отправкой текст сверяется на адрес дерева — абсолютный путь, имя корня, адрес удалённого
|
|
286
|
+
репозитория. Нашлось — отказ с номером строки, и отбивается вся отправка целиком: «уехало два из
|
|
287
|
+
трёх» читается как «всё в порядке». Файл уезжает в чужой репозиторий, и запрет называть чужое
|
|
288
|
+
дерево держится проверкой, а не памятью того, кто пишет.
|
|
289
|
+
|
|
290
|
+
### Что для этого нужно
|
|
291
|
+
|
|
292
|
+
Своего сервера у механизма нет, ключей и учётных записей он не заводит. Наблюдения и сводка
|
|
293
|
+
живут целиком на машине; в сеть ходит только отправка, и только по команде человека.
|
|
294
|
+
|
|
295
|
+
| Где | Что нужно |
|
|
296
|
+
| --- | --- |
|
|
297
|
+
| у потребителя | `jq` — его требуют и сами гарды |
|
|
298
|
+
| у потребителя | помощник хостинга, вошедший в любую учётную запись: запись заводится от её имени |
|
|
299
|
+
| в репозитории пакета | метка, по которой сведение находит предложения, — заводится один раз |
|
|
300
|
+
|
|
301
|
+
```bash
|
|
302
|
+
gh label create agent-kit-feedback --description 'Предложение по слою правил, пришедшее из дерева'
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
Метки нет — отправка отказывает, и понять причину по сообщению помощника нельзя: про метку,
|
|
306
|
+
доступ и собственное отсутствие он говорит одинаково глухо. Поэтому отказ команды называет все
|
|
307
|
+
три сам.
|
|
308
|
+
|
|
241
309
|
## Роли и конвейеры
|
|
242
310
|
|
|
243
311
|
Пакет везёт шесть ролей субагентов и два конвейера из них.
|
|
@@ -260,6 +328,11 @@ npx agent-kit init --laws access,delivery,verifiability
|
|
|
260
328
|
`skill-curator` приезжает и слеш-командой: она собирает сводку о задаче и список загруженных
|
|
261
329
|
правил, без которых разбор выродится в пересказ.
|
|
262
330
|
|
|
331
|
+
Вторая команда, `next-session`, закрывает заход: приводит дерево к главной ветке — переходом на
|
|
332
|
+
неё, если работа шла по правилу и отчёт влит, и вливанием в текущую ветку во всех прочих
|
|
333
|
+
случаях, — снимает влитые локальные ветки, называет невлитые и пишет передачу для следующего
|
|
334
|
+
захода. Незакоммиченная правка её останавливает до первого действия; поставки она не касается.
|
|
335
|
+
|
|
263
336
|
## Как этим пользуются в дереве
|
|
264
337
|
|
|
265
338
|
Всё, что выше, пакет объясняет человеку. Агенту то же самое объясняет скил `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,83 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Свести накопленные предложения и наблюдения в правки ресурсов пакета правил
|
|
3
|
+
argument-hint: '[пусто | --days N]'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Собери предложения, пришедшие из деревьев, и скажи, что из них становится правкой пакета.
|
|
7
|
+
Отрезок от пользователя: `$ARGUMENTS` (без довода — три дня).
|
|
8
|
+
|
|
9
|
+
Зовётся **в репозитории самого пакета**, а не в дереве, где он стоит: здесь лежат ресурсы,
|
|
10
|
+
которые предстоит править, и видно всех потребителей сразу. В чужом дереве команда бессмысленна
|
|
11
|
+
— там есть только своя половина картины.
|
|
12
|
+
|
|
13
|
+
## 1. Собери, что пришло
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
gh issue list --label agent-kit-feedback --state open --limit 100 \
|
|
17
|
+
--json number,title,body,createdAt
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Помощник хостинга и учётная запись машинной работы у каждого дерева свои — как их звать здесь,
|
|
21
|
+
сказано в компаньоне правила `git-workflow`.
|
|
22
|
+
|
|
23
|
+
Каждая запись заведена командой `agent-kit propose` из дерева, где пакет стоит. В теле — ресурс,
|
|
24
|
+
место, повод, готовый текст и сводка наблюдений того дерева. Имени дерева там нет намеренно:
|
|
25
|
+
различать их можно только по сводке и по времени.
|
|
26
|
+
|
|
27
|
+
Своё дерево тоже потребитель — его наблюдения читаются прямо:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npx agent-kit stats --days <отрезок> --json
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## 2. Раздели повторившееся и разовое
|
|
34
|
+
|
|
35
|
+
Это и есть работа сведения; всё остальное — оформление.
|
|
36
|
+
|
|
37
|
+
- **Пришло об одном ресурсе из двух и более записей** — правка пакета. Разные деревья
|
|
38
|
+
споткнулись об одно и то же место, и чинить его надо там, откуда оно к ним приехало.
|
|
39
|
+
- **Пришло однажды и называет то, чего у других нет** — надстройка того дерева, а не пакет.
|
|
40
|
+
Скажи это прямо и назови, куда именно: `overrides/<ресурс>` для текстов, карта гейта и
|
|
41
|
+
профиль — для родов файлов и команд.
|
|
42
|
+
- **Пришло однажды, но верно любому дереву** — правка пакета. Числа записей мало: правило,
|
|
43
|
+
которое молчит о важном, молчит у всех, а споткнулся о него пока один.
|
|
44
|
+
|
|
45
|
+
Ресурс, о котором пришли **противоречащие** предложения, в правку не идёт вовсе: неси оба
|
|
46
|
+
владельцу. Слой правил, который говорит два разных, хуже слоя, который молчит.
|
|
47
|
+
|
|
48
|
+
## 3. Посмотри, что говорят наблюдения
|
|
49
|
+
|
|
50
|
+
Сводка отвечает на то, чего в предложениях нет:
|
|
51
|
+
|
|
52
|
+
- **правило, разложенное и не загруженное ни разу** — кандидат на сокращение или на запись в
|
|
53
|
+
карте гейта: правило, которого гейт не требует, никто и не откроет;
|
|
54
|
+
- **правило, которое гейт отбивает чаще прочих** — его либо забывают, либо оно требуется не
|
|
55
|
+
там; посмотри род правки в той же сводке;
|
|
56
|
+
- **гард, отказывающий чаще прочих** — либо место поставки раз за разом делают не так, либо
|
|
57
|
+
отказ не называет, чем он снимается.
|
|
58
|
+
|
|
59
|
+
## 4. Принеси правки
|
|
60
|
+
|
|
61
|
+
По каждой — ресурс, место, готовый текст и число записей, из которых она вышла. Не правь ничего
|
|
62
|
+
сам: решение принимает владелец, а работа идёт обычным ходом — задача, ветка, папка задачи.
|
|
63
|
+
|
|
64
|
+
Заведи задачу на то, что владелец принял:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
npm run task:new -- --title '<что не так>' --slug <короткое-имя> --label enhancement < тело.md
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Записи, вошедшие в задачу, закрой ссылкой на неё:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
gh issue close <номер> --comment 'Вошло в #<номер задачи>.'
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## 5. Черновик записи в журнал изменений
|
|
77
|
+
|
|
78
|
+
Собери его сразу, пока видно, из чего правка выросла: заголовок коммита по формату дерева и
|
|
79
|
+
одна фраза о том, что менялось и почему. Журнал изменений при выпуске собирается из заголовков,
|
|
80
|
+
и переписывать их задним числом — работа заново.
|
|
81
|
+
|
|
82
|
+
**Выпуск отсюда не запускается.** Это отдельное решение владельца: слияние отчёта пакета не
|
|
83
|
+
публикует. Скажи, что накопилось на выпуск, и остановись.
|