@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.
Files changed (86) hide show
  1. package/README.md +73 -0
  2. package/assets/checks/check-doc-paths.mjs +200 -30
  3. package/assets/checks/check-specs.mjs +42 -7
  4. package/assets/checks/rt-kit-checks.config.mjs +12 -0
  5. package/assets/commands/agent-kit-digest.md +83 -0
  6. package/assets/commands/next-session.md +122 -0
  7. package/assets/commands/skill-curator.md +33 -1
  8. package/assets/defaults/gate-map.sh +23 -1
  9. package/assets/defaults/project.sh +37 -1
  10. package/assets/docs/GLOSSARY.md +77 -0
  11. package/assets/hooks/docs-guard.sh +18 -1
  12. package/assets/hooks/git-guard-delivery.sh +30 -3
  13. package/assets/hooks/git-guard-main.sh +8 -0
  14. package/assets/hooks/git-guard-push-tests.sh +8 -1
  15. package/assets/hooks/grill-gate.sh +38 -16
  16. package/assets/hooks/lint-after-edit.sh +10 -3
  17. package/assets/hooks/observe.sh +90 -0
  18. package/assets/hooks/postmortem-guard.sh +92 -0
  19. package/assets/hooks/profile-check.sh +43 -0
  20. package/assets/hooks/qa-dataid-guard.sh +9 -2
  21. package/assets/hooks/reuse-first-guard.sh +9 -2
  22. package/assets/hooks/skill-gate.sh +15 -0
  23. package/assets/hooks/skill-loaded.sh +7 -0
  24. package/assets/hooks/task-context-load.sh +16 -2
  25. package/assets/hooks/task-flow-guard.sh +9 -2
  26. package/assets/hooks/window-fill-guard.sh +157 -0
  27. package/assets/laws/code-structure.md +10 -0
  28. package/assets/laws/delivery.md +8 -1
  29. package/assets/laws/project-documentation.md +9 -0
  30. package/assets/laws/verifiability.md +9 -0
  31. package/assets/laws/work-conduct.md +47 -0
  32. package/assets/patterns/git-workflow-commit.azure.md +16 -12
  33. package/assets/patterns/git-workflow-commit.github.md +16 -12
  34. package/assets/patterns/git-workflow-commit.gitlab.md +16 -12
  35. package/assets/patterns/spec-driven-domain.md +19 -0
  36. package/assets/patterns/task-flow-close.md +4 -4
  37. package/assets/patterns/task-flow-handoff.md +115 -0
  38. package/assets/patterns/task-flow-resume.md +14 -2
  39. package/assets/patterns/task-flow-start.md +40 -2
  40. package/assets/rules/doc-style.md +39 -1
  41. package/assets/rules/git-workflow.azure.md +39 -0
  42. package/assets/rules/git-workflow.github.md +38 -0
  43. package/assets/rules/git-workflow.gitlab.md +38 -0
  44. package/assets/rules/spec-driven.md +14 -0
  45. package/assets/rules/task-flow.md +70 -3
  46. package/assets/skills/agent-kit.md +32 -0
  47. package/assets/templates/postmortem.md +32 -0
  48. package/assets/templates/proposal.md +39 -0
  49. package/bin/agent-kit.d.ts.map +1 -1
  50. package/bin/agent-kit.js +50 -1
  51. package/bin/agent-kit.js.map +1 -1
  52. package/lib/catalog.d.ts +33 -0
  53. package/lib/catalog.d.ts.map +1 -1
  54. package/lib/catalog.js +55 -1
  55. package/lib/catalog.js.map +1 -1
  56. package/lib/commands.d.ts +41 -0
  57. package/lib/commands.d.ts.map +1 -1
  58. package/lib/commands.js +315 -3
  59. package/lib/commands.js.map +1 -1
  60. package/lib/config.d.ts +11 -1
  61. package/lib/config.d.ts.map +1 -1
  62. package/lib/config.js +6 -0
  63. package/lib/config.js.map +1 -1
  64. package/lib/hooks-map.d.ts +15 -3
  65. package/lib/hooks-map.d.ts.map +1 -1
  66. package/lib/hooks-map.js +47 -11
  67. package/lib/hooks-map.js.map +1 -1
  68. package/lib/observations.d.ts +72 -0
  69. package/lib/observations.d.ts.map +1 -0
  70. package/lib/observations.js +126 -0
  71. package/lib/observations.js.map +1 -0
  72. package/lib/proposals.d.ts +48 -0
  73. package/lib/proposals.d.ts.map +1 -0
  74. package/lib/proposals.js +111 -0
  75. package/lib/proposals.js.map +1 -0
  76. package/lib/submit.d.ts +24 -0
  77. package/lib/submit.d.ts.map +1 -0
  78. package/lib/submit.js +26 -0
  79. package/lib/submit.js.map +1 -0
  80. package/lib/sync.d.ts +9 -1
  81. package/lib/sync.d.ts.map +1 -1
  82. package/lib/sync.js +4 -6
  83. package/lib/sync.js.map +1 -1
  84. package/package.json +1 -1
  85. package/rt-tools-agent-kit-0.5.2.tgz +0 -0
  86. 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
- * собирает. К моменту заведения проверки из 159 путей, названных в `docs/`,
9
- * не существовало 80.
8
+ * собирает.
10
9
  *
11
- * Считаются только пути в обратных кавычках и вне блоков кода: в блоках лежат
12
- * команды и вывод, где `dist/apps/api/main.js` — результат сборки, а не файл
13
- * репозитория. Шаблоны (`*`, `<…>`, `{…}`) пропускаются: это форма пути, а не путь.
10
+ * Считаются только адреса в обратных кавычках и вне блоков кода: в блоках лежат
11
+ * команды и вывод, где путь до собранного — результат сборки, а не файл
12
+ * репозитория. Шаблоны (`*`, `<…>`, `{…}`) пропускаются: это форма адреса, а не адрес.
14
13
  *
15
- * Документы, которые по устройству говорят о несуществующем планы будущего и
16
- * архив, перечислены в tools/doc-paths-allowlist.json.
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` — копии репозитория под `.claude/`: их документы описывают раскладку своей
28
- // ветки, а проверка ищет пути в дереве текущей. Одна брошенная копия дала 73 расхождения и
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
- const PATH_IN_BACKTICKS = new RegExp(`\`([^\`\\n]+?\\.(?:${EXTENSIONS}))\``, 'g');
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
- * закрытый `.gitignore` или `.git/info/exclude`. Проверка судит репозиторий, а не рабочий
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
- * URL и пакеты npm.
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?:|@|~|\/)/.test(candidate)) {
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 ROOTED_IN.has(candidate.split('/')[0]);
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 (!existsSync(join(ROOT, candidate))) {
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.startsWith(ARCHIVE_DIR));
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Либо путь устарел и его надо поправить, либо документ описывает ещё не созданное —\nтогда он вносится в ${ALLOWLIST}.`
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
- /** `### SC-BK-03 — заявка на занятые даты` */
66
- const SCENARIO_HEADING = /^###\s+(SC-([A-Z]{2,4})-(\d{2,3}))\s+—\s+(.+?)\s*$/;
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{2,3}\b/g;
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
- function checkRuleImplementation(specFile, text, mapFile, heading = '## Правила') {
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 read(mapFile).split('\n')) {
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
+ публикует. Скажи, что накопилось на выпуск, и остановись.