@rt-tools/agent-kit 0.8.1 → 0.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/README.md +25 -19
  2. package/assets/agents/qa-engineer.md +1 -1
  3. package/assets/agents/rules-reviewer.md +83 -0
  4. package/assets/checks/board.github.mjs +56 -17
  5. package/assets/checks/check-board.github.mjs +49 -5
  6. package/assets/checks/check-dupes.mjs +66 -6
  7. package/assets/checks/check-specs.mjs +100 -15
  8. package/assets/checks/rt-kit-checks.config.mjs +9 -0
  9. package/assets/checks/task-new.github.mjs +33 -5
  10. package/assets/commands/agent-kit-digest.md +10 -5
  11. package/assets/commands/feedback.md +95 -0
  12. package/assets/commands/next-session.md +4 -4
  13. package/assets/commands/rules-review.md +98 -0
  14. package/assets/commands/skill-curator.md +44 -25
  15. package/assets/defaults/gate-map.sh +11 -4
  16. package/assets/defaults/project.sh +46 -0
  17. package/assets/docs/GLOSSARY.md +49 -46
  18. package/assets/hooks/docs-guard.sh +19 -3
  19. package/assets/hooks/git-guard-delivery.sh +106 -13
  20. package/assets/hooks/proposal-guard.sh +93 -0
  21. package/assets/hooks/reuse-first-guard.sh +83 -15
  22. package/assets/hooks/skill-gate-layers.sh +1 -1
  23. package/assets/hooks/skill-gate.sh +27 -1
  24. package/assets/hooks/task-flow-guard.sh +59 -19
  25. package/assets/hooks/waiting-turn-guard.sh +87 -0
  26. package/assets/hooks/window-fill-guard.sh +1 -1
  27. package/assets/laws/delivery.md +41 -7
  28. package/assets/laws/project-documentation.md +39 -0
  29. package/assets/laws/verifiability.md +6 -1
  30. package/assets/laws/work-conduct.md +83 -3
  31. package/assets/patterns/git-workflow-commit.azure.md +84 -4
  32. package/assets/patterns/git-workflow-commit.github.md +90 -4
  33. package/assets/patterns/git-workflow-commit.gitlab.md +84 -5
  34. package/assets/patterns/git-workflow-docker.md +30 -0
  35. package/assets/patterns/git-workflow-merge.md +1 -1
  36. package/assets/patterns/spec-driven-domain.md +7 -1
  37. package/assets/patterns/spec-driven-rule.md +6 -0
  38. package/assets/patterns/task-flow-close.md +197 -21
  39. package/assets/patterns/task-flow-handoff.md +28 -5
  40. package/assets/patterns/task-flow-resume.md +25 -7
  41. package/assets/patterns/task-flow-start.md +37 -6
  42. package/assets/rules/angular-patterns.md +22 -0
  43. package/assets/rules/api-layer.md +25 -0
  44. package/assets/rules/browser-verification.md +42 -1
  45. package/assets/rules/component-structure.md +21 -0
  46. package/assets/rules/dependencies.md +22 -0
  47. package/assets/rules/doc-style.md +37 -5
  48. package/assets/rules/{entity-conventions.md → entity-conventions.needs-admin.md} +21 -0
  49. package/assets/rules/entity-models.md +21 -0
  50. package/assets/rules/git-workflow.azure.md +62 -8
  51. package/assets/rules/git-workflow.github.md +78 -14
  52. package/assets/rules/git-workflow.gitlab.md +61 -8
  53. package/assets/rules/lib-layers.md +25 -0
  54. package/assets/rules/lists.md +27 -0
  55. package/assets/rules/navigation.md +21 -0
  56. package/assets/rules/{observability.md → observability.needs-app.md} +23 -0
  57. package/assets/rules/permissions.md +23 -0
  58. package/assets/rules/platform-access.md +21 -0
  59. package/assets/rules/reuse-first.md +20 -0
  60. package/assets/rules/seo.md +19 -0
  61. package/assets/rules/shared-code.md +19 -0
  62. package/assets/rules/spec-driven.md +36 -0
  63. package/assets/rules/styling-bem.md +19 -0
  64. package/assets/rules/task-flow.md +150 -35
  65. package/assets/rules/testing.md +50 -0
  66. package/assets/rules/translations.md +21 -0
  67. package/assets/rules/typescript-conventions.md +33 -0
  68. package/assets/samples/specs/_template/spec.md +83 -0
  69. package/assets/samples/tasks/_template/grill.md +28 -0
  70. package/assets/samples/tasks/_template/plan.md +39 -0
  71. package/assets/samples/tasks/_template/progress.md +23 -0
  72. package/assets/skills/agent-kit-extend.md +24 -0
  73. package/assets/skills/agent-kit.md +69 -7
  74. package/assets/templates/rule.md +31 -2
  75. package/assets/traits.json +14 -0
  76. package/bin/agent-kit.d.ts.map +1 -1
  77. package/bin/agent-kit.js +31 -16
  78. package/bin/agent-kit.js.map +1 -1
  79. package/index.d.ts +1 -0
  80. package/index.d.ts.map +1 -1
  81. package/index.js +1 -0
  82. package/index.js.map +1 -1
  83. package/lib/argv.d.ts +17 -0
  84. package/lib/argv.d.ts.map +1 -0
  85. package/lib/argv.js +44 -0
  86. package/lib/argv.js.map +1 -0
  87. package/lib/cargo.d.ts +88 -0
  88. package/lib/cargo.d.ts.map +1 -0
  89. package/lib/cargo.js +16 -0
  90. package/lib/cargo.js.map +1 -0
  91. package/lib/catalog.d.ts +18 -1
  92. package/lib/catalog.d.ts.map +1 -1
  93. package/lib/catalog.js +12 -2
  94. package/lib/catalog.js.map +1 -1
  95. package/lib/commands.d.ts +0 -26
  96. package/lib/commands.d.ts.map +1 -1
  97. package/lib/commands.js +79 -122
  98. package/lib/commands.js.map +1 -1
  99. package/lib/companion.d.ts +37 -0
  100. package/lib/companion.d.ts.map +1 -1
  101. package/lib/companion.js +42 -1
  102. package/lib/companion.js.map +1 -1
  103. package/lib/config.d.ts +38 -1
  104. package/lib/config.d.ts.map +1 -1
  105. package/lib/config.js +24 -0
  106. package/lib/config.js.map +1 -1
  107. package/lib/ship.d.ts +39 -0
  108. package/lib/ship.d.ts.map +1 -0
  109. package/lib/ship.js +87 -0
  110. package/lib/ship.js.map +1 -0
  111. package/lib/shipment.d.ts +60 -0
  112. package/lib/shipment.d.ts.map +1 -0
  113. package/lib/shipment.js +247 -0
  114. package/lib/shipment.js.map +1 -0
  115. package/lib/snapshot.d.ts +30 -0
  116. package/lib/snapshot.d.ts.map +1 -0
  117. package/lib/snapshot.js +73 -0
  118. package/lib/snapshot.js.map +1 -0
  119. package/lib/traits.d.ts +32 -0
  120. package/lib/traits.d.ts.map +1 -0
  121. package/lib/traits.js +82 -0
  122. package/lib/traits.js.map +1 -0
  123. package/package.json +6 -2
  124. package/rt-tools-agent-kit-0.8.3.tgz +0 -0
  125. package/lib/submit.d.ts +0 -24
  126. package/lib/submit.d.ts.map +0 -1
  127. package/lib/submit.js +0 -26
  128. package/lib/submit.js.map +0 -1
  129. package/rt-tools-agent-kit-0.8.1.tgz +0 -0
package/README.md CHANGED
@@ -63,6 +63,7 @@ npx agent-kit sync --check
63
63
  | `agents`, `commands`, `workflows` | `.claude/` | роли, слеш-команды и многошаговые прогоны |
64
64
  | `checks` | `tools/` | проверки, которые зовёт гейт |
65
65
  | `templates` | `.claude/rt-kit/templates/` | формы правила, паттерна, компаньона и надстроек |
66
+ | `samples` | `docs/` | образцы, которые копируют в рабочий файл: папка задачи, спек домена |
66
67
 
67
68
  Правило, паттерн и скил ложатся одинаково — все три скилы; различает их `kind` во вступлении
68
69
  файла. Скил без закона стоит рядом с лестницей, а не в ней: он не про то, что должно быть верно
@@ -289,36 +290,41 @@ npx agent-kit stats --json # то же машиночитаемо
289
290
 
290
291
  ```bash
291
292
  npx agent-kit propose --dry-run # что уехало бы
292
- npx agent-kit propose # завести записи в очереди работ пакета
293
+ npx agent-kit propose # отправить груз в приём
293
294
  ```
294
295
 
295
- Наружу уезжает **только адрес «пакет»** и вместе с ним сводка наблюдений: без цифр
296
- предложение читается как мнение. Отправленное помечается ссылкой в том же файле и второй раз не
297
- уезжает. Куда отправлять, пакет берёт из своего манифеста, а не из зашитого в код адреса.
296
+ Груз уезжает при каждом прогоне тремя родами: **сводка наблюдений со снимком надстроек**,
297
+ **предложения** с адресом «пакет» когда они есть и **разборы происшествий**. Прогон без
298
+ замечаний тоже говорит, чем пользовались, чем не пользовались ни разу и что дерево
299
+ переопределило. Отправленное предложение помечается в том же файле и второй раз не уезжает.
298
300
 
299
- Перед отправкой текст сверяется на адрес дерева абсолютный путь, имя корня, адрес удалённого
300
- репозитория. Нашлосьотказ с номером строки, и отбивается вся отправка целиком: «уехало два из
301
- трёх» читается как «всё в порядке». Файл уезжает в чужой репозиторий, и запрет называть чужое
302
- дерево держится проверкой, а не памятью того, кто пишет.
301
+ Уезжает груз в **закрытый приём**, а не в открытую очередь работ: сводка говорит о рабочих
302
+ привычках командычем пользуются, обо что спотыкаются, сколько раз признавали промах. Адрес
303
+ приёма объявлен настройкой дерева, а не зашит в код пакета.
304
+
305
+ Перед отправкой сводка и тексты предложений сверяются на адрес дерева — абсолютный путь, имя
306
+ корня, адрес удалённого репозитория. Нашлось — отказ с номером строки, и отбивается вся отправка
307
+ целиком: «уехало два из трёх» читается как «всё в порядке». Разбор происшествия проверкой не
308
+ накрыт: он по устройству называет файлы дерева, где промах случился.
303
309
 
304
310
  ### Что для этого нужно
305
311
 
306
- Своего сервера у механизма нет, ключей и учётных записей он не заводит. Наблюдения и сводка
307
- живут целиком на машине; в сеть ходит только отправка, и только по команде человека.
312
+ Наблюдения и сводка живут целиком на машине; в сеть ходит только отправка, и только по команде
313
+ человека. Приём своя служба, а не чужая: она поднимается там, где решит владелец пакета.
308
314
 
309
315
  | Где | Что нужно |
310
316
  | --- | --- |
311
317
  | у потребителя | `jq` — его требуют и сами гарды |
312
- | у потребителя | помощник хостинга, вошедший в любую учётную запись: запись заводится от её имени |
313
- | в репозитории пакета | метка, по которой сведение находит предложения, заводится один раз |
318
+ | у потребителя | адрес приёма ключом `intake` в `.claude/rt-kit.json` |
319
+ | у потребителя | токен дерева в файле вне дерева, названном ключом `token` |
320
+ | у приёма | заведённое дерево: токен выдаёт его команда и печатает один раз |
314
321
 
315
- ```bash
316
- gh label create agent-kit-feedback --description 'Предложение по слою правил, пришедшее из дерева'
317
- ```
322
+ Признак, которым дерево представляется приёму, считается хешем от адреса его удалённого
323
+ репозитория: адрес по нему не восстанавливается, а у двух рабочих копий одного репозитория он
324
+ один. Репозитория нет — признак называется ключом `tree`.
318
325
 
319
- Метки нет отправка отказывает, и понять причину по сообщению помощника нельзя: про метку,
320
- доступ и собственное отсутствие он говорит одинаково глухо. Поэтому отказ команды называет все
321
- три сам.
326
+ Адреса нет, токена нет, токен отозван отправка отказывает ненулевым кодом и называет, чего
327
+ именно не хватило и где это объявляется. Молчаливая отправка выглядела бы работающей.
322
328
 
323
329
  ## Роли и конвейеры
324
330
 
@@ -356,7 +362,7 @@ gh label create agent-kit-feedback --description 'Предложение по с
356
362
  | `/agent-kit-digest` | `commands/agent-kit-digest.md` | сводит накопленные предложения и наблюдения в правки ресурсов |
357
363
 
358
364
  `/next-session` приводит дерево к главной ветке — переходом на неё, если работа шла по правилу и
359
- отчёт влит, и вливанием в текущую ветку во всех прочих случаях, — снимает влитые локальные
365
+ PR влит, и вливанием в текущую ветку во всех прочих случаях, — снимает влитые локальные
360
366
  ветки, называет невлитые и пишет передачу для следующего захода. Незакоммиченная правка
361
367
  останавливает её до первого действия; поставки она не касается.
362
368
 
@@ -8,7 +8,7 @@ tools: Read, Grep, Glob, Bash, Write, Edit, Skill, mcp__claude-in-chrome__select
8
8
  `CLAUDE.md` и в правилах `testing` и `browser-verification`, а не предполагай. Отвечаешь
9
9
  **по-русски**.
10
10
 
11
- Твоя задача — найти, где сделанное не работает, а не подтвердить, что работает. Отчёт без
11
+ Твоя задача — найти, где сделанное не работает, а не подтвердить, что работает. PR без
12
12
  единой находки допустим только тогда, когда ты честно пытался её получить.
13
13
 
14
14
  ## Чего делать нельзя
@@ -0,0 +1,83 @@
1
+ ---
2
+ name: rules-reviewer
3
+ description: Читает семью текстов слоя правил целиком — закон, все правила под ним и все паттерны при них — и ищет то, чего не считает машина: два текста, говорящих об одном разное, и случай, которого не назвал ни один. Файлов не правит. Использовать перед выпуском новой редакции пакета правил и после правки закона или правила.
4
+ tools: Read, Grep, Glob, Bash
5
+ ---
6
+
7
+ Ты читаешь семью текстов слоя правил и ищешь расхождения смысла. Отвечаешь **по-русски**.
8
+
9
+ Твой результат — список находок. Не правки: ты ничего не меняешь.
10
+
11
+ Промах в этих текстах уезжает ко всем деревьям разом и находит его тот, кто пошёл за правилом и
12
+ сделал не то. Считаемое — недостающий раздел, правило без паттерна, имя соседа, которому ничего
13
+ не отвечает — уже ловит проверка. Тебе остаётся то, что видно только чтением.
14
+
15
+ ## Чего делать нельзя
16
+
17
+ - **Никаких git-команд вообще**, включая `status` и `diff`. Историю ведёт главный агент.
18
+ - Ничего не править: ни закон, ни правило, ни паттерн. Ты возвращаешь находки.
19
+ - Не пересказывать найденное своими словами: находка без дословной цитаты не проверяется ничем,
20
+ и человек не отличит настоящее расхождение от твоего прочтения.
21
+ - Не считать того, что уже считает проверка полноты текстов. Повтор её вывода вытесняет из
22
+ ответа то, ради чего тебя и звали.
23
+ - Не судить о дереве, в котором ты запущен. Тексты пакета переносятся, и что из них разложено
24
+ здесь — не их предмет.
25
+
26
+ ## По чему идёшь
27
+
28
+ Семья — это **один закон, все правила под ним и все паттерны при этих правилах**. Имя закона
29
+ тебе даёт вызывающий. Пакет целиком не читается: текстов в нём больше десяти тысяч строк, и
30
+ прочитанные разом они дают крошку по каждому файлу вместо находок.
31
+
32
+ Правило принадлежит закону по полю `law:` в его шапке, паттерн правилу — по полю `rule:`.
33
+ Приставка имени ненадёжна: её несут не все.
34
+
35
+ **Все виды правила читаются, а не выбранный.** У одного ресурса бывает несколько редакций —
36
+ `git-workflow.github`, `git-workflow.gitlab`, `git-workflow.azure`. Дерево раскладывает одну, и
37
+ остальные не читает никто: разойдясь, они молчат до первого дерева, выбравшего другую.
38
+
39
+ **Суффикс требования видом не является.** `entity-conventions.needs-admin`,
40
+ `observability.needs-app` — правила, которые дерево берёт, только объявив нужную черту. В поле
41
+ `rule:` у паттерна стоит голое имя, без обоих суффиксов: собранная по имени с суффиксом семья
42
+ приходит без паттернов, и пустота эта выглядит как их отсутствие. Пришедшая к тебе семья без
43
+ единого паттерна — повод сказать об этом, а не молча разобрать что дали.
44
+
45
+ Ищешь два рода находок, и они разные.
46
+
47
+ **Расхождение — два места, говорящих об одном разное.** Правило требует того, что паттерн при
48
+ соседнем правиле запрещает. Закон называет одно число, правило — другое. Паттерн показывает
49
+ приём, который правило объявило отвергнутым. Сюда же — одно понятие под двумя именами и одно имя
50
+ над двумя понятиями.
51
+
52
+ **Пробел — случай, которого не назвал ни один текст семьи.** Статья закона, под которую ни в
53
+ одном правиле нет ни строки. Развилка, у которой описана одна ветка из двух. Отказ, о котором
54
+ сказано, что он бывает, и не сказано, что делать. Раздел «Чего из закона здесь нет», который
55
+ молчит о том, чего в дереве действительно нет.
56
+
57
+ **Граф хода — такое же место расхождения, как проза.** Он стоит в правиле разделом «Ход»
58
+ блоком `mermaid` и изображает тот же ход, что описан ниже словами: ветка графа, которой в прозе нет, и статья, до
59
+ которой по графу не дойти, — расхождение того же рода, что и два текста об одном. Правится он тем
60
+ же изменением, что и проза, и разойдясь, обе стороны читаются как действующие.
61
+
62
+ Три места, где расхождения заводятся чаще прочего, — проверь каждое:
63
+
64
+ - **«Чего из закона здесь нет»** — его не читает ни одна сверка, и неправда живёт в нём сколько
65
+ угодно: раздел говорит об отсутствии механизма, а механизм давно заведён, и заметить это может
66
+ только тот, кто пошёл его искать.
67
+ - **Числа** — счёт правил, статей, шагов, строк. Они стареют без единой правки рядом.
68
+ - **Ловушки** — их пишут по случаю и не перечитывают: приём, который они запрещают, мог с тех
69
+ пор стать рабочим.
70
+
71
+ ## Что возвращаешь
72
+
73
+ Список находок, самая дорогая первой. Роды не смешивай — сперва расхождения, потом пробелы.
74
+
75
+ У **расхождения**: имена обоих ресурсов, обе цитаты дословно, и одной фразой — чем именно они
76
+ расходятся и что исполнитель сделает не так, пойдя за той или другой.
77
+
78
+ У **пробела**: имя ресурса, в котором его недостаёт, цитата места, где он должен был стоять
79
+ (или имя раздела, если места нет вовсе), и что случится, когда этот случай наступит. Второй
80
+ цитаты у пробела не бывает — не выдумывай её.
81
+
82
+ Находок нет — так и скажи. Пустой ответ дешевле выдуманного: по выдуманному правят настоящие
83
+ тексты.
@@ -45,7 +45,7 @@ export const STATUS_FIELD_ID = BOARD.statusFieldId ?? '';
45
45
  export const STATUS_OPTIONS = BOARD.statusOptions ?? {};
46
46
  /** Колонка вновь заведённой задачи */
47
47
  export const BACKLOG_OPTION_ID = STATUS_OPTIONS.backlog?.id ?? '';
48
- /** Колонка задачи, взятой в работу, и задачи, отчёт по которой ждёт разбора */
48
+ /** Колонка задачи, взятой в работу, и задачи, PR по которой ждёт разбора */
49
49
  export const IN_PROGRESS_STATUS = 'in-progress';
50
50
  export const IN_REVIEW_STATUS = 'in-review';
51
51
  /** Учётная запись машинной работы — та же, от которой идут коммиты */
@@ -69,12 +69,6 @@ if (PROJECT_ID && !TASK_KEY) {
69
69
  }
70
70
  /** Кого запрашивают на разбор: без ревьювера PR не попадает во входящие владельца. */
71
71
  export const REVIEWER = BOARD.reviewer ?? '';
72
- /**
73
- * Где лежит токен машинной учётной записи — так, как это назвало дерево. Идёт в текст отказа:
74
- * зашитый путь послал бы чужое дерево заводить файл, который никто не читает.
75
- */
76
- export const TOKEN_PATH = BOARD.tokenPath || 'путь не назван в .claude/rt-kit/checks.json';
77
-
78
72
  const BOT_TOKEN_FILE = BOARD.tokenPath ? BOARD.tokenPath.replace(/^~/, homedir()) : '';
79
73
 
80
74
  /**
@@ -89,7 +83,11 @@ function ghBinary() {
89
83
  return existsSync(homebrew) ? homebrew : 'gh';
90
84
  }
91
85
 
92
- /** Токен бота лежит вне репозитория и в вывод не попадает */
86
+ /**
87
+ * Токен машинной записи лежит вне репозитория и в вывод не попадает. Его отсутствие — не отказ:
88
+ * дерево, не назвавшее токена в `board.tokenPath`, работает с очередью учётной записью, под
89
+ * которой залогинен клиент хостинга.
90
+ */
93
91
  export function botToken() {
94
92
  if (!existsSync(BOT_TOKEN_FILE)) {
95
93
  return null;
@@ -172,7 +170,7 @@ export function fetchBoard(options) {
172
170
 
173
171
  /**
174
172
  * Перевод задачи в другую колонку. Состояние задачи на борде — единственное, по чему
175
- * видно ход работы: ветку и открытый отчёт борда сама не читает.
173
+ * видно ход работы: ветку и открытый PR борда сама не читает.
176
174
  */
177
175
  export function moveTask(number, status, options) {
178
176
  const option = STATUS_OPTIONS[status];
@@ -284,11 +282,53 @@ export function taskState(number, options) {
284
282
  onBoard: item !== undefined,
285
283
  status: item?.status ?? null,
286
284
  assigned: issue.assignees.length > 0,
285
+ assignees: issue.assignees.map((assignee) => assignee.login),
287
286
  numbered: numberFromTitle(issue.title) === issue.number,
288
287
  labels: issue.labels.map((label) => label.name),
289
288
  };
290
289
  }
291
290
 
291
+ /**
292
+ * Ответ очереди работ о заведённой задаче, сложенный в строки, и приговор: обеспечена работа
293
+ * или нет.
294
+ *
295
+ * Отделено от вызовов сети намеренно. Заведение кончается не выводом команды, а ответом
296
+ * очереди, и решение о том, что напечатать и чем кончиться, — это то самое место, где
297
+ * шестнадцать задач подряд прошли как успешные, не попав в очередь ни одна. Внутри вызовов
298
+ * сети оно проверяется только живой бордой, то есть не проверяется никогда.
299
+ *
300
+ * `state` — то, что вернул `taskState`, либо `{ offline: <причина> }`, если спросить не удалось.
301
+ */
302
+ export function describeTaskState(number, state) {
303
+ if (state?.offline) {
304
+ return {
305
+ ok: false,
306
+ lines: [
307
+ `в очереди работ: спросить не удалось — ${state.offline}`,
308
+ 'состояние очереди неизвестно, и заведённым это не считается',
309
+ ],
310
+ };
311
+ }
312
+ if (!state?.exists) {
313
+ return { ok: false, lines: [`задачи #${number} у хостинга нет — заведение не состоялось`] };
314
+ }
315
+ if (!state.onBoard) {
316
+ return {
317
+ ok: false,
318
+ lines: [`в очереди работ: НЕТ`, 'задача, которой нет в очереди, работой не обеспечена — по ней никто не придёт'],
319
+ };
320
+ }
321
+
322
+ const column = state.status ? `колонка «${state.status}»` : 'колонки нет';
323
+ if (!state.assigned) {
324
+ return {
325
+ ok: false,
326
+ lines: [`в очереди работ: ${column}, исполнителя нет`, 'ничья задача стоит в очереди невидимой для того, кто её делает'],
327
+ };
328
+ }
329
+ return { ok: true, lines: [`в очереди работ: ${column}, исполнитель ${state.assignees.join(', ')}`] };
330
+ }
331
+
292
332
  const isEntryPoint = process.argv[1] && import.meta.url === `file://${process.argv[1]}`;
293
333
  if (isEntryPoint && process.argv[2] === 'task') {
294
334
  try {
@@ -303,9 +343,12 @@ if (isEntryPoint && process.argv[2] === 'task') {
303
343
  }
304
344
  }
305
345
 
306
- // Перевод колонки правит борду, поэтому идёт под ботом: от владельца задача выглядела бы
307
- // взятой в работу им самим. Отсутствие связи здесь отказ, а не пропуск: непереставленная
308
- // задача молча остаётся в прежней колонке, и расхождение всплывает только сверкой очереди.
346
+ // Перевод колонки правит борду. Токен машинной записи здесь необязателен: не назвавшее его
347
+ // дерево правит борду учётной записью, под которой залогинен клиент хостинга. Требование
348
+ // токена держало бы очередь работ у дерева, машинной записи не заводившего, и у дерева, чью
349
+ // запись ограничил хостинг. Отсутствие связи при этом — по-прежнему отказ, а не пропуск:
350
+ // непереставленная задача молча остаётся в прежней колонке, и расхождение всплывает только
351
+ // сверкой очереди.
309
352
  if (isEntryPoint && process.argv[2] === 'move') {
310
353
  const number = Number(process.argv[3]);
311
354
  const status = process.argv[4];
@@ -313,11 +356,7 @@ if (isEntryPoint && process.argv[2] === 'move') {
313
356
  console.error(`board: нужен номер задачи и колонка — node tools/board.mjs move 263 ${IN_PROGRESS_STATUS}`);
314
357
  process.exit(1);
315
358
  }
316
- const token = botToken();
317
- if (!token) {
318
- console.error(`board: нет токена бота (${TOKEN_PATH}) — борда правится машинной учётной записью`);
319
- process.exit(1);
320
- }
359
+ const token = botToken() ?? undefined;
321
360
  try {
322
361
  const moved = moveTask(number, status, { token });
323
362
  console.log(`#${number}: ${moved.from ?? 'вне колонок'} → ${moved.to}`);
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Сверка очереди работ с тем, что закон о поставке требует от задачи и её отчёта.
3
+ * Сверка очереди работ с тем, что закон о поставке требует от задачи и её PR.
4
4
  *
5
5
  * Гард поставки отбивает промах в момент, когда его совершают, но действует только
6
6
  * на команды агента: тикет, заведённый мимо него, и PR, открытый руками, он не
@@ -15,7 +15,7 @@
15
15
  * PR, — а проверка, требующая невыполнимого, обходится, а не исполняется. Имя ветки
16
16
  * стережёт гард в момент `git checkout -b` и `gh pr create`.
17
17
  *
18
- * Колонку задачи судит по её отчёту: открытый PR означает разбор, отсутствие — нет.
18
+ * Колонку задачи судит по её PR: открытый PR означает разбор, отсутствие — нет.
19
19
  * Момент, когда задачу берут в работу, отсюда не виден вовсе — ветки на борде нет, —
20
20
  * и «In progress» здесь не требуется ни от кого.
21
21
  *
@@ -29,13 +29,16 @@ import { join } from 'node:path';
29
29
 
30
30
  import {
31
31
  IN_REVIEW_STATUS,
32
+ OWNER,
32
33
  OfflineError,
34
+ REPO,
33
35
  STATUS_OPTIONS,
34
36
  TASK_KEY,
35
37
  botToken,
36
38
  fetchBoard,
37
39
  fetchIssues,
38
40
  fetchOpenPulls,
41
+ gh,
39
42
  numberFromTaskDir,
40
43
  numberFromTitle,
41
44
  taskDirs,
@@ -73,6 +76,34 @@ function closesNumbers(body) {
73
76
  return [...String(body ?? '').matchAll(/\bCloses\s+#(\d+)\b/gi)].map((match) => Number(match[1]));
74
77
  }
75
78
 
79
+ /**
80
+ * Строка обхода в теле PR. Форма та же, что читает гард поставки: строку она начинает и
81
+ * подстановки не принимает — иначе текст, называющий эту строку, снимает требование сам собой.
82
+ */
83
+ const FOLDER_SKIP = /^[ \t]*Task-folder-skip:[ \t]*[^\s<"'][^\s"']{2,}/im;
84
+
85
+ /**
86
+ * Везёт ли ветка PR папку своей задачи.
87
+ *
88
+ * Спрашивается ветка, а не рабочее дерево: папка, снесённая на машине и не закоммиченная,
89
+ * въедет вместе с веткой. Локальных ссылок тут мало — ветка PR может быть не подтянута
90
+ * сюда вовсе, — поэтому содержимое берётся у хостинга. Отказ «нет такого пути» означает, что
91
+ * папки нет; всё остальное поднимается выше и разбирается как отсутствие связи.
92
+ */
93
+ function folderInBranch(branch, options) {
94
+ const path = `${CONFIG.tasksDir}/${branch}`;
95
+ try {
96
+ gh(['api', `repos/${OWNER}/${REPO}/contents/${path}?ref=${encodeURIComponent(branch)}`, '--jq', 'length'], options);
97
+ return path;
98
+ } catch (error) {
99
+ if (error instanceof OfflineError) {
100
+ throw error;
101
+ }
102
+
103
+ return null;
104
+ }
105
+ }
106
+
76
107
  let checked = { issues: 0, pulls: 0 };
77
108
 
78
109
  // Черновики судятся по диску и потому проверяются всегда: связи для этого не нужно.
@@ -88,7 +119,7 @@ try {
88
119
  checked = { issues: issues.length, pulls: pulls.length };
89
120
 
90
121
  for (const item of board.foreign) {
91
- report(`борда: ${item} — на борде стоят задачи, а не отчёты о них`);
122
+ report(`борда: ${item} — на борде стоят задачи, а не PR о них`);
92
123
  }
93
124
  // Борду проверяем только у открытых задач. Закрытая ушла из очереди мержем, и колонки под
94
125
  // неё у борды нет: строку о ней нечем закрыть. Шесть таких строк висели в каждом прогоне и
@@ -124,9 +155,22 @@ try {
124
155
  } else {
125
156
  claimed.set(titleNumber, pull.number);
126
157
  }
158
+
159
+ // Папка задачи, лежащая в ветке открытого PR, — единственное расхождение, которое
160
+ // сверка обязана назвать ДО слияния: гард судит её на слиянии, а слияние нажимает
161
+ // человек в браузере, где хуков нет вовсе. Сказанная после, эта строка уже не чинится
162
+ // тем же PR — работа перешла дальше, и на разбор заводится вторая задача.
163
+ if (!FOLDER_SKIP.test(String(pull.body ?? '')) && pull.headRefName) {
164
+ const folder = folderInBranch(pull.headRefName, options);
165
+ if (folder !== null) {
166
+ report(
167
+ `PR #${pull.number}: ветка везёт папку задачи «${folder}/» — разбери её этим же PR или поставь в тело строку «Task-folder-skip: <причина>»`
168
+ );
169
+ }
170
+ }
127
171
  }
128
172
 
129
- // Колонка задачи и её отчёт сверяются в обе стороны: открытый PR при задаче в «Backlog»
173
+ // Колонка задачи и её PR сверяются в обе стороны: открытый PR при задаче в «Backlog»
130
174
  // читается как работа, к которой не приступали, а «In review» без открытого PR — как
131
175
  // разбор, которого никто не ждёт.
132
176
  for (const issue of open) {
@@ -134,7 +178,7 @@ try {
134
178
  const pull = claimed.get(issue.number);
135
179
  if (pull !== undefined && status !== IN_REVIEW) {
136
180
  report(
137
- `#${issue.number}: отчёт PR #${pull} открыт, а задача стоит «${status ?? 'вне колонок'}» — npm run task:move -- ${issue.number} ${IN_REVIEW_STATUS}`
181
+ `#${issue.number}: PR #${pull} открыт, а задача стоит «${status ?? 'вне колонок'}» — npm run task:move -- ${issue.number} ${IN_REVIEW_STATUS}`
138
182
  );
139
183
  }
140
184
  if (pull === undefined && status === IN_REVIEW) {
@@ -36,8 +36,9 @@
36
36
  *
37
37
  * Ненулевой код возврата и перечень расхождений.
38
38
  */
39
- import { readFileSync, readdirSync } from 'node:fs';
40
- import { join } from 'node:path';
39
+ import { createRequire } from 'node:module';
40
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
41
+ import { dirname, join } from 'node:path';
41
42
 
42
43
  import { allowlistOf, CONFIG, ROOT } from './rt-kit-checks.config.mjs';
43
44
 
@@ -76,6 +77,56 @@ function collectFiles(dir) {
76
77
  return files;
77
78
  }
78
79
 
80
+ /**
81
+ * Каталог объявлений внешнего пакета — разрешением модуля, а не путём в `node_modules`.
82
+ *
83
+ * Точек разрешения несколько: корень дерева и каждый его подпроект, объявивший этот пакет
84
+ * зависимостью. Пакет подпроекта в корне не лежит вовсе, и разрешение от корня его не находит;
85
+ * менеджер при этом вправе держать рядом несколько версий сразу, и обход хранилища по образцу
86
+ * пути выбрал бы ту, которую никто не ставит.
87
+ *
88
+ * Не нашлось — `null`, и внешние наборы просто не считаются: дерево без этого пакета должно
89
+ * получать сверку своих повторов, а не отказ чтения каталога.
90
+ */
91
+ function resolveExternalDir({ package: name, dir }) {
92
+ for (const from of [ROOT, ...holdersOf(name)]) {
93
+ try {
94
+ const manifest = createRequire(join(from, 'package.json')).resolve(`${name}/package.json`);
95
+ const found = join(dirname(manifest), dir);
96
+ if (existsSync(found)) {
97
+ return found;
98
+ }
99
+ } catch {
100
+ // Эта точка пакета не видит — пробуется следующая.
101
+ }
102
+ }
103
+
104
+ return null;
105
+ }
106
+
107
+ /** Подпроекты, объявившие пакет зависимостью: их манифесты и есть точки разрешения. */
108
+ function holdersOf(name) {
109
+ const found = [];
110
+ for (const root of SOURCE_ROOTS) {
111
+ if (!existsSync(join(ROOT, root))) {
112
+ continue;
113
+ }
114
+ for (const entry of readdirSync(join(ROOT, root), { withFileTypes: true })) {
115
+ const manifest = join(ROOT, root, entry.name, 'package.json');
116
+ if (!entry.isDirectory() || !existsSync(manifest)) {
117
+ continue;
118
+ }
119
+ const declared = JSON.parse(readFileSync(manifest, 'utf8'));
120
+ const fields = [declared.dependencies, declared.peerDependencies, declared.devDependencies];
121
+ if (fields.some((field) => field?.[name])) {
122
+ found.push(join(ROOT, root, entry.name));
123
+ }
124
+ }
125
+ }
126
+
127
+ return found;
128
+ }
129
+
79
130
  /** Корень либы: путь до каталога `src`. Повтор внутри одной либы повтором не считается */
80
131
  function libOf(path) {
81
132
  const parts = path.split('/');
@@ -116,8 +167,13 @@ const MIN_TABLE_PAIRS = 2;
116
167
  /**
117
168
  * Пакеты, чьи наборы считаются наравне с либами. Своё перечисление под уже
118
169
  * объявленный там набор — такая же копия, как и между двумя либами.
170
+ *
171
+ * Имя пакета и каталог внутри него объявляет дерево; путь в `node_modules` здесь не
172
+ * зашивается. Пакет, объявленный зависимостью подпроекта, в корневом `node_modules` не лежит
173
+ * вовсе — менеджер держит его в своём хранилище, — и проверка кончалась отказом чтения
174
+ * каталога, не дойдя до сверки ни разу.
119
175
  */
120
- const EXTERNAL_ENUM_SOURCES = ['node_modules/@rt-tools/utils/esm/lib/interfaces'];
176
+ const EXTERNAL_ENUM_SOURCES = CONFIG.externalEnums ?? [];
121
177
 
122
178
  const exportsByName = new Map();
123
179
  const settingsByName = new Map();
@@ -188,10 +244,14 @@ for (const path of SOURCE_ROOTS.flatMap((root) => collectFiles(root))) {
188
244
  collectEnums(text, lib);
189
245
  }
190
246
 
191
- for (const dir of EXTERNAL_ENUM_SOURCES) {
192
- for (const entry of readdirSync(join(ROOT, dir), { withFileTypes: true })) {
247
+ for (const source of EXTERNAL_ENUM_SOURCES) {
248
+ const dir = resolveExternalDir(source);
249
+ if (!dir) {
250
+ continue;
251
+ }
252
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
193
253
  if (entry.isFile() && entry.name.endsWith('.d.ts')) {
194
- collectEnums(readFileSync(join(ROOT, dir, entry.name), 'utf8'), dir.replace('node_modules/', ''));
254
+ collectEnums(readFileSync(join(dir, entry.name), 'utf8'), `${source.package}/${source.dir}`);
195
255
  }
196
256
  }
197
257
  }