@rt-tools/agent-kit 0.4.0 → 0.5.1

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 (52) hide show
  1. package/README.md +5 -0
  2. package/assets/checks/board.github.mjs +45 -2
  3. package/assets/checks/check-board.github.mjs +7 -14
  4. package/assets/checks/check-doc-paths.mjs +200 -30
  5. package/assets/checks/check-specs.mjs +131 -17
  6. package/assets/checks/rt-kit-checks.config.mjs +12 -0
  7. package/assets/commands/next-session.md +122 -0
  8. package/assets/defaults/gate-map.sh +29 -3
  9. package/assets/defaults/project.sh +50 -0
  10. package/assets/docs/GLOSSARY.md +74 -0
  11. package/assets/hooks/git-guard-delivery.sh +87 -4
  12. package/assets/hooks/grill-gate.sh +96 -0
  13. package/assets/hooks/task-flow-guard.sh +14 -3
  14. package/assets/hooks/window-fill-guard.sh +150 -0
  15. package/assets/laws/code-structure.md +10 -0
  16. package/assets/laws/delivery.md +8 -1
  17. package/assets/laws/project-documentation.md +14 -0
  18. package/assets/laws/verifiability.md +13 -0
  19. package/assets/laws/work-conduct.md +19 -0
  20. package/assets/patterns/git-workflow-commit.github.md +4 -0
  21. package/assets/patterns/spec-driven-domain.md +35 -0
  22. package/assets/patterns/task-flow-close.md +72 -8
  23. package/assets/patterns/task-flow-handoff.md +115 -0
  24. package/assets/patterns/task-flow-resume.md +2 -2
  25. package/assets/patterns/task-flow-start.md +18 -2
  26. package/assets/rules/angular-patterns.md +4 -0
  27. package/assets/rules/browser-verification.md +4 -3
  28. package/assets/rules/doc-style.md +53 -1
  29. package/assets/rules/git-workflow.azure.md +24 -0
  30. package/assets/rules/git-workflow.github.md +23 -0
  31. package/assets/rules/git-workflow.gitlab.md +23 -0
  32. package/assets/rules/spec-driven.md +19 -1
  33. package/assets/rules/task-flow.md +88 -2
  34. package/assets/skills/agent-kit.md +4 -0
  35. package/assets/templates/rule.md +1 -1
  36. package/lib/commands.d.ts.map +1 -1
  37. package/lib/commands.js +2 -1
  38. package/lib/commands.js.map +1 -1
  39. package/lib/config.d.ts +3 -1
  40. package/lib/config.d.ts.map +1 -1
  41. package/lib/config.js +2 -0
  42. package/lib/config.js.map +1 -1
  43. package/lib/hooks-map.d.ts +20 -5
  44. package/lib/hooks-map.d.ts.map +1 -1
  45. package/lib/hooks-map.js +56 -15
  46. package/lib/hooks-map.js.map +1 -1
  47. package/lib/sync.d.ts.map +1 -1
  48. package/lib/sync.js +2 -5
  49. package/lib/sync.js.map +1 -1
  50. package/package.json +1 -1
  51. package/rt-tools-agent-kit-0.5.1.tgz +0 -0
  52. package/rt-tools-agent-kit-0.4.0.tgz +0 -0
@@ -62,14 +62,20 @@ const TEST_ROOTS = CONFIG.sourceRoots;
62
62
  const SOURCE_ROOTS = [...CONFIG.sourceRoots, ...(CONFIG.schemaFile ? [CONFIG.schemaFile.split('/')[0]] : [])];
63
63
  const SKIPPED_DIRS = CONFIG.skippedDirs;
64
64
 
65
- /** `### SC-BK-03 — заявка на занятые даты` */
66
- const SCENARIO_HEADING = /^###\s+(SC-([A-Z]{2,4})-(\d{2,3}))\s+—\s+(.+?)\s*$/;
65
+ /**
66
+ * `### SC-BK-03 — заявка на занятые даты`
67
+ *
68
+ * Номер принимается от одной цифры до трёх. Заголовок, не подошедший под шаблон, сценария не
69
+ * заводит и отказа не даёт: дерево, пронумеровавшее сценарии с единицы, теряло бы первые
70
+ * девять из них молча — ни в покрытии, ни в долгах, при зелёной сверке.
71
+ */
72
+ const SCENARIO_HEADING = /^###\s+(SC-([A-Z]{2,4})-(\d{1,3}))\s+—\s+(.+?)\s*$/;
67
73
  /** Отметка осознанно непокрытого сценария; причина обязательна */
68
74
  const UNCOVERED = /^Не покрыто:\s*\S/;
69
75
  /** Тест есть, но проверяет не всё обещанное или идёт другим путём */
70
76
  const PARTIAL = /^Покрытие:\s*частичное\s*—\s*\S/;
71
- /** Упоминание сценария в заголовке теста */
72
- const SCENARIO_REFERENCE = /\bSC-[A-Z]{2,4}-\d{2,3}\b/g;
77
+ /** Упоминание сценария в заголовке теста; номер той же длины, что и в заголовке сценария */
78
+ const SCENARIO_REFERENCE = /\bSC-[A-Z]{2,4}-\d{1,3}\b/g;
73
79
  /** Строка обещания сценария; её продолжения идут с отступом */
74
80
  const PROMISE = /^Тогда\s+\S/;
75
81
  /**
@@ -229,7 +235,34 @@ function ruleHeadOf(bulletText) {
229
235
  * Одно слово в двух смыслах развели именно здесь: «правило» — слой между законом и скилом,
230
236
  * а внутри закона живут статьи.
231
237
  */
232
- function checkRuleImplementation(specFile, text, mapFile, heading = '## Правила') {
238
+ /**
239
+ * Строки таблицы привязок компаньона.
240
+ *
241
+ * Компаньон правила держит три таблицы: чем вещи правила названы в этом дереве, где лежат
242
+ * механизмы и где исполняется каждая статья. Привязки — только третья, и берётся она по имени
243
+ * раздела, а не по месту в файле. Пока читался весь файл, строки первых двух попадали в список
244
+ * наравне с настоящими и тут же объявлялись расхождением: статьи с таким текстом в правиле нет
245
+ * и быть не может. Две трети перечня в дереве были ими, и правильно дописанная строка «Где это
246
+ * лежит» отвечала отказом.
247
+ *
248
+ * У компаньона спека домена раздела нет: там таблица одна, и сужать нечего — такой зовёт без
249
+ * имени раздела. У правила раздел стоит в образце компаньона, поэтому его отсутствие — отказ:
250
+ * молча прочесть вместо него весь файл значило бы вернуть тот же дефект.
251
+ */
252
+ function rowsOfMap(specFile, mapFile, mapHeading) {
253
+ const text = read(mapFile);
254
+ if (!mapHeading) {
255
+ return text.split('\n');
256
+ }
257
+ const section = sectionOf(text, mapHeading);
258
+ if (!section.length) {
259
+ report(mapFile, `нет раздела \`${mapHeading}\` — привязкам правила негде лежать`);
260
+ }
261
+
262
+ return section;
263
+ }
264
+
265
+ function checkRuleImplementation(specFile, text, mapFile, heading = '## Правила', mapHeading = '') {
233
266
  const bullets = bulletsOf(sectionOf(text, heading));
234
267
  if (!bullets.length) {
235
268
  report(specFile, `в разделе \`${heading}\` нет ни одного пункта`);
@@ -244,7 +277,7 @@ function checkRuleImplementation(specFile, text, mapFile, heading = '## Прав
244
277
  }
245
278
 
246
279
  const rows = new Map();
247
- for (const line of read(mapFile).split('\n')) {
280
+ for (const line of rowsOfMap(specFile, mapFile, mapHeading)) {
248
281
  const cells = line.match(/^\|([^|]+)\|([^|]*)\|\s*$/);
249
282
  if (!cells) {
250
283
  continue;
@@ -730,6 +763,20 @@ function collectReferences() {
730
763
  // идентификатором: состояние теста читается, когда файл разобран целиком
731
764
  found.forEach(({ id, test: own, place }) => remember(id, { place, screen: Boolean(e2eRoot), off: Boolean(own?.off) }));
732
765
  }
766
+
767
+ // Наборы сценариев на shell. Так проверяются исполняемые файлы — гарды, проверки,
768
+ // умолчания: они не на TypeScript, и набор к ним пишут на том же языке, что и их
769
+ // самих. Выключателей здесь нет: пропустить сценарий в таком наборе нечем, поэтому
770
+ // достаточно найти идентификатор.
771
+ for (const file of walk(root, (name) => name.endsWith('.test.sh'))) {
772
+ read(file)
773
+ .split('\n')
774
+ .forEach((line, index) => {
775
+ for (const [id] of line.matchAll(SCENARIO_REFERENCE)) {
776
+ remember(id, { place: `${file}:${index + 1}`, screen: false, off: false });
777
+ }
778
+ });
779
+ }
733
780
  }
734
781
 
735
782
  return references;
@@ -774,15 +821,47 @@ for (const file of walk(CONSTITUTION_DIR, (name) => name.endsWith('.md'))) {
774
821
  laws.set(name, file);
775
822
  }
776
823
 
824
+ /**
825
+ * Директории, описывающие предмет: сам домен и его поддомены. Поддомен заводится, когда домен
826
+ * вырос настолько, что читать его целиком ради одной подробности дороже, чем найти её; устроен
827
+ * он так же — три файла и свой префикс сценариев.
828
+ *
829
+ * `proposed/` предметом не является: это договорённость о продукте до кода, и её сценарии живут
830
+ * в нумерации того спека, в который она вольётся.
831
+ */
832
+ function collectSpecDirs(base) {
833
+ const found = [base];
834
+ let entries;
835
+ try {
836
+ entries = readdirSync(join(ROOT, base), { withFileTypes: true });
837
+ } catch {
838
+ return found;
839
+ }
840
+
841
+ for (const entry of entries) {
842
+ if (entry.isDirectory() && entry.name !== 'proposed') {
843
+ found.push(...collectSpecDirs(`${base}/${entry.name}`));
844
+ }
845
+ }
846
+
847
+ return found;
848
+ }
849
+
850
+ /** Префикс сценариев принадлежит одному спеку по всему дереву. */
851
+ const prefixOwners = new Map();
852
+
777
853
  for (const domain of domains) {
778
854
  const base = `${SPECS_DIR}/${domain}`;
779
- // Домен, у которого есть только `proposed/`, ещё не существует: спека о нём
780
- // нет, пока фича не выкачена
781
- const isProposedOnly = exists(`${base}/proposed`) && !exists(`${base}/spec.md`);
782
- if (!isProposedOnly) {
855
+ for (const dir of collectSpecDirs(base)) {
856
+ // Спек, у которого есть только `proposed/`, ещё не существует: его самого нет, пока
857
+ // фича не выкачена
858
+ if (exists(`${dir}/proposed`) && !exists(`${dir}/spec.md`)) {
859
+ continue;
860
+ }
861
+ const what = dir === base ? 'домен' : 'поддомен';
783
862
  ['spec.md', 'scenarios.md']
784
- .filter((name) => !exists(`${base}/${name}`))
785
- .forEach((name) => report(base, `нет файла \`${name}\` — домен описан наполовину`));
863
+ .filter((name) => !exists(`${dir}/${name}`))
864
+ .forEach((name) => report(dir, `нет файла \`${name}\` — ${what} описан наполовину`));
786
865
  }
787
866
 
788
867
  // Спеки фич из `proposed/` проверяются наравне со спеком домена: они и есть
@@ -803,10 +882,38 @@ for (const domain of domains) {
803
882
  }
804
883
 
805
884
  const found = walk(base, (name) => name === 'scenarios.md').flatMap(parseScenarios);
806
- const prefixes = new Set(found.map((scenario) => scenario.prefix));
807
- if (prefixes.size > 1) {
808
- report(base, домене больше одного префикса сценариев: ${[...prefixes].sort().join(', ')}`);
885
+
886
+ // Префикс судится в пределах одного спека, а не всего дерева домена: у поддомена он свой, и
887
+ // по номеру видно, о чём сценарий. Два префикса в одном спеке по-прежнему означают, что
888
+ // предмет описан дважды.
889
+ const prefixesOf = new Map();
890
+ for (const scenario of found) {
891
+ const dir = dirname(scenario.file);
892
+ if (!prefixesOf.has(dir)) {
893
+ prefixesOf.set(dir, new Set());
894
+ }
895
+ prefixesOf.get(dir).add(scenario.prefix);
896
+ }
897
+
898
+ for (const [dir, prefixes] of prefixesOf) {
899
+ if (prefixes.size > 1) {
900
+ report(dir, `в спеке больше одного префикса сценариев: ${[...prefixes].sort().join(', ')}`);
901
+ }
902
+ // Договорённость о продукте нумеруется вместе со спеком, в который вольётся:
903
+ // идентификаторы переезд переживают, и занятым префикс от неё не становится
904
+ if (dir.includes('/proposed/')) {
905
+ continue;
906
+ }
907
+ for (const prefix of prefixes) {
908
+ const owner = prefixOwners.get(prefix);
909
+ if (owner && owner !== dir) {
910
+ report(dir, `префикс \`SC-${prefix}\` уже занят — \`${owner}\`; по номеру не видно, чей сценарий`);
911
+ continue;
912
+ }
913
+ prefixOwners.set(prefix, dir);
914
+ }
809
915
  }
916
+
810
917
  scenarios.push(...found);
811
918
  }
812
919
 
@@ -833,6 +940,8 @@ for (const file of walk(CONSTITUTION_DIR, (name) => name.endsWith('.md'))) {
833
940
  // Правило — скил с `kind: rule` в шапке. Оно и знает о проекте: имена, пути, связи. Привязка
834
941
  // его утверждений к коду живёт в `implementation.md` рядом со скилом.
835
942
  const RULE_HEADING = '## Как закон применяется здесь';
943
+ /** Раздел компаньона правила, где лежат привязки; остальные его таблицы называют имена дерева. */
944
+ const MAP_HEADING = '## Где исполняются статьи';
836
945
 
837
946
  /**
838
947
  * Шапка скила — первый блок между `---`. Читается только она: паттерн, который учит заводить
@@ -879,7 +988,7 @@ for (const file of walk('.claude/skills', (name) => name === 'SKILL.md')) {
879
988
  } else {
880
989
  ruled.add(law);
881
990
  }
882
- checkRuleImplementation(file, text, `${dirname(file)}/implementation.md`, RULE_HEADING);
991
+ checkRuleImplementation(file, text, `${dirname(file)}/implementation.md`, RULE_HEADING, MAP_HEADING);
883
992
 
884
993
  const name = nameOf(head);
885
994
  if (name && name !== file.slice('.claude/skills/'.length, -'/SKILL.md'.length)) {
@@ -887,11 +996,16 @@ for (const file of walk('.claude/skills', (name) => name === 'SKILL.md')) {
887
996
  }
888
997
  }
889
998
 
999
+ // Предложенный закон правила не требует: договорённость записана раньше кода, привязывать её
1000
+ // не к чему, и требование правила заставило бы завести его с якорями в несуществующие места.
1001
+ // Признак стоит строкой статуса в самом законе, а не списком исключений рядом с проверкой.
1002
+ const isProposedLaw = (file) => /^\*\*Статус:\*\*\s*предложен/m.test(read(file));
1003
+
890
1004
  // Обратные стороны связи. Закон без правила читается как договорённость, которую этот проект
891
1005
  // не применяет; правило без паттерна оставляет готовый код там, где ему не место, — в самом
892
1006
  // правиле, которое читается при каждой правке.
893
1007
  [...laws]
894
- .filter(([law]) => !ruled.has(law))
1008
+ .filter(([law, file]) => !ruled.has(law) && !isProposedLaw(file))
895
1009
  .forEach(([, file]) => report(file, 'у закона нет ни одного правила — заведи скил с `law:` на него'));
896
1010
 
897
1011
  for (const file of walk('.claude/skills', (name) => name === 'SKILL.md')) {
@@ -36,6 +36,18 @@ const DEFAULTS = {
36
36
  docsDir: 'docs',
37
37
  /** Отложенное: про него проверки молчат — оно описывает прошлое, а не дерево. */
38
38
  archiveDir: 'docs/archive/',
39
+ /**
40
+ * Каталоги, чей указатель сверяется с содержимым: обзорный документ в них перечисляет
41
+ * записи таблицей, и читатель ищет по ней, а не обходом. Пусто — сверки указателя нет.
42
+ */
43
+ indexedDirs: ['docs/archive/'],
44
+ /**
45
+ * Каталоги, где лежат исходники переносимых текстов. Такой текст называет адреса того
46
+ * дерева, куда он ложится, а не того, где написан, — и сверять его с этим деревом значит
47
+ * краснеть на каждый пример. Разложенную копию проверка узнаёт по шапке сама; сюда
48
+ * вносится только исходник. Пусто — переносимых текстов дерево не держит.
49
+ */
50
+ portableDirs: [],
39
51
  /** Где лежат спеки доменов; пусто — их в дереве нет, и сверка спеков не запускается. */
40
52
  specsDir: 'docs/specs',
41
53
  tasksDir: 'docs/tasks',
@@ -0,0 +1,122 @@
1
+ ---
2
+ description: Закрытие захода — главная ветка подтянута, влитые ветки сняты, передача написана
3
+ argument-hint: '[пусто | <что дописать в передачу от себя>]'
4
+ ---
5
+
6
+ Закрой заход: приведи дерево к главной ветке, убери влитые ветки и напиши передачу для
7
+ следующего захода. Дописка владельца к передаче: `$ARGUMENTS`
8
+
9
+ Вызывается **последним действием захода** — после того, как работа закоммичена, а отчёт открыт
10
+ или влит. Команда ничего не мержит, не пушит и не открывает: закрытие захода — уборка, а не
11
+ поставка.
12
+
13
+ ## 1. Прочитай профиль дерева
14
+
15
+ Имя главной ветки, каталог папок задач и каталог передачи у каждого дерева свои:
16
+
17
+ ```bash
18
+ for profile in .claude/rt-kit/defaults/project.sh .claude/rt-kit/project.sh; do
19
+ [ -f "$profile" ] && . "$profile"
20
+ done
21
+ printf 'главная: %s · задачи: %s · передача: %s\n' \
22
+ "${RT_MAIN_BRANCH:-main}" "${RT_TASKS_DIR:-docs/tasks}" "${RT_HANDOFF_DIR:-.claude/handoff}"
23
+ ```
24
+
25
+ Зашивать эти имена в команду нельзя: в первом же дереве, которое зовёт главную ветку иначе,
26
+ уборка уедет не туда.
27
+
28
+ ## 2. Остановись, если в дереве есть незакоммиченное
29
+
30
+ ```bash
31
+ git status --short
32
+ ```
33
+
34
+ Непустой вывод — конец команды. Назови файлы владельцу и не трогай ни веток, ни главной: смена
35
+ ветки уносит правку за собой или отбивается на полпути, а решает, что с ней делать, владелец.
36
+
37
+ Неотслеживаемый файл — тоже незакоммиченное. Скажи о нём отдельной строкой: он мог остаться от
38
+ работы, которую бросили.
39
+
40
+ ## 3. Пойми, по правилу ли ведётся работа
41
+
42
+ ```bash
43
+ git fetch --prune --quiet
44
+ branch="$(git branch --show-current)"
45
+ ```
46
+
47
+ Работа идёт **по правилу**, если имя ветки несёт номер задачи — это `rt_task_branch_ok` из
48
+ профиля — или если в каталоге папок задач лежит папка с именем ветки. Отчёт **влит**, когда
49
+ коммиты ветки уже есть в удалённой главной:
50
+
51
+ ```bash
52
+ git merge-base --is-ancestor HEAD "origin/${RT_MAIN_BRANCH:-main}" && echo влит || echo 'не влит'
53
+ ```
54
+
55
+ ## 4. Приведи дерево к главной ветке
56
+
57
+ - **Работа по правилу и отчёт влит** — задача закрыта, ветка больше не нужна:
58
+
59
+ ```bash
60
+ git switch "${RT_MAIN_BRANCH:-main}" && git pull --ff-only
61
+ ```
62
+
63
+ - **Всё остальное** — работа не кончилась, и ветка остаётся местом, где она продолжится:
64
+
65
+ ```bash
66
+ git merge "origin/${RT_MAIN_BRANCH:-main}"
67
+ ```
68
+
69
+ Конфликт разбирается сейчас, а не в начале следующего захода: назови его владельцу и
70
+ останови команду до его решения.
71
+
72
+ ## 5. Убери ветки
73
+
74
+ Снимаются только влитые в главную: их коммиты есть в ней, и восстанавливать нечего.
75
+
76
+ ```bash
77
+ git branch --merged "${RT_MAIN_BRANCH:-main}" \
78
+ | grep -vE "^\*|^\s*${RT_MAIN_BRANCH:-main}$" \
79
+ | xargs -r git branch -d
80
+ ```
81
+
82
+ Невлитую ветку **не сноси**. Назови её владельцу вместе с числом коммитов, которых нет в
83
+ главной, — по ним видно, что именно потеряется, если её снести:
84
+
85
+ ```bash
86
+ for b in $(git branch --no-merged "${RT_MAIN_BRANCH:-main}" --format='%(refname:short)'); do
87
+ printf '%s: %s коммитов мимо главной\n' "$b" "$(git rev-list --count "${RT_MAIN_BRANCH:-main}..$b")"
88
+ done
89
+ ```
90
+
91
+ Мёртвые ссылки на удалённые ветки снял `git fetch --prune` шагом 3.
92
+
93
+ ## 6. Напиши передачу
94
+
95
+ Что в ней стоит и в какой форме — паттерн `task-flow-handoff`; здесь только место и порядок.
96
+ Файл один на ветку и лежит вне истории дерева:
97
+
98
+ ```bash
99
+ mkdir -p "${RT_HANDOFF_DIR:-.claude/handoff}"
100
+ # файл — ${RT_HANDOFF_DIR:-.claude/handoff}/<ветка>.md
101
+ ```
102
+
103
+ Имя берётся от той ветки, в которой шла работа, — не от той, куда команда перешла шагом 4.
104
+
105
+ К тому, что требует паттерн, эта команда добавляет своё: что она убрала — снятые ветки,
106
+ состояние главной, оставшееся невлитым. Следующий заход начинается ровно с этого.
107
+
108
+ Заход, кончившийся ничем, передачу пишет тоже: «пробовали так — не вышло, потому что» стоит
109
+ дороже пустого файла. Дописку владельца из `$ARGUMENTS` вставь своим разделом, не пересказывая.
110
+
111
+ ## 7. Отдай итог
112
+
113
+ Последней строкой — путь к передаче: владелец вставляет её в новый заход одной вставкой. Перед
114
+ ней: что стало с главной веткой, какие ветки сняты, какие остались невлитыми. Содержание
115
+ передачи не пересказывай — владелец её и так прочитает.
116
+
117
+ ## Чего команда не делает
118
+
119
+ - не мержит отчёт и не пушит: это поставка, и вслепую она не делается;
120
+ - не сносит невлитую ветку и не трогает папку задачи;
121
+ - не коммитит передачу — она лежит вне дерева намеренно, иначе рядом с ходом работы заводится
122
+ вторая запись об одном и том же.
@@ -51,7 +51,15 @@ skill_for_default() {
51
51
  case "$kind" in
52
52
  edit)
53
53
  case "$target" in
54
- # Файлы самого агента правятся без правила: правило на них это оно само.
54
+ # Правило и паттерн такая же договорённость, как спек: обязательные разделы,
55
+ # утверждение с привязкой, граница между статьёй закона и утверждением правила.
56
+ # Ветка стоит раньше общего исключения и раньше `*.md`: под исключением текст
57
+ # правила переписывался без единого требования, а `*.md` увёл бы его в правило
58
+ # формулировок — оно про слова, не про устройство.
59
+ */.claude/skills/*.md) printf '%s\n' 'spec-driven' ;;
60
+
61
+ # Остальные файлы самого агента правятся без правила: правило на них — это оно
62
+ # само.
55
63
  */.claude/skills/* | */.claude/agents/* | */.claude/commands/* | */.claude/workflows/*) return 0 ;;
56
64
 
57
65
  # Тексты проекта. Спек держит устройство домена, закон — договорённость,
@@ -61,8 +69,26 @@ skill_for_default() {
61
69
  */docs/constitution/*) printf '%s\n' 'spec-driven' ;;
62
70
  *.md) printf '%s\n' 'doc-style' ;;
63
71
 
64
- # Поставка: состав зависимостей это то, что приезжает на прод.
65
- */package.json | */pnpm-lock.yaml | */pnpm-workspace.yaml | */package-lock.json) printf '%s\n' 'dependencies' ;;
72
+ # Конфиги линтеровто же самое, только запреты в них исполняемые: они и есть
73
+ # исполнение правил про типы и про оформление, а комментарии в них пересказывают
74
+ # эти правила поимённо. Правились без единого правила под рукой.
75
+ */eslint.config.mjs | */eslint.config.js) printf '%s\n' 'typescript-conventions' ;;
76
+ */stylelint.config.js | */stylelint.config.mjs) printf '%s\n' 'styling-bem' ;;
77
+
78
+ # Проверка повторов исполняет утверждения правила об общем коде и требуется
79
+ # только им: правило о раскладке либ говорит про неё одной строкой с отсылкой,
80
+ # а привязки её признаков стоят при общем коде. Два отказа подряд на правку двух
81
+ # строк комментария стоят захода, а второе прочитанное правило не пригождается.
82
+ */tools/check-dupes.mjs | */tools/dupes-allowlist.json) printf '%s\n' 'shared-code' ;;
83
+
84
+ # Поставка: состав зависимостей — это то, что приезжает на прод. Правка
85
+ # скриптов зависимостью не является, и правило про версии на неё не вступает.
86
+ # Оговорка: удаление зависимости приходит правкой без номера версии и сюда не
87
+ # попадает — его ловит снимок дерева, который правится тем же коммитом.
88
+ */package.json)
89
+ printf '%s' "$written" | grep -qE '"(dependencies|devDependencies|peerDependencies|optionalDependencies|overrides|resolutions|packageManager)"|"[^"]+"[[:space:]]*:[[:space:]]*"[~^]?[0-9]+\.[0-9]+' \
90
+ && printf '%s\n' 'dependencies' ;;
91
+ */pnpm-lock.yaml | */pnpm-workspace.yaml | */package-lock.json) printf '%s\n' 'dependencies' ;;
66
92
 
67
93
  # Границы между либами: манифест, алиасы, барель.
68
94
  */project.json | */tsconfig.base.json | */eslint/boundaries/* | */src/index.ts | */public-api.ts | */ng-package.json)
@@ -72,6 +72,54 @@ rt_lint_for_default() {
72
72
  # Каталог папок задач. Пусто — ведения работы папкой в дереве нет, и гард замысла молчит.
73
73
  RT_TASKS_DIR="${RT_TASKS_DIR:-docs/tasks}"
74
74
 
75
+ # Каталог записей о законченных работах. Туда переносят то, что объясняет решения закрытой
76
+ # задачи. Гард поставки требует, чтобы ветка, удалившая папку задачи, что-то сюда добавила:
77
+ # удалить проще, чем разобрать, а слова владельца больше нигде не записаны.
78
+ RT_ARCHIVE_DIR="${RT_ARCHIVE_DIR:-docs/archive}"
79
+
80
+ # Размер окна захода в токенах и пороги стража. Пусто — стража нет: считать долю не от чего, а
81
+ # выведенный из записи захода размер врал бы — модель записана там без пометки о расширенном
82
+ # окне. Дерево задаёт его в настройке агента, переменной окружения того же имени.
83
+ RT_WINDOW_TOKENS="${RT_WINDOW_TOKENS:-}"
84
+ RT_WINDOW_WARN_PCT="${RT_WINDOW_WARN_PCT:-40}"
85
+ RT_WINDOW_STOP_PCT="${RT_WINDOW_STOP_PCT:-50}"
86
+
87
+ # Куда кладётся передача захода. Вне дерева: состояние работы живёт в ходе работы и коммитится,
88
+ # а передача его пересказывает для вставки в новый заход и в историю не едет.
89
+ RT_HANDOFF_DIR="${RT_HANDOFF_DIR:-.claude/handoff}"
90
+
91
+ # Команды, которые проходят после порога остановки: ими заход закрывается. Отбить их значило бы
92
+ # отобрать у него единственный способ закончиться. Вызов считается по началу строки или сразу за
93
+ # разделителем — упоминание команды в тексте командой не является.
94
+ rt_handoff_allowed_cmd_default() {
95
+ case "$1" in
96
+ git\ * | *[\;\&\|]\ *git\ * | *\$\(git\ *) return 0 ;;
97
+ gh\ * | */gh\ * | glab\ * | */glab\ * | az\ * | */az\ *) return 0 ;;
98
+ *task:move* | *check:* | mkdir\ -p\ * | cat\ * | ls\ *) return 0 ;;
99
+ esac
100
+
101
+ return 1
102
+ }
103
+
104
+ # Где лежат тексты, которые читают до вопроса владельцу: законы, правила и договорённости о
105
+ # продукте. По ним гард разговора судит, читалось ли за ход хоть что-то, и их же называет в
106
+ # подсказке. Пусто у законов и правил разом — дерево этого требования не получает: читать
107
+ # нечего.
108
+ RT_LAWS_DIR="${RT_LAWS_DIR:-docs/constitution}"
109
+ RT_RULES_DIR="${RT_RULES_DIR:-.claude/skills}"
110
+ RT_SPECS_DIR="${RT_SPECS_DIR:-docs/specs}"
111
+
112
+ # Главная ветка. Гарду она нужна, чтобы найти общего предка и понять, что ветка сделала с
113
+ # папкой задачи и с архивом. Если общего предка нет, сравнивать не с чем — проверка молчит.
114
+ RT_MAIN_BRANCH="${RT_MAIN_BRANCH:-main}"
115
+
116
+ # Тело PR по его номеру. Обход требования пишут в PR, а в команде слияния его нет — там только
117
+ # номер. Пустой ответ значит «спросить не у кого»: тогда обход ищется только в тексте команды.
118
+ rt_report_body_default() {
119
+ command -v gh >/dev/null 2>&1 || return 1
120
+ gh pr view "$1" --json body -q '.body' 2>/dev/null
121
+ }
122
+
75
123
  # Код приложения ли это. Успех — да, и тогда правка требует замысла на диске.
76
124
  #
77
125
  # Признак — путь, а не оценка на глаз: оценку назначает тот, кому она мешает, и порог плывёт.
@@ -177,3 +225,5 @@ rt_reinvented_in() { rt_reinvented_in_default "$@"; }
177
225
  rt_is_app_code() { rt_is_app_code_default "$@"; }
178
226
  rt_qa_decorative() { rt_qa_decorative_default "$@"; }
179
227
  rt_task_state() { rt_task_state_default "$@"; }
228
+ rt_report_body() { rt_report_body_default "$@"; }
229
+ rt_handoff_allowed_cmd() { rt_handoff_allowed_cmd_default "$@"; }
@@ -0,0 +1,74 @@
1
+ # Словарь проекта
2
+
3
+ Слова, которые в этом дереве значат что-то определённое. Читается перед тем, как написать спек,
4
+ правило, комментарий, тело коммита или описание отчёта: слово отсюда употребляется в том
5
+ значении, что здесь, а слово не отсюда либо заводится здесь же, либо заменяется простым.
6
+
7
+ Термины одного домена живут в разделе «Терминология» его спека — здесь только те, что проходят
8
+ сквозь весь проект.
9
+
10
+ Разделы ниже везёт пакет: это слова слоя правил, и значат они одно и то же везде, где он стоит.
11
+ Предметные слова дерево дописывает своими разделами через надстройку — они сливаются сюда по
12
+ заголовкам, и правка пакета их не трогает.
13
+
14
+ ## Слой правил
15
+
16
+ | Термин | Что это |
17
+ | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
18
+ | Закон | Файл в каталоге конституции. Говорит, что должно быть верно, и не знает ни путей, ни имён файлов. Верен для любого приложения этого класса |
19
+ | Законы приложения | Слой законов, верных только для этого приложения: деньги, локали, доступ. Предметность в них законна — она их предмет |
20
+ | Правило | Скил с `kind: rule`. Привязывает закон к этому дереву: чем это здесь названо и где лежит |
21
+ | Паттерн | Скил с `kind: pattern`. Готовый код и порядок действий; стоит при правиле |
22
+ | Компаньон | Файл `implementation.md` рядом с правилом: имена и пути этого дерева. Пакет знает приём, но не знает имён — их пишет проект |
23
+ | Спек | Описание домена: как он работает. Говорит об установившемся, а не о предстоящем |
24
+ | Домен | Предмет, у которого свой спек. Выросший домен делится на поддомены, а не на соседние домены |
25
+ | Сценарий | Наблюдаемое поведение под номером `SC-<ПРЕФИКС>-<НОМЕР>`. Номер стоит в заголовке теста |
26
+ | Привязка | Строка `` `файл:символ` `` в компаньоне или в спутнике спека — место, где утверждение исполняется |
27
+ | Спутник | Файл рядом со спеком или правилом: компаньон, перечень сценариев |
28
+ | Договорённость о продукте | Как продукт себя поведёт, записанное до кода. Единственное место, где спек говорит о будущем; после выкатки вливается в спек домена, а директория удаляется |
29
+ | Ресурс | Единица того, что везёт пакет правил: закон, правило, паттерн, гард, проверка, роль, команда, конвейер, шаблон, умолчание, документ |
30
+ | Раскладка | Перенос ресурса из пакета в дерево по его роду и настройке слоя |
31
+ | Разложенный файл | Файл в дереве с шапкой пакета. Правится не на месте, а надстройкой: правка на месте теряется на следующей раскладке |
32
+ | Надстройка | Файл дерева, который сливается с разложенным по заголовкам разделов |
33
+
34
+ ## Работа
35
+
36
+ | Термин | Что это |
37
+ | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
38
+ | Задача | Единица работы в очереди работ. Заводится до ветки, и номер её стоит в имени ветки и в заголовке отчёта |
39
+ | Отчёт | Заявка на слияние: то же название, что у задачи, переведённое в сделанное |
40
+ | Очередь работ | Доска, на которой видно состояние каждой задачи. Ветки она не видит |
41
+ | Папка задачи | Одна работа от разбора до слияния: разбор просьбы, замысел, ход работы. Умирает со слиянием — разбирается, и объясняющее решение уезжает в архив |
42
+ | Разбор | Расспрос владельца до первой правки. Записывается его словами и задним числом не переписывается |
43
+ | Замысел | Файл папки задачи: след задачи и этапы с признаками готовности. После написания не правится — с ним сверяют результат при приёмке |
44
+ | Ход работы | Файл папки задачи: «Где стоим», решения по ходу с причинами, записи заходов. Единственное место, где отмечается сделанное. Журналом не называется |
45
+ | След задачи | Раздел замысла: какие спеки, законы, правила и части кода работа задевает |
46
+ | Заход | Одна сессия работы над задачей. Работа живёт дольше одного захода, и между ними её состояние держит только ход работы |
47
+ | Заполнение окна | Доля места захода, которую он уже занял: вход, запись в кэш, прочитанное из кэша и вывод последнего ответа, делённые на размер окна. Не «расход» и не «бюджет»: речь о месте, а не о деньгах |
48
+ | Передача | Текст, которым заход закрывается: рабочее дерево, ветка, задача, где лежит ход работы, что сделано, следующий шаг, особенности захода. Кладётся вне дерева и не коммитится |
49
+ | Линия работ | Файл с порядком задач и зависимостями между ними, когда из одного разбора вышло несколько задач. Шире одной ветки |
50
+ | Архив | Записи о состоявшемся: что объясняет закрытое решение. После выкатки не правится |
51
+
52
+ ## Проверки
53
+
54
+ | Термин | Что это |
55
+ | --------------------- | --------------------------------------------------------------------------------------------------------------------------- |
56
+ | Гард | Хук агента, который отбивает действие до того, как оно сделано, и говорит, чем отказ снимается |
57
+ | Гейт | Требование, которое пропускает действие один раз за сессию после того, как выполнено: загружено правило, пройдены проверки |
58
+ | Отказ в пользу работы | Устройство гарда, при котором любая его поломка пропускает действие. Сломанный гард не имеет права остановить работу совсем |
59
+ | Прогон | Запуск набора сценариев. «Тесты гоняются», а не «запускаются в работу» |
60
+ | Сверка | Проверка, которая ничего не правит, а называет расхождения: раскладки с пакетом, спеков с кодом, очереди работ с ветками |
61
+ | Замер | Число, снятое с работающего приложения. Взгляд на экран замером не является |
62
+
63
+ ## Так не пишем
64
+
65
+ | Так не пишем | Пишем так |
66
+ | -------------------------- | ---------------------------------------------------------------------------------------------- |
67
+ | спека (о тесте) | тест — файл рядом с исходником; спек — документ. Одна буква разницы, а значения противоположны |
68
+ | таска, тикет | задача |
69
+ | пул-реквест, мёрдж-реквест | отчёт, а действие — слияние |
70
+ | джоба, пайплайн | конвейер и его шаг |
71
+ | хендофф | передача |
72
+ | бэклог | очередь работ |
73
+ | контекст-виндоу | окно захода, а его доля — заполнение окна |
74
+ | скилл, скилы | правило, паттерн или скил без закона — по тому, что это на самом деле |