@rt-tools/agent-kit 0.3.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 (201) hide show
  1. package/README.md +194 -30
  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 +0 -0
  23. package/assets/hooks/browser-guard-device-id.sh +2 -1
  24. package/assets/hooks/browser-guard-no-asking.sh +27 -0
  25. package/assets/hooks/browser-guard-no-listing.sh +2 -1
  26. package/assets/hooks/browser-guard-no-other-drivers.sh +2 -1
  27. package/assets/hooks/browser-guard-require-select.sh +2 -1
  28. package/assets/hooks/commit-msg.sh +1 -1
  29. package/assets/hooks/constitution-index.sh +5 -4
  30. package/assets/hooks/dev-server-guard.sh +8 -6
  31. package/assets/hooks/docs-guard.sh +223 -37
  32. package/assets/hooks/git-guard-delivery.sh +86 -29
  33. package/assets/hooks/git-guard-main.sh +1 -0
  34. package/assets/hooks/git-guard-push-tests.sh +34 -13
  35. package/assets/hooks/glossary-load.sh +23 -0
  36. package/assets/hooks/lint-after-edit.sh +155 -30
  37. package/assets/hooks/qa-dataid-guard.sh +72 -32
  38. package/assets/hooks/reuse-first-guard.sh +105 -34
  39. package/assets/hooks/skill-gate-rearm.sh +1 -0
  40. package/assets/hooks/skill-gate.sh +75 -15
  41. package/assets/hooks/skill-loaded.sh +1 -0
  42. package/assets/hooks/sql-guard.sh +606 -56
  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 +29 -22
  66. package/assets/patterns/api-layer-pair.md +40 -30
  67. package/assets/patterns/browser-verification-measure.md +41 -38
  68. package/assets/patterns/browser-verification-stand.md +106 -42
  69. package/assets/patterns/component-structure-new.md +33 -32
  70. package/assets/patterns/dependencies-upgrade.md +65 -0
  71. package/assets/patterns/doc-style-sweep.md +65 -28
  72. package/assets/patterns/doc-style-write.md +36 -33
  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 +42 -25
  80. package/assets/patterns/git-workflow-migration.md +61 -31
  81. package/assets/patterns/git-workflow-restart.md +20 -20
  82. package/assets/patterns/lib-layers-move.md +50 -32
  83. package/assets/patterns/lib-layers-new.md +41 -29
  84. package/assets/patterns/ownership-scope-resolve.md +69 -0
  85. package/assets/patterns/permissions-procedure.md +35 -33
  86. package/assets/patterns/platform-access-di.md +39 -25
  87. package/assets/patterns/pricing-quote.md +71 -0
  88. package/assets/patterns/reuse-first-extend.md +22 -22
  89. package/assets/patterns/seo-page.md +52 -40
  90. package/assets/patterns/seo-verify.md +48 -29
  91. package/assets/patterns/shared-code-new.md +37 -31
  92. package/assets/patterns/spec-driven-domain.md +44 -37
  93. package/assets/patterns/spec-driven-rule.md +55 -40
  94. package/assets/patterns/styling-bem-component.md +43 -32
  95. package/assets/patterns/styling-bem-layout.md +30 -24
  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 +53 -51
  100. package/assets/patterns/testing-unit.md +70 -46
  101. package/assets/patterns/translations-key.md +32 -19
  102. package/assets/patterns/ts-procedure.md +24 -25
  103. package/assets/rules/angular-patterns.md +46 -27
  104. package/assets/rules/api-layer.md +46 -28
  105. package/assets/rules/browser-verification.md +66 -48
  106. package/assets/rules/component-structure.md +43 -27
  107. package/assets/rules/dependencies.md +66 -0
  108. package/assets/rules/doc-style.md +81 -39
  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 +56 -30
  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 +43 -25
  119. package/assets/rules/platform-access.md +57 -29
  120. package/assets/rules/pricing.md +64 -0
  121. package/assets/rules/reuse-first.md +57 -43
  122. package/assets/rules/seo.md +51 -30
  123. package/assets/rules/shared-code.md +51 -26
  124. package/assets/rules/spec-driven.md +96 -50
  125. package/assets/rules/styling-bem.md +54 -39
  126. package/assets/rules/task-flow.md +110 -0
  127. package/assets/rules/testing.md +78 -47
  128. package/assets/rules/translations.md +48 -31
  129. package/assets/rules/typescript-conventions.md +57 -27
  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 +23 -15
  133. package/assets/templates/implementation.md +14 -8
  134. package/assets/templates/pattern.md +1 -1
  135. package/assets/templates/project.sh +32 -19
  136. package/assets/templates/rule.md +1 -1
  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 +8 -3
  152. package/lib/assets.d.ts.map +1 -1
  153. package/lib/assets.js +13 -3
  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 +202 -14
  162. package/lib/commands.js.map +1 -1
  163. package/lib/companion.d.ts +5 -1
  164. package/lib/companion.d.ts.map +1 -1
  165. package/lib/companion.js +29 -2
  166. package/lib/companion.js.map +1 -1
  167. package/lib/config.d.ts +26 -9
  168. package/lib/config.d.ts.map +1 -1
  169. package/lib/config.js +41 -15
  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/sync.d.ts +26 -0
  188. package/lib/sync.d.ts.map +1 -1
  189. package/lib/sync.js +59 -4
  190. package/lib/sync.js.map +1 -1
  191. package/lib/variants.d.ts +44 -0
  192. package/lib/variants.d.ts.map +1 -0
  193. package/lib/variants.js +82 -0
  194. package/lib/variants.js.map +1 -0
  195. package/package.json +1 -1
  196. package/rt-tools-agent-kit-0.4.0.tgz +0 -0
  197. package/assets/laws/admin-lists.md +0 -35
  198. package/assets/laws/admin-navigation.md +0 -38
  199. package/assets/patterns/git-workflow-commit.md +0 -175
  200. package/assets/rules/git-workflow.md +0 -106
  201. package/rt-tools-agent-kit-0.3.0.tgz +0 -0
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Проверка того, что класс элемента BEM подкреплён правилом.
4
+ *
5
+ * Класс без правила заводится молча: раскладка переезжает из файла экрана в
6
+ * общий слой, а `rtElem` остаётся в шаблоне и больше ничему не соответствует.
7
+ * Ни линт, ни сборка, ни браузер этого не показывают — лишний класс просто
8
+ * ничего не делает, и разметка обрастает именами, за которыми ничего нет.
9
+ *
10
+ * Совпадение считается по имени элемента, а не по паре «блок — элемент»:
11
+ * `rtElem` берёт имя блока у ближайшего предка с `rtBlock`, и повторить этот
12
+ * разбор по тексту шаблона нечем. Из-за этого проверка пропускает класс, у
13
+ * которого правило есть, но у чужого блока, — направление выбрано в сторону
14
+ * ложных пропусков, а не ложных отказов.
15
+ *
16
+ * Динамическое `[rtElem]` не считается: имя там известно только в рантайме.
17
+ *
18
+ * Накопленное к моменту заведения проверки лежит в tools/styles-allowlist.json
19
+ * и отказом не считается: гейт падает на НОВОМ классе без правила, а старое
20
+ * остаётся видимым числом в сводке.
21
+ *
22
+ * Ненулевой код возврата и перечень расхождений.
23
+ */
24
+ import { readFileSync, readdirSync } from 'node:fs';
25
+ import { join } from 'node:path';
26
+
27
+ import { allowlistOf, CONFIG, ROOT } from './rt-kit-checks.config.mjs';
28
+
29
+ const ALLOWLIST = allowlistOf('styles');
30
+ const SOURCE_ROOTS = CONFIG.sourceRoots;
31
+ const SKIPPED_DIRS = CONFIG.skippedDirs;
32
+
33
+ const ELEM_RE = /rtElem="([a-z0-9-]+)"/g;
34
+ /** Объявление элемента: и вложенное `&__item`, и полное `.<блок>__item` */
35
+ const RULE_RE = /__([a-z0-9-]+)/g;
36
+
37
+ const allowlist = JSON.parse(readFileSync(join(ROOT, ALLOWLIST), 'utf8'));
38
+ const known = new Set([...(allowlist.accepted ?? []), ...(allowlist.debt ?? [])]);
39
+ const debt = new Set(allowlist.debt ?? []);
40
+
41
+ function collectFiles(dir, extension) {
42
+ const files = [];
43
+ for (const entry of readdirSync(join(ROOT, dir), { withFileTypes: true })) {
44
+ if (SKIPPED_DIRS.includes(entry.name)) {
45
+ continue;
46
+ }
47
+ const path = `${dir}/${entry.name}`;
48
+ if (entry.isDirectory()) {
49
+ files.push(...collectFiles(path, extension));
50
+ } else if (entry.name.endsWith(extension)) {
51
+ files.push(path);
52
+ }
53
+ }
54
+ return files;
55
+ }
56
+
57
+ const declared = new Set();
58
+ const usedIn = new Map();
59
+
60
+ for (const root of SOURCE_ROOTS) {
61
+ for (const path of collectFiles(root, '.scss')) {
62
+ for (const match of readFileSync(join(ROOT, path), 'utf8').matchAll(RULE_RE)) {
63
+ declared.add(match[1]);
64
+ }
65
+ }
66
+
67
+ for (const path of collectFiles(root, '.html')) {
68
+ for (const match of readFileSync(join(ROOT, path), 'utf8').matchAll(ELEM_RE)) {
69
+ const name = match[1];
70
+ if (!usedIn.has(name)) {
71
+ usedIn.set(name, new Set());
72
+ }
73
+ usedIn.get(name).add(path);
74
+ }
75
+ }
76
+ }
77
+
78
+ const findings = [];
79
+
80
+ for (const [name, files] of [...usedIn].sort(([first], [second]) => first.localeCompare(second))) {
81
+ if (declared.has(name)) {
82
+ continue;
83
+ }
84
+ const where = [...files].sort().join(', ');
85
+ findings.push({ key: `elem ${name} @ ${where}`, text: `rtElem="${name}" — правила нет ни в одном файле стилей: ${where}` });
86
+ }
87
+
88
+ const fresh = findings.filter((finding) => !known.has(finding.key));
89
+ const staleKeys = [...known].filter((key) => !findings.some((finding) => finding.key === key));
90
+
91
+ if (process.argv.includes('--baseline')) {
92
+ console.log(JSON.stringify({ ...allowlist, debt: findings.map((finding) => finding.key).sort() }, null, 4));
93
+ process.exit(0);
94
+ }
95
+
96
+ const problems = [
97
+ ...fresh.map((finding) => finding.text),
98
+ ...staleKeys.map((key) => `${key}: значится в ${ALLOWLIST}, но класс уже подкреплён правилом — строку убрать`),
99
+ ];
100
+
101
+ if (problems.length > 0) {
102
+ console.error(`check-styles: расхождений ${problems.length}\n`);
103
+ problems.forEach((problem) => console.error(` ${problem}`));
104
+ process.exit(1);
105
+ }
106
+
107
+ console.log(
108
+ `check-styles: классов без правила ${findings.length}, из них принято ${findings.length - debt.size}, долг ${debt.size} — новых нет`
109
+ );
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Настройки проверок: что считать исходниками, куда не ходить и где лежат списки долгов.
4
+ *
5
+ * Проверки везёт пакет, а корни и имена — своё у каждого дерева: где-то `apps` и `libs`,
6
+ * где-то `projects`. Держать их в каждом скрипте отдельно значило бы править девять файлов
7
+ * ради одного переименования, и девятый забывался бы молча.
8
+ *
9
+ * Умолчания здесь, надстройка — `.claude/rt-kit/checks.json` в дереве проекта. Нет файла —
10
+ * действуют умолчания; есть — его ключи ложатся поверх, по одному, а не целиком: проект,
11
+ * назвавший только корни, не теряет список пропускаемых каталогов.
12
+ *
13
+ * Слияние идёт по вложенным ключам: дерево, назвавшее один ключ борды, сохраняет остальные.
14
+ * Пока слияние было одноуровневым, вложенный объект замещался целиком, и такое дерево видело
15
+ * отказ «нет токена бота» — то есть читало неполный конфиг как неполадку машины.
16
+ *
17
+ * Глубже вложенных объектов слияние не идёт: список, названный деревом, замещает умолчание
18
+ * целиком. Дописывать в список нельзя — иначе `skippedDirs`, из которого дерево что-то убрало,
19
+ * приезжал бы обратно, и убрать оттуда стало бы невозможно вовсе.
20
+ */
21
+ import { existsSync, readFileSync } from 'node:fs';
22
+ import { dirname, join, resolve } from 'node:path';
23
+ import { fileURLToPath } from 'node:url';
24
+
25
+ /** Корень дерева: проверки лежат в его `tools/`, поэтому на уровень выше. */
26
+ export const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
27
+
28
+ const CONFIG_PATH = '.claude/rt-kit/checks.json';
29
+
30
+ const DEFAULTS = {
31
+ /** Где лежит код, который проверки читают. */
32
+ sourceRoots: ['apps', 'libs'],
33
+ /** Куда не ходить никогда: сборка, зависимости, порождённое. */
34
+ skippedDirs: ['node_modules', 'dist', '.git', '.nx', 'tmp', 'coverage', 'worktrees', 'gen', 'generated'],
35
+ /** Где лежат тексты проекта. */
36
+ docsDir: 'docs',
37
+ /** Отложенное: про него проверки молчат — оно описывает прошлое, а не дерево. */
38
+ archiveDir: 'docs/archive/',
39
+ /** Где лежат спеки доменов; пусто — их в дереве нет, и сверка спеков не запускается. */
40
+ specsDir: 'docs/specs',
41
+ tasksDir: 'docs/tasks',
42
+ /** Куда сложены списки принятых долгов. */
43
+ allowlistDir: 'tools',
44
+ /** Корни сквозных тестов; пусто — их в дереве нет. */
45
+ e2eRoots: ['apps/site-e2e', 'apps/admin-e2e'],
46
+ /** Корни бэкенда: у него нет ни компонентов, ни шаблонов, и часть признаков к нему не применяется. */
47
+ backendRoots: ['libs/api/', 'apps/api/'],
48
+ /** Схема хранилища и её миграции; пусто — хранилища в дереве нет. */
49
+ schemaFile: 'prisma/schema.prisma',
50
+ migrationsDir: 'prisma/migrations',
51
+ /** Каталог, под которым лежат семьи либ: `<корень>/<семья>/<домен>/<слой>`. */
52
+ libsRoot: 'libs',
53
+ /** Семьи фронтовых либ: `<корень либ>/<семья>/<домен>/<слой>`. */
54
+ families: ['site', 'admin'],
55
+ /** Семья бэкенда под тем же корнем: у её доменов своя лесенка слоёв. */
56
+ apiFamily: 'api',
57
+ /** Область алиасов импорта: `@область/<семья>/<домен>/<слой>` в `tsconfig.base.json`. */
58
+ importScope: '@app',
59
+ /**
60
+ * Признаки боевого хранилища: порт, адрес, имя домена. Проверки туда не ходят ни читать,
61
+ * ни писать — схема на проде меняется выкаткой. Пусто — признаков нет, и адрес боевым
62
+ * не считается никогда.
63
+ */
64
+ productionMarks: [],
65
+ /** Очередь работ: владелец, репозиторий, борда и учётная запись машинной работы. */
66
+ board: {
67
+ owner: '',
68
+ repo: '',
69
+ projectId: '',
70
+ /** Ключ задач: даёт ветку `<ключ>-<номер>-<slug>` и заголовок `[<ключ>-<номер>]`. */
71
+ taskKey: '',
72
+ bot: '',
73
+ tokenPath: '',
74
+ reviewer: '',
75
+ },
76
+ };
77
+
78
+ function readOverrides() {
79
+ const path = join(ROOT, CONFIG_PATH);
80
+ if (!existsSync(path)) {
81
+ return {};
82
+ }
83
+ try {
84
+ return JSON.parse(readFileSync(path, 'utf8'));
85
+ } catch (error) {
86
+ console.error(`${CONFIG_PATH} — не разбирается как JSON: ${error.message}`);
87
+ process.exit(1);
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Слияние надстройки с умолчанием по вложенным ключам.
93
+ *
94
+ * Объект сливается ключ за ключом, всё остальное — замещается: список, названный деревом,
95
+ * приходит целиком, потому что «дописать в список» и «убрать из списка» — разные действия, а
96
+ * различить их в JSON нечем.
97
+ */
98
+ const mergeDeep = (base, over) => {
99
+ const isPlain = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
100
+ if (!isPlain(base) || !isPlain(over)) {
101
+ return over === undefined ? base : over;
102
+ }
103
+ const merged = { ...base };
104
+ for (const [key, value] of Object.entries(over)) {
105
+ merged[key] = mergeDeep(base[key], value);
106
+ }
107
+
108
+ return merged;
109
+ };
110
+
111
+ /** Настройки этого дерева: умолчания пакета, поверх них — то, что назвал проект. */
112
+ export const CONFIG = mergeDeep(DEFAULTS, readOverrides());
113
+
114
+ /** Путь к списку принятых долгов по имени проверки: `dupes` → `tools/dupes-allowlist.json`. */
115
+ export const allowlistOf = (name) => join(CONFIG.allowlistDir, `${name}-allowlist.json`);
116
+
117
+ /**
118
+ * Список долгов; нет файла — пустой. Заводить его руками не требуется: проверка, встреченная
119
+ * впервые, покажет всё найденное как новое, и это честнее, чем молчать из-за отсутствия файла.
120
+ */
121
+ export const readAllowlist = (name) => {
122
+ const path = join(ROOT, allowlistOf(name));
123
+
124
+ return existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) : {};
125
+ };
126
+
127
+ /** Есть ли в дереве то, без чего проверке нечего делать. Нет — она выходит с нулём и говорит это. */
128
+ export const skipUnless = (present, what) => {
129
+ if (present) {
130
+ return false;
131
+ }
132
+ console.log(`пропущено: в дереве нет ${what}`);
133
+ process.exit(0);
134
+ };
@@ -0,0 +1,198 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Заведение задачи, с которой начинается правка.
4
+ *
5
+ * Заведение состоит из четырёх шагов, и правка начинается только после всех
6
+ * четырёх: тикет, номер в его заголовке, добавление на борду, состояние
7
+ * «Backlog». Пока шаги переписывались руками из паттерна, промах на любом из
8
+ * них давал задачу, которой нет в очереди работ: две такие простояли месяц.
9
+ *
10
+ * Ветку команда не заводит — печатает готовую строку. Заведение ветки отдельным
11
+ * вызовом требует гард главной ветки: составную «создать ветку и сразу
12
+ * коммитить» он отклоняет целиком.
13
+ *
14
+ * npm run task:new -- --title 'Письма владельцу не уходят' --label bug --label area:api
15
+ * npm run task:new -- --title '…' --slug mail-owner-silence < описание.md
16
+ *
17
+ * Тело читается со стандартного ввода. Автор и исполнитель — учётная запись бота,
18
+ * та же, от которой идут коммиты; `--assignee` перекрывает исполнителя.
19
+ */
20
+ import { existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
21
+ import { dirname, join, resolve } from 'node:path';
22
+ import { fileURLToPath } from 'node:url';
23
+
24
+ import {
25
+ BACKLOG_OPTION_ID,
26
+ BOT,
27
+ IN_PROGRESS_STATUS,
28
+ OfflineError,
29
+ OWNER,
30
+ PROJECT_ID,
31
+ REPO,
32
+ STATUS_FIELD_ID,
33
+ TASK_KEY,
34
+ TOKEN_PATH,
35
+ botToken,
36
+ gh,
37
+ ghJson,
38
+ graphql,
39
+ numberFromTitle,
40
+ } from './board.mjs';
41
+
42
+ function parseArgs(argv) {
43
+ const args = { labels: [], assignee: BOT, title: null, slug: null };
44
+ for (let index = 0; index < argv.length; index += 1) {
45
+ const value = argv[index + 1];
46
+ switch (argv[index]) {
47
+ case '--title':
48
+ args.title = value;
49
+ index += 1;
50
+ break;
51
+ case '--label':
52
+ args.labels.push(value);
53
+ index += 1;
54
+ break;
55
+ case '--assignee':
56
+ args.assignee = value;
57
+ index += 1;
58
+ break;
59
+ case '--slug':
60
+ args.slug = value;
61
+ index += 1;
62
+ break;
63
+ default:
64
+ fail(`неизвестный ключ ${argv[index]}`);
65
+ }
66
+ }
67
+ return args;
68
+ }
69
+
70
+ function fail(message) {
71
+ console.error(`task-new: ${message}`);
72
+ process.exit(1);
73
+ }
74
+
75
+ function readBody() {
76
+ if (process.stdin.isTTY) {
77
+ return '';
78
+ }
79
+ try {
80
+ return readFileSync(0, 'utf8').trim();
81
+ } catch {
82
+ return '';
83
+ }
84
+ }
85
+
86
+ const args = parseArgs(process.argv.slice(2));
87
+ if (!args.title) {
88
+ fail("нужен --title '<Что не так>'");
89
+ }
90
+ if (numberFromTitle(args.title) !== null || args.title.startsWith(`[${TASK_KEY}-`)) {
91
+ fail('номер в заголовок не пишется руками: он известен только после заведения и дописывается сам');
92
+ }
93
+ if (args.slug !== null && !/^[a-z0-9][a-z0-9-]*$/.test(args.slug)) {
94
+ fail('slug — строчные латинские буквы, цифры и дефисы: имя ветки читают в списке из полусотни строк');
95
+ }
96
+
97
+ const token = botToken();
98
+ if (!token) {
99
+ fail(`нет токена бота (${TOKEN_PATH}): задача завелась бы от чужого имени`);
100
+ }
101
+
102
+ let number = null;
103
+ try {
104
+ const created = gh(
105
+ [
106
+ 'issue',
107
+ 'create',
108
+ '--repo',
109
+ `${OWNER}/${REPO}`,
110
+ '--title',
111
+ args.title,
112
+ '--body',
113
+ readBody(),
114
+ '--assignee',
115
+ args.assignee,
116
+ ...args.labels.flatMap((label) => ['--label', label]),
117
+ ],
118
+ { token }
119
+ ).trim();
120
+ number = Number(created.split('/').pop());
121
+ if (!Number.isInteger(number)) {
122
+ fail(`не разобрать номер заведённой задачи в ответе: ${created}`);
123
+ }
124
+
125
+ // Номер известен только теперь, поэтому заголовок дописывается вторым вызовом.
126
+ gh(['api', '-X', 'PATCH', `repos/${OWNER}/${REPO}/issues/${number}`, '-f', `title=[${TASK_KEY}-${number}] ${args.title}`], {
127
+ token,
128
+ });
129
+
130
+ const issueId = ghJson(['api', `repos/${OWNER}/${REPO}/issues/${number}`, '--jq', '{id:.node_id}'], { token }).id;
131
+ const itemId = graphql(
132
+ `mutation { addProjectV2ItemById(input: {projectId: "${PROJECT_ID}", contentId: "${issueId}"}) { item { id } } }`,
133
+ { token }
134
+ ).data.addProjectV2ItemById.item.id;
135
+ graphql(
136
+ `mutation { updateProjectV2ItemFieldValue(input: {projectId: "${PROJECT_ID}", itemId: "${itemId}", fieldId: "${STATUS_FIELD_ID}", value: {singleSelectOptionId: "${BACKLOG_OPTION_ID}"}}) { projectV2Item { id } } }`,
137
+ { token }
138
+ );
139
+ } catch (error) {
140
+ const reason = error instanceof OfflineError ? `нет связи с GitHub: ${error.message}` : String(error.message ?? error);
141
+ if (number === null) {
142
+ fail(reason);
143
+ }
144
+ // Тикет уже есть, а на борде его может не быть — ровно то состояние, из-за которого
145
+ // задачи и терялись. Молчать здесь нельзя: номер печатается, чтобы доделать руками.
146
+ console.error(`task-new: задача #${number} заведена, но доведена не до конца — ${reason}`);
147
+ console.error(`task-new: проверь и доправь — npm run check:board`);
148
+ process.exit(1);
149
+ }
150
+
151
+ const branch = args.slug ? `${TASK_KEY}-${number}-${args.slug}` : `${TASK_KEY}-${number}-<короткий-slug>`;
152
+
153
+ /**
154
+ * Папка задачи переименовывается здесь, потому что номер и имя ветки в этот момент известны
155
+ * единственный раз за всю работу. Разбор просьбы владельца идёт до заведения задачи — до
156
+ * конца разбора неизвестно даже, сколько задач из него выйдет, — и лежит в
157
+ * `docs/tasks/_draft-<slug>`. Оставленный черновиком, он остаётся вне истории, а следующий
158
+ * заход его не находит: хук запуска ищет папку по имени ветки.
159
+ */
160
+ function adoptDraft() {
161
+ if (!args.slug) {
162
+ console.log(`\nПапка задачи: --slug не задан, переименовать черновик нечем.`);
163
+ return;
164
+ }
165
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
166
+ const draft = join(root, 'docs/tasks', `_draft-${args.slug}`);
167
+ const target = join(root, 'docs/tasks', branch);
168
+
169
+ if (existsSync(target)) {
170
+ console.log(`\nПапка задачи уже на месте: docs/tasks/${branch}/`);
171
+ } else if (existsSync(draft)) {
172
+ renameSync(draft, target);
173
+ console.log(`\nПапка задачи: docs/tasks/_draft-${args.slug}/ → docs/tasks/${branch}/`);
174
+ } else {
175
+ console.log(`\nПапка задачи собирается с образца:\n cp -r docs/tasks/_template docs/tasks/${branch}`);
176
+ return;
177
+ }
178
+
179
+ // Шапку замысла читает гард: по ней он находит договорённость о продукте. Номер в ней
180
+ // проставляется здесь же — руками его переписывают из подсказки и ошибаются.
181
+ const plan = join(target, 'plan.md');
182
+ if (!existsSync(plan)) {
183
+ return;
184
+ }
185
+ const before = readFileSync(plan, 'utf8');
186
+ const after = before.replace(/^\*\*Задача:\*\*.*$/m, `**Задача:** ${TASK_KEY}-${number} · **Ветка:** ${branch}`);
187
+ if (after !== before) {
188
+ writeFileSync(plan, after);
189
+ console.log(`Шапка замысла проставлена: docs/tasks/${branch}/plan.md`);
190
+ }
191
+ }
192
+
193
+ adoptDraft();
194
+
195
+ console.log(`[${TASK_KEY}-${number}] ${args.title}`);
196
+ console.log(`https://github.com/${OWNER}/${REPO}/issues/${number}`);
197
+ console.log(`\nВетка заводится отдельным вызовом:\n git checkout -b ${branch}`);
198
+ console.log(`Взятая в работу задача переставляется на борде:\n npm run task:move -- ${number} ${IN_PROGRESS_STATUS}`);
@@ -0,0 +1,70 @@
1
+ ---
2
+ description: Разбор закрытой задачи глазами правил дерева — что грузилось, что помогло, чего не хватило
3
+ argument-hint: '[пусто | <акцент, на что смотреть в первую очередь>]'
4
+ ---
5
+
6
+ Запусти агента `skill-curator` на разбор только что закрытой задачи. Акцент от пользователя:
7
+ `$ARGUMENTS`
8
+
9
+ Вызывается **после того, как задача сделана и проверена**, до перехода к следующей. Агент ничего
10
+ не правит — приносит готовые формулировки, а решение вставлять их принимает пользователь.
11
+
12
+ ## 1. Найди список загруженного
13
+
14
+ Гейт правил пишет его по одному файлу на сессию:
15
+
16
+ ```bash
17
+ ls -t "${TMPDIR}claude-skill-gate/"*.loaded | head -5
18
+ ```
19
+
20
+ Нужен файл **текущей** сессии: её идентификатор стоит в пути скретчпада из системного
21
+ приглашения. Брать самый свежий по времени вслепую нельзя — параллельная сессия в другом окне
22
+ обгонит твою, и разбор уедет на чужую задачу.
23
+
24
+ Запись обнуляется при сжатии контекста, поэтому список покрывает последний отрезок сессии. Если
25
+ знаешь, что грузилось раньше, — допиши это в сводку словами, а не выдавай список за полный.
26
+
27
+ ## 2. Собери сводку
28
+
29
+ Без неё разбор выродится в пересказ правил: агент не видел ни задачи, ни того, где ты
30
+ спотыкался. В сводке:
31
+
32
+ - **что делали** — задача одной фразой, объём числами (файлов, коммитов, ветка);
33
+ - **что пошло не так** — по пунктам, и это главная часть. Каждый: что сломалось, чем ловилось
34
+ (или почему не поймалось ничем), нашёл ты сам или проверка;
35
+ - **что было под рукой** — какие правила грузились и что каждое дало: где сработало, где
36
+ промолчало, где увело не туда. Правило, которое загрузилось и не пригодилось, — такая же
37
+ находка, как недостающее;
38
+ - **грабли окружения** — правило линтера, ограничение инструмента, поведение стенда, на которые
39
+ наткнулись и которых нет ни в одном правиле.
40
+
41
+ Честность здесь дороже полноты: «нашёл разбор, а не я» и «пункт был помечен сделанным по работе,
42
+ которой не было» — ровно то, из чего получаются правила. Приглаженная сводка даёт приглаженный
43
+ разбор.
44
+
45
+ ## 3. Запусти агента
46
+
47
+ Инструментом `Agent`, `subagent_type: 'skill-curator'`. В промпт — путь к `.loaded` и сводку
48
+ целиком.
49
+
50
+ ## 4. Отдай результат
51
+
52
+ Покажи предложения агента **как есть**: он пишет готовый текст для вставки, и пересказ его
53
+ портит. По каждому скажи своё — согласен или нет и почему; правило, с которым ты не согласен,
54
+ вставлять не надо.
55
+
56
+ У каждого предложения агент ставит пометку «пакет», «компаньон» или «дерево»: тексты приезжают
57
+ из `@rt-tools/agent-kit`, и правка разложенного файла на месте теряется на следующем
58
+ `agent-kit sync`. Пометку оставляй в тексте предложения — решает по ней человек.
59
+
60
+ Куда что идёт:
61
+
62
+ - **пакет** — правка ресурса в `@rt-tools/agent-kit`; сюда же попадает всё, что верно любому
63
+ дереву мастерской;
64
+ - **компаньон** — `implementation.md` рядом с правилом: имена этого дерева и привязка статей;
65
+ - **дерево** — надстройка: `.claude/rt-kit/overrides/<идентификатор>` для текстов,
66
+ `.claude/rt-kit/gate-map.sh` и `project.sh` для карты и профиля.
67
+
68
+ Правило и его компаньон меняются вместе: расхождение между ними хуже отсутствия правила. Новое
69
+ правило требует ещё и записи в карте гейта, иначе его никто не загрузит, — а после правки карты
70
+ или хука обязателен прогон их сценариев целиком.
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env bash
2
+ # Карта «что правится — какое правило». Умолчание пакета: настоящие пути, а не образцы.
3
+ #
4
+ # Деревья этой мастерской устроены одинаково — Nx, `apps/` и `libs/`, те же расширения и те же
5
+ # имена каталогов, — поэтому карту везёт пакет, а не пишет каждый проект заново. Пятнадцать её
6
+ # редакций расходились бы молча, и заметить расхождение можно было бы только по тому, что гейт
7
+ # перестал требовать правило там, где оно есть.
8
+ #
9
+ # Своё дерево дописывает надстройкой — `.claude/rt-kit/gate-map.sh`. Она грузится второй,
10
+ # объявляет `skill_for` заново и зовёт отсюда `skill_for_default` для всего, чего не назвала.
11
+ #
12
+ # Функция печатает ИМЯ ПРАВИЛА или молчит. Молчание — «правила на это нет», и гейт пропускает.
13
+ # Имён может быть несколько, по одному в строке: первое — доменное правило файла, следующие —
14
+ # те, что действуют вторым слоем. Гейт потребует первое незагруженное.
15
+ #
16
+ # Порядок веток решает: первое совпадение выигрывает, поэтому частное идёт раньше общего.
17
+
18
+ # Правила, которые вступают не от рода файла, а от того, что в него пишут.
19
+ #
20
+ # Обращение к среде исполнения приходит в обычный сервис, а число-настройка и перечисление —
21
+ # в обычный класс: по имени файла ни то ни другое не видно, и правило, требуемое только по
22
+ # расширению, здесь молчало бы.
23
+ skill_for_written() {
24
+ target="$1"
25
+ written="$2"
26
+
27
+ [ -z "$written" ] && return 0
28
+
29
+ case "$target" in
30
+ *.spec.ts | */docs/* | *.md) return 0 ;;
31
+ esac
32
+
33
+ printf '%s' "$written" | grep -qE '(globalThis|window\.|document\.defaultView|PLATFORM_ID|isPlatformBrowser|localStorage|sessionStorage)' \
34
+ && printf '%s\n' 'platform-access'
35
+
36
+ case "$target" in
37
+ *.ts)
38
+ printf '%s' "$written" | grep -qE '^[[:space:]]*(export[[:space:]]+)?(const[[:space:]]+[A-Z][A-Z0-9_]*[[:space:]]*(:[^=]*)?=[[:space:]]*-?[0-9]|enum[[:space:]])' \
39
+ && printf '%s\n' 'shared-code'
40
+ ;;
41
+ esac
42
+
43
+ return 0
44
+ }
45
+
46
+ skill_for_default() {
47
+ kind="$1"
48
+ target="$2"
49
+ written="$3"
50
+
51
+ case "$kind" in
52
+ edit)
53
+ case "$target" in
54
+ # Файлы самого агента правятся без правила: правило на них — это оно само.
55
+ */.claude/skills/* | */.claude/agents/* | */.claude/commands/* | */.claude/workflows/*) return 0 ;;
56
+
57
+ # Тексты проекта. Спек держит устройство домена, закон — договорённость,
58
+ # и оба правятся не так, как правится код.
59
+ */docs/specs/*) printf '%s\n' 'spec-driven' ;;
60
+ */docs/tasks/*) printf '%s\n' 'task-flow' ;;
61
+ */docs/constitution/*) printf '%s\n' 'spec-driven' ;;
62
+ *.md) printf '%s\n' 'doc-style' ;;
63
+
64
+ # Поставка: состав зависимостей — это то, что приезжает на прод.
65
+ */package.json | */pnpm-lock.yaml | */pnpm-workspace.yaml | */package-lock.json) printf '%s\n' 'dependencies' ;;
66
+
67
+ # Границы между либами: манифест, алиасы, барель.
68
+ */project.json | */tsconfig.base.json | */eslint/boundaries/* | */src/index.ts | */public-api.ts | */ng-package.json)
69
+ printf '%s\n' 'lib-layers' ;;
70
+
71
+ *.spec.ts) printf '%s\n' 'testing' ;;
72
+ *.component.ts | *.component.html) printf '%s\n' 'component-structure' ;;
73
+ *.scss) printf '%s\n' 'styling-bem' ;;
74
+
75
+ # Классы каркаса: состояние, потоки и место подписки. Бэкенд сюда не идёт —
76
+ # ни компонентов, ни подписок в шаблоне у него нет.
77
+ */libs/api/* | */apps/api/*) printf '%s\n' 'typescript-conventions' ;;
78
+ *.store.ts | *.service.ts | *.directive.ts | *.pipe.ts | *.guard.ts | *.interceptor.ts) printf '%s\n' 'angular-patterns' ;;
79
+
80
+ *.ts) printf '%s\n' 'typescript-conventions' ;;
81
+ esac
82
+ skill_for_written "$target" "$written"
83
+ ;;
84
+ bash)
85
+ case "$target" in
86
+ *git\ commit* | *git\ push* | *git\ merge* | *git\ rebase* | *git\ cherry-pick* | *gh\ pr\ * | *glab\ mr\ * | *az\ repos\ *)
87
+ printf '%s\n' 'git-workflow' ;;
88
+ *prisma\ migrate* | *prisma\ db\ *) printf '%s\n' 'git-workflow' ;;
89
+ *curl\ *localhost* | *wget\ *localhost*) printf '%s\n' 'browser-verification' ;;
90
+ esac
91
+ ;;
92
+ esac
93
+
94
+ return 0
95
+ }
96
+
97
+ # Без надстройки проекта карта — это умолчание. С надстройкой она объявит `skill_for` заново.
98
+ skill_for() {
99
+ skill_for_default "$@"
100
+ }