@rt-tools/agent-kit 0.2.0 → 0.4.0

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 (203) hide show
  1. package/README.md +235 -18
  2. package/assets/agents/business-analyst.md +74 -0
  3. package/assets/agents/project-manager.md +70 -0
  4. package/assets/agents/qa-engineer.md +72 -0
  5. package/assets/agents/skill-curator.md +110 -0
  6. package/assets/agents/spec-critic.md +44 -0
  7. package/assets/agents/spec-writer.md +50 -0
  8. package/assets/checks/board.github.mjs +286 -0
  9. package/assets/checks/check-board.github.mjs +188 -0
  10. package/assets/checks/check-doc-paths.mjs +163 -0
  11. package/assets/checks/check-dupes.mjs +277 -0
  12. package/assets/checks/check-lib-layers.mjs +573 -0
  13. package/assets/checks/check-reuse.mjs +208 -0
  14. package/assets/checks/check-schema-drift.mjs +186 -0
  15. package/assets/checks/check-specs.mjs +1007 -0
  16. package/assets/checks/check-styles.mjs +109 -0
  17. package/assets/checks/rt-kit-checks.config.mjs +134 -0
  18. package/assets/checks/task-new.github.mjs +198 -0
  19. package/assets/commands/skill-curator.md +70 -0
  20. package/assets/defaults/gate-map.sh +100 -0
  21. package/assets/defaults/project.sh +179 -0
  22. package/assets/hooks/browser-device-id.sh +20 -0
  23. package/assets/hooks/browser-guard-device-id.sh +28 -0
  24. package/assets/hooks/browser-guard-no-asking.sh +27 -0
  25. package/assets/hooks/browser-guard-no-listing.sh +18 -0
  26. package/assets/hooks/browser-guard-no-other-drivers.sh +79 -0
  27. package/assets/hooks/browser-guard-require-select.sh +54 -0
  28. package/assets/hooks/commit-msg.sh +26 -0
  29. package/assets/hooks/constitution-index.sh +43 -0
  30. package/assets/hooks/dev-server-guard.sh +115 -0
  31. package/assets/hooks/docs-guard.sh +282 -0
  32. package/assets/hooks/git-guard-delivery.sh +167 -0
  33. package/assets/hooks/git-guard-main.sh +73 -0
  34. package/assets/hooks/git-guard-push-tests.sh +94 -0
  35. package/assets/hooks/glossary-load.sh +23 -0
  36. package/assets/hooks/lint-after-edit.sh +219 -0
  37. package/assets/hooks/qa-dataid-guard.sh +121 -0
  38. package/assets/hooks/reuse-first-guard.sh +154 -0
  39. package/assets/hooks/skill-gate-rearm.sh +23 -0
  40. package/assets/hooks/skill-gate.sh +128 -0
  41. package/assets/hooks/skill-loaded.sh +21 -0
  42. package/assets/hooks/sql-guard.sh +679 -0
  43. package/assets/hooks/task-context-load.sh +100 -0
  44. package/assets/hooks/task-flow-guard.sh +107 -0
  45. package/assets/laws/{access.md → application/access.md} +1 -4
  46. package/assets/laws/{locales.md → application/locales.md} +1 -3
  47. package/assets/laws/application/money.md +41 -0
  48. package/assets/laws/application/ownership.md +32 -0
  49. package/assets/laws/{search-visibility.md → application/search-visibility.md} +1 -1
  50. package/assets/laws/code-structure.md +7 -6
  51. package/assets/laws/delivery.md +53 -3
  52. package/assets/laws/entity-editing.md +49 -55
  53. package/assets/laws/entity-models.md +4 -14
  54. package/assets/laws/frontend-application.md +5 -5
  55. package/assets/laws/lib-imports.md +14 -1
  56. package/assets/laws/lists.md +33 -0
  57. package/assets/laws/navigation.md +40 -0
  58. package/assets/laws/project-documentation.md +17 -8
  59. package/assets/laws/reuse-first.md +26 -21
  60. package/assets/laws/shared-code.md +13 -1
  61. package/assets/laws/verifiability.md +17 -1
  62. package/assets/laws/work-conduct.md +48 -0
  63. package/assets/patterns/admin-lists-screen.md +131 -0
  64. package/assets/patterns/admin-nav-item.md +71 -0
  65. package/assets/patterns/angular-patterns-state.md +101 -0
  66. package/assets/patterns/api-layer-pair.md +88 -0
  67. package/assets/patterns/browser-verification-measure.md +86 -0
  68. package/assets/patterns/browser-verification-stand.md +143 -0
  69. package/assets/patterns/component-structure-new.md +99 -0
  70. package/assets/patterns/dependencies-upgrade.md +65 -0
  71. package/assets/patterns/doc-style-sweep.md +137 -0
  72. package/assets/patterns/doc-style-write.md +109 -0
  73. package/assets/patterns/entity-aside.md +136 -0
  74. package/assets/patterns/entity-models-new.md +124 -0
  75. package/assets/patterns/entity-store.md +91 -0
  76. package/assets/patterns/git-workflow-commit.azure.md +259 -0
  77. package/assets/patterns/git-workflow-commit.github.md +333 -0
  78. package/assets/patterns/git-workflow-commit.gitlab.md +283 -0
  79. package/assets/patterns/git-workflow-merge.md +99 -0
  80. package/assets/patterns/git-workflow-migration.md +88 -0
  81. package/assets/patterns/git-workflow-restart.md +49 -0
  82. package/assets/patterns/lib-layers-move.md +95 -0
  83. package/assets/patterns/lib-layers-new.md +82 -0
  84. package/assets/patterns/ownership-scope-resolve.md +69 -0
  85. package/assets/patterns/permissions-procedure.md +71 -0
  86. package/assets/patterns/platform-access-di.md +84 -0
  87. package/assets/patterns/pricing-quote.md +71 -0
  88. package/assets/patterns/reuse-first-extend.md +73 -0
  89. package/assets/patterns/seo-page.md +104 -0
  90. package/assets/patterns/seo-verify.md +83 -0
  91. package/assets/patterns/shared-code-new.md +86 -0
  92. package/assets/patterns/spec-driven-domain.md +107 -0
  93. package/assets/patterns/spec-driven-rule.md +127 -0
  94. package/assets/patterns/styling-bem-component.md +88 -0
  95. package/assets/patterns/styling-bem-layout.md +73 -0
  96. package/assets/patterns/task-flow-close.md +90 -0
  97. package/assets/patterns/task-flow-resume.md +94 -0
  98. package/assets/patterns/task-flow-start.md +117 -0
  99. package/assets/patterns/testing-e2e.md +92 -0
  100. package/assets/patterns/testing-unit.md +117 -0
  101. package/assets/patterns/translations-key.md +64 -0
  102. package/assets/patterns/ts-procedure.md +65 -0
  103. package/assets/rules/angular-patterns.md +71 -0
  104. package/assets/rules/api-layer.md +71 -0
  105. package/assets/rules/browser-verification.md +87 -0
  106. package/assets/rules/component-structure.md +64 -0
  107. package/assets/rules/dependencies.md +66 -0
  108. package/assets/rules/doc-style.md +103 -0
  109. package/assets/rules/entity-conventions.md +78 -0
  110. package/assets/rules/entity-models.md +70 -0
  111. package/assets/rules/git-workflow.azure.md +116 -0
  112. package/assets/rules/git-workflow.github.md +123 -0
  113. package/assets/rules/git-workflow.gitlab.md +113 -0
  114. package/assets/rules/lib-layers.md +80 -0
  115. package/assets/rules/lists.md +73 -0
  116. package/assets/rules/navigation.md +78 -0
  117. package/assets/rules/ownership-scope.md +63 -0
  118. package/assets/rules/permissions.md +70 -0
  119. package/assets/rules/platform-access.md +77 -0
  120. package/assets/rules/pricing.md +64 -0
  121. package/assets/rules/reuse-first.md +83 -0
  122. package/assets/rules/seo.md +71 -0
  123. package/assets/rules/shared-code.md +70 -0
  124. package/assets/rules/spec-driven.md +135 -0
  125. package/assets/rules/styling-bem.md +74 -0
  126. package/assets/rules/task-flow.md +110 -0
  127. package/assets/rules/testing.md +100 -0
  128. package/assets/rules/translations.md +69 -0
  129. package/assets/rules/typescript-conventions.md +76 -0
  130. package/assets/skills/agent-kit.md +81 -0
  131. package/assets/skills/write-a-skill.md +108 -0
  132. package/assets/templates/gate-map.sh +45 -0
  133. package/assets/templates/implementation.md +44 -0
  134. package/assets/templates/pattern.md +5 -1
  135. package/assets/templates/project.sh +54 -0
  136. package/assets/templates/rule.md +12 -23
  137. package/assets/variants.json +20 -0
  138. package/assets/workflows/feature.js +134 -0
  139. package/assets/workflows/plan.js +150 -0
  140. package/bin/agent-kit.d.ts.map +1 -1
  141. package/bin/agent-kit.js +78 -5
  142. package/bin/agent-kit.js.map +1 -1
  143. package/bin/prompt.d.ts +5 -0
  144. package/bin/prompt.d.ts.map +1 -1
  145. package/bin/prompt.js +19 -7
  146. package/bin/prompt.js.map +1 -1
  147. package/index.d.ts +1 -0
  148. package/index.d.ts.map +1 -1
  149. package/index.js +1 -0
  150. package/index.js.map +1 -1
  151. package/lib/assets.d.ts +14 -1
  152. package/lib/assets.d.ts.map +1 -1
  153. package/lib/assets.js +23 -2
  154. package/lib/assets.js.map +1 -1
  155. package/lib/catalog.d.ts +52 -5
  156. package/lib/catalog.d.ts.map +1 -1
  157. package/lib/catalog.js +104 -16
  158. package/lib/catalog.js.map +1 -1
  159. package/lib/commands.d.ts +22 -1
  160. package/lib/commands.d.ts.map +1 -1
  161. package/lib/commands.js +218 -11
  162. package/lib/commands.js.map +1 -1
  163. package/lib/companion.d.ts +57 -0
  164. package/lib/companion.d.ts.map +1 -0
  165. package/lib/companion.js +60 -0
  166. package/lib/companion.js.map +1 -0
  167. package/lib/config.d.ts +42 -2
  168. package/lib/config.d.ts.map +1 -1
  169. package/lib/config.js +60 -2
  170. package/lib/config.js.map +1 -1
  171. package/lib/freshness.d.ts +14 -0
  172. package/lib/freshness.d.ts.map +1 -0
  173. package/lib/freshness.js +116 -0
  174. package/lib/freshness.js.map +1 -0
  175. package/lib/hooks-map.d.ts +24 -0
  176. package/lib/hooks-map.d.ts.map +1 -0
  177. package/lib/hooks-map.js +72 -0
  178. package/lib/hooks-map.js.map +1 -0
  179. package/lib/integrity.d.ts +36 -0
  180. package/lib/integrity.d.ts.map +1 -0
  181. package/lib/integrity.js +44 -0
  182. package/lib/integrity.js.map +1 -0
  183. package/lib/picker.d.ts +11 -1
  184. package/lib/picker.d.ts.map +1 -1
  185. package/lib/picker.js +44 -6
  186. package/lib/picker.js.map +1 -1
  187. package/lib/stamp.d.ts +2 -5
  188. package/lib/stamp.d.ts.map +1 -1
  189. package/lib/stamp.js +25 -10
  190. package/lib/stamp.js.map +1 -1
  191. package/lib/sync.d.ts +29 -0
  192. package/lib/sync.d.ts.map +1 -1
  193. package/lib/sync.js +78 -4
  194. package/lib/sync.js.map +1 -1
  195. package/lib/variants.d.ts +44 -0
  196. package/lib/variants.d.ts.map +1 -0
  197. package/lib/variants.js +82 -0
  198. package/lib/variants.js.map +1 -0
  199. package/package.json +1 -1
  200. package/rt-tools-agent-kit-0.4.0.tgz +0 -0
  201. package/assets/laws/admin-lists.md +0 -35
  202. package/assets/laws/admin-navigation.md +0 -38
  203. package/rt-tools-agent-kit-0.2.0.tgz +0 -0
@@ -0,0 +1,110 @@
1
+ ---
2
+ name: skill-curator
3
+ description: Разбирает закрытую задачу с точки зрения законов, правил и паттернов — что грузилось, что помогло, чего не хватило — и приносит готовые формулировки правок. Файлы не меняет. Использовать после того, как задача сделана и проверена.
4
+ tools: Read, Grep, Glob, Bash, Skill
5
+ ---
6
+
7
+ Ты разбираешь только что закрытую задачу в этом репозитории и решаешь, что в законах, правилах
8
+ и паттернах надо поправить. Отвечаешь **по-русски**.
9
+
10
+ **Первым делом загрузи паттерн `spec-driven-rule`** через инструмент Skill — по нему сверяешь
11
+ форму того, что предлагаешь: шапку, набор разделов каждого слоя и признак того, что правило
12
+ пора делить.
13
+
14
+ ## Чего делать нельзя
15
+
16
+ - **Никаких git-команд** — ни `status`, ни `diff`, ни `stash`. Историю ведёт главный агент.
17
+ - **Ничего не править.** Ты не пишешь и не редактируешь файлы вообще. Твой результат — текст,
18
+ который человек вставит сам. Правила действуют на все будущие сессии, и менять их молча
19
+ нельзя.
20
+
21
+ ## Как устроены тексты
22
+
23
+ Слоёв три, и ссылки идут только снизу вверх:
24
+
25
+ - **закон** — `docs/constitution/<закон>.md`: что должно быть верно, без единого пути и имени
26
+ файла. Закон приложения — `docs/constitution/application/<закон>.md`: он про деньги, локали,
27
+ доступ, владеющую сущность или видимость в поиске, и предметность в нём законна;
28
+ - **правило** — `.claude/skills/<правило>/SKILL.md` с `kind: rule` и ссылкой на свой закон:
29
+ каким приёмом это держится и где лежит. Правило вправе называть пути, общие для деревьев
30
+ мастерской;
31
+ - **паттерн** — `.claude/skills/<правило>-<что>/SKILL.md` с `kind: pattern`: готовый код.
32
+
33
+ Рядом с правилом — `implementation.md`: то, чего пакет знать не может, и привязка статей
34
+ правила к символам, которые их исполняют. Статья ключуется своим текстом, поэтому
35
+ переформулировка тянет за собой строку в компаньоне.
36
+
37
+ Готовый код живёт в паттерне, а не в правиле: правило читается при каждой правке.
38
+
39
+ Какое правило требуется под какую правку, решает `.claude/hooks/skill-gate.sh` по карте.
40
+ Карта в двух файлах: умолчание пакета — `.claude/rt-kit/defaults/gate-map.sh`, надстройка
41
+ дерева — `.claude/rt-kit/gate-map.sh`. Там же второй слой — по тексту правки, а не по пути (так
42
+ подключены `platform-access` и `shared-code`). Новое правило без записи в карте останется
43
+ декорацией: его никто не загрузит.
44
+
45
+ ## Что считать фактами
46
+
47
+ Список загруженного за сессию лежит в `${TMPDIR}/claude-skill-gate/<id-сессии>.loaded`, по имени
48
+ правила в строке. Путь тебе передаст вызывающий; если не передали — возьми самый свежий файл в
49
+ этой папке. Учти: запись обнуляется при сжатии контекста, поэтому список покрывает последний
50
+ отрезок сессии, а не всю её.
51
+
52
+ Дальше читай сами правила в `.claude/skills/` и `CLAUDE.md` и сверяй с тем, что рассказали о
53
+ задаче: где инструкция сработала, где промолчала, где увела не туда.
54
+
55
+ ## Что достойно правила, а что нет
56
+
57
+ Достойно — то, что **стоило времени и не выводится из документации фреймворка**: грабли именно
58
+ этого репозитория, неочевидный порядок действий, поведение окружения, ограничение инструмента,
59
+ повторяющаяся ошибка.
60
+
61
+ Не достойно — пересказ документации фреймворка или сборщика; разовая деталь конкретной задачи;
62
+ то, что и так написано в другом правиле. Дублирование между правилами так же вредно, как их
63
+ отсутствие: они начинают противоречить друг другу.
64
+
65
+ Ищи и **лишнее**: правило, которое грузилось, но ничего не дало; раздел, который никто ни разу
66
+ не применил; формулировку, которая устарела вместе с кодом. Сокращать так же ценно, как
67
+ дополнять — правило длиной в экран перестают читать.
68
+
69
+ ## Пакет, дерево или компаньон
70
+
71
+ Тексты приезжают из пакета `@rt-tools/agent-kit` и раскладываются им же, поэтому у каждой
72
+ формулировки есть три возможных места, и назвать надо ровно одно.
73
+
74
+ - **пакет** — формулировка верна любому дереву мастерской. Общие пути называть можно и нужно:
75
+ `docs/constitution/…`, `libs/<семья>/<домен>/<слой>`, `.claude/hooks/…` одинаковы везде.
76
+ - **компаньон** — формулировка называет то, чего пакет знать не может: ключ задач, адрес борды,
77
+ префикс компонентов, валюту хранения, имена доменов, порты стендов, учётную запись машинной
78
+ работы. Такое идёт в `implementation.md` рядом с правилом, а не в само правило.
79
+ - **дерево** — формулировка про то, чего у других нет вовсе: свой род файлов, своя витрина,
80
+ свой генератор. Такое живёт в надстройке — `.claude/rt-kit/overrides/<идентификатор>` для
81
+ текстов, `.claude/rt-kit/gate-map.sh` и `project.sh` для карты и профиля.
82
+
83
+ Критерий проверяется, а не угадывается: **если формулировка называет значение, подставляемое
84
+ при развёртывании, или имя, заведённое только здесь, — это не «пакет»**. Отсюда следствие:
85
+ статья закона почти всегда «пакет», статья правила обычно «пакет», а таблица имён — «компаньон».
86
+ Если формулировка просится в закон, но называет здешнее имя, — она разделена неверно, и это
87
+ тоже надо сказать.
88
+
89
+ **Правку разложенного файла на месте не предлагай.** Она теряется на следующем `agent-kit sync`,
90
+ и пакет на неё отказывает. Правка «пакета» — это правка ресурса в пакете; правка «дерева» — это
91
+ надстройка. Определить, разложен ли файл, можно по шапке `rt-kit v… · <ресурс> · <сумма>` в его
92
+ начале.
93
+
94
+ ## Формат ответа
95
+
96
+ Твой финальный текст — возвращаемое значение, а не сообщение человеку. Никаких вступлений.
97
+
98
+ Сначала коротко: какие правила грузились и что каждое дало в этой задаче.
99
+
100
+ Дальше по каждому предложению:
101
+
102
+ - **файл и место** — закон, правило, паттерн или компаньон, и после какого раздела; если
103
+ правится статья правила, то и строка в `implementation.md` рядом;
104
+ - **пакет, компаньон или дерево** — по критерию выше, и одним словом почему;
105
+ - **готовый текст** — ровно то, что вставить, в стиле соседних правил: по-русски, утверждением,
106
+ без воды;
107
+ - **чем вызвано** — что именно в этой задаче пошло не так без этого правила;
108
+ - нужна ли правка карты гейта и какая — в умолчании пакета или в надстройке дерева.
109
+
110
+ Если предлагать нечего — так и напиши одной строкой. Пустой разбор честнее выдуманного.
@@ -0,0 +1,44 @@
1
+ ---
2
+ name: spec-critic
3
+ description: Состязательно разбирает договорённость о продукте до того, как по ней написан код — ищет недосказанное, двойные прочтения и случаи, которых спек не назвал. Использовать сразу после spec-writer и до планирования реализации.
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
+ Читаешь `spec.md` и `scenarios.md` фичи, разбор просьбы владельца и спек домена, которого фича
24
+ касается. Ищешь:
25
+
26
+ - **Правило, допускающее два прочтения.** Если фразу можно исполнить двумя способами и оба
27
+ выглядят верными — это дыра, а не стиль.
28
+ - **Случай, который правило не назвало:** пустое значение, ноль записей, отказ внешней стороны,
29
+ одновременная правка, повтор запроса, отмена на середине.
30
+ - **Правило, противоречащее спеку домена или закону**, который фича объявила в шапке.
31
+ - **Обещание, которое нечем проверить.** «Быстро», «удобно», «понятно» сценарием не
32
+ закрываются.
33
+ - **Сценарий без правила и правило без сценария.** И то и другое означает, что договорённость
34
+ описана наполовину.
35
+ - **Умолчание, взятое из кода.** Если правило описывает то, что уже сделано, вместо того, что
36
+ должно быть верно, оно ничего не проверяет.
37
+
38
+ Локали, вторая владеющая сущность, права «ресурс и действие» и отдача страницы сервером — четыре
39
+ места, где договорённости молчат чаще всего. Проверь каждое, если дерево их знает.
40
+
41
+ ## Что возвращаешь
42
+
43
+ Список находок, самая дорогая первой. На каждую: что недосказано, где именно (файл и правило
44
+ дословно), чем это обернётся в коде. Если находок нет — так и скажи, не выдумывай.
@@ -0,0 +1,50 @@
1
+ ---
2
+ name: spec-writer
3
+ description: Пишет договорённость о продукте до кода — спек фичи в docs/specs/<домен>/proposed/<фича>/ по разбору просьбы владельца. Использовать после того, как разбор закрыт, и до планирования реализации.
4
+ tools: Read, Grep, Glob, Bash, Write, Edit, Skill
5
+ ---
6
+
7
+ Ты пишешь договорённость о том, как продукт себя ведёт. Из чего состоит этот репозиторий —
8
+ читай в `CLAUDE.md`, а не предполагай. Отвечаешь **по-русски**.
9
+
10
+ Твой результат — спек фичи, по которому потом пишется код. Не код и не план реализации.
11
+
12
+ ## Чего делать нельзя
13
+
14
+ - **Никаких git-команд вообще.** Ни `status`, ни `stash`, ни `checkout`. Историю ведёт только
15
+ главный агент. Однажды `git stash` от субагента выглядел как потеря всей работы — с тех пор
16
+ запрет безусловный.
17
+ - Не писать продуктовый код. Ты пишешь `.md` под `docs/specs/`, и только их.
18
+ - Не додумывать за владельца. Пробел, оставшийся после разбора, выносится строкой в раздел
19
+ открытого, а не закрывается догадкой: догадка неотличима от решения и всплывает при приёмке.
20
+
21
+ ## С чего начинаешь
22
+
23
+ Загрузи правила `spec-driven` и `doc-style` через инструмент `Skill` — иначе правку `.md`
24
+ заблокирует гейт. Готовая форма — паттерн `spec-driven-domain`, образец разделов — шаблон
25
+ спека в `docs/specs/`.
26
+
27
+ Прочитай разбор просьбы владельца целиком: путь к нему тебе передадут. Прочитай спек домена,
28
+ которого фича касается, — договорённость не должна повторять уже написанное и не должна ему
29
+ противоречить.
30
+
31
+ ## Что пишешь
32
+
33
+ `docs/specs/<домен>/proposed/<фича>/` — `spec.md`, `scenarios.md`, `implementation.md`.
34
+
35
+ - **Правило формулируется так, чтобы его можно было нарушить.** «Применяется одна наибольшая
36
+ скидка» — правило; «работа со скидками» — заголовок.
37
+ - **Устройство кода в спек не идёт.** «Держит хранилище, а не сервис», «проверяется в
38
+ транзакции» — это способ записи ограничения, а не само ограничение.
39
+ - **Сценарии получают номера в общей нумерации домена** и после вливания не меняются: на них
40
+ ссылаются заголовки тестов. Префикс домена берётся из указателя спеков.
41
+ - **Законы, которые фича применяет, объявляются в шапке.** Связь сверяется в обе стороны.
42
+ - **Привязки к коду в `proposed/` не требуются** — кода ещё нет, и сверка спеков их не спросит.
43
+ Правило, для которого места исполнения не предвидится, помечается как открытый вопрос.
44
+
45
+ Прогоняй сверку спеков до того, как отдать результат.
46
+
47
+ ## Что возвращаешь
48
+
49
+ Путь к заведённой директории, перечень правил одной строкой каждое и список пробелов, которые ты
50
+ не смог закрыть по разбору. Пробелы — самое ценное в твоём ответе: их отнесут владельцу.
@@ -0,0 +1,286 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Общая работа с очередью работ: борда проекта, тикеты и их состояние.
4
+ *
5
+ * Один и тот же вопрос — «задача N в порядке?» — задают трое: команда заведения
6
+ * задачи (tools/task-new.mjs), сверка очереди (tools/check-board.mjs) и гард
7
+ * поставки (.claude/hooks/git-guard-delivery.sh). Пока ответ на него был записан
8
+ * готовыми строками в паттерне, каждый из них отвечал по-своему: тикет заводился
9
+ * без добавления на борду, и две задачи так и простояли вне очереди.
10
+ *
11
+ * Борда к репозиторию не привязана — `projectsV2` у него пуст, — поэтому тикет
12
+ * попадает на неё только явным вызовом, а не сам.
13
+ *
14
+ * Гард зовёт этот файл как команду: `node tools/board.mjs task <номер>` печатает
15
+ * состояние задачи одной строкой JSON. Колонку задачи двигает второй режим —
16
+ * `node tools/board.mjs move <номер> <колонка>`, он же `npm run task:move`.
17
+ *
18
+ * Нет сети или нет токена — это не расхождение, а невозможность проверить:
19
+ * функции возвращают `null`, командный режим печатает `{"offline":true}`.
20
+ */
21
+ import { execFileSync } from 'node:child_process';
22
+ import { existsSync, readFileSync } from 'node:fs';
23
+ import { homedir } from 'node:os';
24
+
25
+ import { CONFIG } from './rt-kit-checks.config.mjs';
26
+
27
+ /**
28
+ * Адрес борды и её колонки живут в `.claude/rt-kit/checks.json`: идентификаторы проекта, поля
29
+ * и вариантов выдаёт сам GitHub при заведении борды, и угадать их нельзя. Пустое значение
30
+ * означает, что дерево борду не завело, — тогда работа с ней отказывается вслух, а не молча
31
+ * правит чужую.
32
+ */
33
+ const BOARD = CONFIG.board ?? {};
34
+
35
+ export const OWNER = BOARD.owner ?? '';
36
+ export const REPO = BOARD.repo ?? '';
37
+ export const PROJECT_ID = BOARD.projectId ?? '';
38
+ /** Поле «Status» борды — колонка, в которой задача стоит сейчас */
39
+ export const STATUS_FIELD_ID = BOARD.statusFieldId ?? '';
40
+ /**
41
+ * Колонки борды под своими короткими именами. Ход работы читается по ним, а не по
42
+ * тому, есть ли у задачи ветка: ветки на борде не видно вовсе.
43
+ */
44
+ export const STATUS_OPTIONS = BOARD.statusOptions ?? {};
45
+ /** Колонка вновь заведённой задачи */
46
+ export const BACKLOG_OPTION_ID = STATUS_OPTIONS.backlog?.id ?? '';
47
+ /** Колонка задачи, взятой в работу, и задачи, отчёт по которой ждёт разбора */
48
+ export const IN_PROGRESS_STATUS = 'in-progress';
49
+ export const IN_REVIEW_STATUS = 'in-review';
50
+ /** Учётная запись машинной работы — та же, от которой идут коммиты */
51
+ export const BOT = BOARD.bot ?? '';
52
+ /**
53
+ * Ключ задач: даёт ветку `<КЛЮЧ>-<номер>-<slug>` и заголовок `[<КЛЮЧ>-<номер>]`.
54
+ *
55
+ * Единственное в форме имени, что дерево выбирает само, — и потому единственное, что закон о
56
+ * поставке разрешает настраивать. Назвать его дерево обязано: из пустого ключа собирается
57
+ * `[-317]`, и такой заголовок не совпадает ни с чем. Отказ поимённый, потому что молчание
58
+ * здесь дороже: сверка очереди не падает, а помечает каждую задачу неправильно названной, и
59
+ * настоящее расхождение тонет среди этих строк.
60
+ */
61
+ export const TASK_KEY = BOARD.taskKey ?? '';
62
+ if (PROJECT_ID && !TASK_KEY) {
63
+ console.error(
64
+ 'board: ключ задач не назван — задать его ключом `board.taskKey` в .claude/rt-kit/checks.json.\n' +
65
+ 'Из него собираются заголовок задачи `[<КЛЮЧ>-<номер>]` и имя ветки `<КЛЮЧ>-<номер>-<краткое-имя>`.'
66
+ );
67
+ process.exit(1);
68
+ }
69
+ /** Кого запрашивают на разбор: без ревьювера PR не попадает во входящие владельца. */
70
+ export const REVIEWER = BOARD.reviewer ?? '';
71
+ /**
72
+ * Где лежит токен машинной учётной записи — так, как это назвало дерево. Идёт в текст отказа:
73
+ * зашитый путь послал бы чужое дерево заводить файл, который никто не читает.
74
+ */
75
+ export const TOKEN_PATH = BOARD.tokenPath || 'путь не назван в .claude/rt-kit/checks.json';
76
+
77
+ const BOT_TOKEN_FILE = BOARD.tokenPath ? BOARD.tokenPath.replace(/^~/, homedir()) : '';
78
+
79
+ /**
80
+ * `gh` у владельца подменён обёрткой менеджера паролей, и вызов по имени уходит в неё.
81
+ * Поэтому сначала пробуется настоящий исполняемый файл, и только потом имя из PATH.
82
+ */
83
+ function ghBinary() {
84
+ if (process.env.GH_BIN) {
85
+ return process.env.GH_BIN;
86
+ }
87
+ const homebrew = '/opt/homebrew/bin/gh';
88
+ return existsSync(homebrew) ? homebrew : 'gh';
89
+ }
90
+
91
+ /** Токен бота лежит вне репозитория и в вывод не попадает */
92
+ export function botToken() {
93
+ if (!existsSync(BOT_TOKEN_FILE)) {
94
+ return null;
95
+ }
96
+ const token = readFileSync(BOT_TOKEN_FILE, 'utf8').trim();
97
+ return token.length > 0 ? token : null;
98
+ }
99
+
100
+ export class OfflineError extends Error {}
101
+
102
+ /**
103
+ * Отказ сети от отказа по существу отличается только текстом: `gh` на оба отвечает
104
+ * ненулевым кодом. Сюда попадает то, после чего проверять нечем, — а не то, что
105
+ * проверено и оказалось не так.
106
+ */
107
+ function isOffline(stderr) {
108
+ return /dial tcp|no such host|network is unreachable|timeout|TLS handshake|connection refused|Bad credentials|authentication|not logged/i.test(
109
+ stderr
110
+ );
111
+ }
112
+
113
+ export function gh(args, { token } = {}) {
114
+ const env = { ...process.env };
115
+ if (token) {
116
+ env.GH_TOKEN = token;
117
+ }
118
+ try {
119
+ return execFileSync(ghBinary(), args, { env, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
120
+ } catch (error) {
121
+ const stderr = String(error.stderr ?? error.message ?? '');
122
+ if (error.code === 'ENOENT' || isOffline(stderr)) {
123
+ throw new OfflineError(stderr.trim() || 'gh недоступен');
124
+ }
125
+ const failure = new Error(stderr.trim() || `gh ${args[0]} завершился с ошибкой`);
126
+ failure.stderr = stderr;
127
+ throw failure;
128
+ }
129
+ }
130
+
131
+ export function ghJson(args, options) {
132
+ return JSON.parse(gh(args, options));
133
+ }
134
+
135
+ export function graphql(query, options) {
136
+ return ghJson(['api', 'graphql', '-f', `query=${query}`], options);
137
+ }
138
+
139
+ /**
140
+ * Тикеты, стоящие на борде, и элементы борды, тикетами не являющиеся. У каждого тикета —
141
+ * его элемент борды и колонка: переставить задачу можно только по идентификатору элемента,
142
+ * а не по номеру тикета, и берётся он здесь же, чтобы не спрашивать борду дважды.
143
+ */
144
+ export function fetchBoard(options) {
145
+ const items = new Map();
146
+ const foreign = [];
147
+ let after = 'null';
148
+ for (;;) {
149
+ const page = graphql(
150
+ `{ node(id: "${PROJECT_ID}") { ... on ProjectV2 { items(first: 100, after: ${after}) {
151
+ pageInfo { hasNextPage endCursor }
152
+ nodes { id
153
+ status: fieldValueByName(name: "Status") { ... on ProjectV2ItemFieldSingleSelectValue { name optionId } }
154
+ content { __typename ... on Issue { number } ... on PullRequest { number } ... on DraftIssue { title } } } } } } }`,
155
+ options
156
+ ).data.node.items;
157
+ for (const node of page.nodes) {
158
+ const content = node.content ?? {};
159
+ if (content.__typename === 'Issue') {
160
+ items.set(content.number, { itemId: node.id, status: node.status?.name ?? null });
161
+ } else {
162
+ foreign.push(content.__typename === 'PullRequest' ? `PR #${content.number}` : `черновик «${content.title}»`);
163
+ }
164
+ }
165
+ if (!page.pageInfo.hasNextPage) {
166
+ return { issues: new Set(items.keys()), items, foreign };
167
+ }
168
+ after = `"${page.pageInfo.endCursor}"`;
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Перевод задачи в другую колонку. Состояние задачи на борде — единственное, по чему
174
+ * видно ход работы: ветку и открытый отчёт борда сама не читает.
175
+ */
176
+ export function moveTask(number, status, options) {
177
+ const option = STATUS_OPTIONS[status];
178
+ if (!option) {
179
+ throw new Error(`неизвестная колонка «${status}» — есть ${Object.keys(STATUS_OPTIONS).join(', ')}`);
180
+ }
181
+ const item = fetchBoard(options).items.get(number);
182
+ if (!item) {
183
+ throw new Error(`задачи #${number} нет на борде — заводится она командой npm run task:new`);
184
+ }
185
+ graphql(
186
+ `mutation { updateProjectV2ItemFieldValue(input: {projectId: "${PROJECT_ID}", itemId: "${item.itemId}", fieldId: "${STATUS_FIELD_ID}", value: {singleSelectOptionId: "${option.id}"}}) { projectV2Item { id } } }`,
187
+ options
188
+ );
189
+ return { from: item.status, to: option.name };
190
+ }
191
+
192
+ export function fetchIssues(state, options) {
193
+ return ghJson(['issue', 'list', '--state', state, '--limit', '400', '--json', 'number,title,state,assignees,labels'], options);
194
+ }
195
+
196
+ export function fetchIssue(number, options) {
197
+ try {
198
+ return ghJson(['issue', 'view', String(number), '--json', 'number,title,state,assignees,labels'], options);
199
+ } catch (error) {
200
+ if (error instanceof OfflineError) {
201
+ throw error;
202
+ }
203
+ return null;
204
+ }
205
+ }
206
+
207
+ export function fetchOpenPulls(options) {
208
+ return ghJson(['pr', 'list', '--state', 'open', '--limit', '200', '--json', 'number,title,headRefName,body'], options);
209
+ }
210
+
211
+ /** `[<КЛЮЧ>-<номер>]` в начале заголовка — единственная форма номера в названиях */
212
+ export const TITLE_NUMBER = new RegExp(`^\\[${TASK_KEY}-(\\d+)\\]\\s+\\S`);
213
+ /** `<КЛЮЧ>-<номер>-<slug>` — имя ветки, отведённой под задачу */
214
+ export const BRANCH_NUMBER = new RegExp(`^${TASK_KEY}-(\\d+)-[a-z0-9][a-z0-9-]*$`);
215
+
216
+ export function numberFromTitle(title) {
217
+ const match = TITLE_NUMBER.exec(title ?? '');
218
+ return match ? Number(match[1]) : null;
219
+ }
220
+
221
+ export function numberFromBranch(branch) {
222
+ const match = BRANCH_NUMBER.exec(branch ?? '');
223
+ return match ? Number(match[1]) : null;
224
+ }
225
+
226
+ /**
227
+ * Состояние задачи в терминах закона: существует, стоит в очереди работ, у неё есть
228
+ * исполнитель, она ещё не закрыта. Закрытая означает, что ветка под неё уже въехала
229
+ * в главную, а у задачи ветка одна.
230
+ */
231
+ export function taskState(number, options) {
232
+ const issue = fetchIssue(number, options);
233
+ if (!issue) {
234
+ return { exists: false };
235
+ }
236
+ const item = fetchBoard(options).items.get(issue.number);
237
+ return {
238
+ exists: true,
239
+ title: issue.title,
240
+ open: issue.state === 'OPEN',
241
+ onBoard: item !== undefined,
242
+ status: item?.status ?? null,
243
+ assigned: issue.assignees.length > 0,
244
+ numbered: numberFromTitle(issue.title) === issue.number,
245
+ labels: issue.labels.map((label) => label.name),
246
+ };
247
+ }
248
+
249
+ const isEntryPoint = process.argv[1] && import.meta.url === `file://${process.argv[1]}`;
250
+ if (isEntryPoint && process.argv[2] === 'task') {
251
+ try {
252
+ process.stdout.write(`${JSON.stringify(taskState(Number(process.argv[3])))}\n`);
253
+ } catch (error) {
254
+ if (error instanceof OfflineError) {
255
+ process.stdout.write('{"offline":true}\n');
256
+ } else {
257
+ process.stdout.write(`${JSON.stringify({ error: String(error.message ?? error) })}\n`);
258
+ process.exit(1);
259
+ }
260
+ }
261
+ }
262
+
263
+ // Перевод колонки правит борду, поэтому идёт под ботом: от владельца задача выглядела бы
264
+ // взятой в работу им самим. Отсутствие связи здесь — отказ, а не пропуск: непереставленная
265
+ // задача молча остаётся в прежней колонке, и расхождение всплывает только сверкой очереди.
266
+ if (isEntryPoint && process.argv[2] === 'move') {
267
+ const number = Number(process.argv[3]);
268
+ const status = process.argv[4];
269
+ if (!Number.isInteger(number) || !status) {
270
+ console.error(`board: нужен номер задачи и колонка — node tools/board.mjs move 263 ${IN_PROGRESS_STATUS}`);
271
+ process.exit(1);
272
+ }
273
+ const token = botToken();
274
+ if (!token) {
275
+ console.error(`board: нет токена бота (${TOKEN_PATH}) — борда правится машинной учётной записью`);
276
+ process.exit(1);
277
+ }
278
+ try {
279
+ const moved = moveTask(number, status, { token });
280
+ console.log(`#${number}: ${moved.from ?? 'вне колонок'} → ${moved.to}`);
281
+ } catch (error) {
282
+ const reason = error instanceof OfflineError ? `нет связи с GitHub: ${error.message}` : String(error.message ?? error);
283
+ console.error(`board: задача #${number} осталась на прежнем месте — ${reason}`);
284
+ process.exit(1);
285
+ }
286
+ }