@rt-tools/agent-kit 0.13.0 → 0.15.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 (94) hide show
  1. package/README.md +17 -0
  2. package/assets/checks/board-runs.github.mjs +45 -0
  3. package/assets/checks/board.github.mjs +43 -1
  4. package/assets/checks/check-board.github.mjs +51 -1
  5. package/assets/checks/check-descriptions.mjs +123 -0
  6. package/assets/checks/check-dupes.mjs +31 -3
  7. package/assets/checks/check-file-size.mjs +47 -2
  8. package/assets/checks/check-turn-map.mjs +20 -3
  9. package/assets/checks/lib-common.mjs +12 -1
  10. package/assets/checks/lib-domains.mjs +1 -1
  11. package/assets/checks/rt-kit-checks.config.mjs +17 -1
  12. package/assets/checks/spec-anchors.mjs +18 -3
  13. package/assets/checks/spec-common.mjs +5 -1
  14. package/assets/checks/task-new.github.mjs +15 -3
  15. package/assets/defaults/project.sh +21 -16
  16. package/assets/defaults/turn-map.md +15 -19
  17. package/assets/docs/GLOSSARY.md +52 -58
  18. package/assets/hooks/claim-guard.sh +34 -0
  19. package/assets/hooks/dispatch.sh +48 -15
  20. package/assets/hooks/hook-input.sh +39 -0
  21. package/assets/hooks/rule-article.sh +12 -0
  22. package/assets/hooks/skill-gate.sh +5 -4
  23. package/assets/hooks/task-flow-guard.sh +20 -0
  24. package/assets/hooks/turn-exit-guard.sh +248 -8
  25. package/assets/hooks/waiting-turn-guard.sh +39 -3
  26. package/assets/laws/delivery.md +92 -104
  27. package/assets/laws/frontend-application.md +4 -0
  28. package/assets/laws/project-documentation.md +64 -68
  29. package/assets/laws/verifiability.md +32 -33
  30. package/assets/laws/work-conduct.md +167 -157
  31. package/assets/patterns/doc-style-sweep.md +1 -1
  32. package/assets/patterns/doc-style-trace.md +1 -1
  33. package/assets/patterns/git-workflow-commit.azure.md +1 -1
  34. package/assets/patterns/git-workflow-commit.github.md +7 -1
  35. package/assets/patterns/git-workflow-commit.gitlab.md +1 -1
  36. package/assets/patterns/git-workflow-docker.md +1 -1
  37. package/assets/patterns/git-workflow-merge.md +14 -3
  38. package/assets/patterns/git-workflow-pr.azure.md +1 -1
  39. package/assets/patterns/git-workflow-pr.github.md +1 -1
  40. package/assets/patterns/git-workflow-pr.gitlab.md +1 -1
  41. package/assets/patterns/git-workflow-restart.md +1 -1
  42. package/assets/patterns/git-workflow-secrets.md +1 -1
  43. package/assets/patterns/git-workflow-stack.md +93 -0
  44. package/assets/patterns/seo-page.md +1 -1
  45. package/assets/patterns/spec-driven-rule.md +55 -0
  46. package/assets/patterns/status-report-table.github.md +88 -0
  47. package/assets/patterns/task-flow-archive.md +3 -4
  48. package/assets/patterns/task-flow-close.md +6 -1
  49. package/assets/patterns/task-flow-start.md +17 -5
  50. package/assets/patterns/ts-procedure.md +1 -1
  51. package/assets/pitfalls/doc-style.md +5 -0
  52. package/assets/pitfalls/git-workflow.github.md +55 -0
  53. package/assets/pitfalls/task-flow.md +28 -0
  54. package/assets/pitfalls/testing.md +14 -0
  55. package/assets/pitfalls/turn-conduct.md +33 -0
  56. package/assets/rules/angular-patterns.md +1 -1
  57. package/assets/rules/api-layer.md +3 -3
  58. package/assets/rules/browser-verification.md +15 -1
  59. package/assets/rules/dependencies.md +1 -1
  60. package/assets/rules/deploy-flow.azure.md +1 -1
  61. package/assets/rules/deploy-flow.github.md +1 -1
  62. package/assets/rules/deploy-flow.gitlab.md +1 -1
  63. package/assets/rules/doc-style.md +7 -0
  64. package/assets/rules/entity-conventions.needs-admin.md +1 -1
  65. package/assets/rules/entity-models.md +1 -1
  66. package/assets/rules/git-workflow.azure.md +1 -1
  67. package/assets/rules/git-workflow.github.md +154 -181
  68. package/assets/rules/git-workflow.gitlab.md +1 -1
  69. package/assets/rules/lib-layers.md +1 -1
  70. package/assets/rules/observability.needs-app.md +1 -1
  71. package/assets/rules/platform-access.md +1 -1
  72. package/assets/rules/reuse-first.md +1 -1
  73. package/assets/rules/seo.md +4 -3
  74. package/assets/rules/shared-code.md +1 -1
  75. package/assets/rules/spec-driven.md +68 -1
  76. package/assets/rules/status-report.md +97 -0
  77. package/assets/rules/styling-bem.md +12 -0
  78. package/assets/rules/task-flow.md +102 -98
  79. package/assets/rules/testing.md +67 -66
  80. package/assets/rules/turn-conduct.md +155 -85
  81. package/assets/rules/turn-entry.md +7 -1
  82. package/assets/rules/typescript-conventions.md +1 -1
  83. package/assets/skills/agent-kit-extend.md +1 -1
  84. package/assets/skills/agent-kit.md +18 -1
  85. package/bin/agent-kit.d.ts.map +1 -1
  86. package/bin/agent-kit.js +25 -0
  87. package/bin/agent-kit.js.map +1 -1
  88. package/lib/cost.d.ts +44 -0
  89. package/lib/cost.d.ts.map +1 -0
  90. package/lib/cost.js +181 -0
  91. package/lib/cost.js.map +1 -0
  92. package/package.json +1 -1
  93. package/rt-tools-agent-kit-0.15.0.tgz +0 -0
  94. package/rt-tools-agent-kit-0.13.0.tgz +0 -0
@@ -66,8 +66,12 @@ const E2E_ROOTS = CONFIG.e2eRoots;
66
66
  * переписан целиком. Алфавит не перечисляется диапазонами: перечисленные молча не покрывают
67
67
  * соседнего, и промах выглядит отсутствием привязки. Путь при этом остаётся латинским — он
68
68
  * адрес в дереве, а не слово текста.
69
+ *
70
+ * Решётка перед именем законна: приватное поле класса объявлено с ней, и записанное без неё имя
71
+ * называет метод не тем именем, каким он объявлен. Проверка при этом остаётся зелёной — граница
72
+ * слова перед решёткой есть, — поэтому промах не краснеет ни разу и виден только чтением.
69
73
  */
70
- const ANCHOR = /`([\w./-]+\.[A-Za-z]{2,10}):(\p{L}[\p{L}\p{N}_-]*|_[\w-]*)`/gu;
74
+ const ANCHOR = /`([\w./-]+\.[A-Za-z]{2,10}):(#?\p{L}[\p{L}\p{N}_-]*|#?_[\w-]*)`/gu;
71
75
  /**
72
76
  * Явный вердикт вместо якоря: статья, которой в дереве исполняться негде. Так бывает
73
77
  * законно — правило говорит о службе, которой дерево не держит, или о движении человека,
@@ -17,7 +17,7 @@
17
17
  * Тело читается со стандартного ввода. Автор и исполнитель — учётная запись бота,
18
18
  * та же, от которой идут коммиты; `--assignee` перекрывает исполнителя.
19
19
  */
20
- import { existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
20
+ import { cpSync, existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
21
21
  import { dirname, join, resolve } from 'node:path';
22
22
  import { fileURLToPath } from 'node:url';
23
23
 
@@ -38,6 +38,7 @@ import {
38
38
  graphql,
39
39
  numberFromTitle,
40
40
  taskState,
41
+ unstampFolder,
41
42
  } from './board.mjs';
42
43
 
43
44
  function parseArgs(argv) {
@@ -161,6 +162,7 @@ const branch = args.slug ? `${TASK_KEY}-${number}-${args.slug}` : `${TASK_KEY}-$
161
162
  * `docs/tasks/_draft-<slug>`. Оставленный черновиком, он остаётся вне истории, а следующий
162
163
  * заход его не находит: хук запуска ищет папку по имени ветки.
163
164
  */
165
+
164
166
  function adoptDraft() {
165
167
  if (!args.slug) {
166
168
  console.log(`\nПапка задачи: --slug не задан, переименовать черновик нечем.`);
@@ -174,10 +176,20 @@ function adoptDraft() {
174
176
  console.log(`\nПапка задачи уже на месте: docs/tasks/${branch}/`);
175
177
  } else if (existsSync(draft)) {
176
178
  renameSync(draft, target);
179
+ // Черновик тоже собирают с образца, и шапка в нём та же: снимается она и здесь.
180
+ unstampFolder(target);
177
181
  console.log(`\nПапка задачи: docs/tasks/_draft-${args.slug}/ → docs/tasks/${branch}/`);
178
182
  } else {
179
- console.log(`\nПапка задачи собирается с образца:\n cp -r docs/tasks/_template docs/tasks/${branch}`);
180
- return;
183
+ const template = join(root, 'docs/tasks/_template');
184
+
185
+ if (!existsSync(template)) {
186
+ console.log(`\nПапки задачи нет, и собрать её не с чего: образца ${'docs/tasks/_template'} в дереве не лежит`);
187
+ return;
188
+ }
189
+
190
+ cpSync(template, target, { recursive: true });
191
+ unstampFolder(target);
192
+ console.log(`\nПапка задачи собрана с образца: docs/tasks/${branch}/`);
181
193
  }
182
194
 
183
195
  // Шапку замысла читает гард: по ней он находит договорённость о продукте. Номер в ней
@@ -258,24 +258,29 @@ rt_shell_writes_default() {
258
258
  # Судится заголовок команды — именно в нём стоит тот путь, куда команда пишет.
259
259
  #
260
260
  # Исключение — интерпретатор: ему код приходит телом, и путь записи стоит именно там. Признак
261
- # ошибается в сторону лишнего чтения тела: имя интерпретатора, стоящее в команде где угодно,
262
- # возвращает разбор тела целиком.
261
+ # читается у той строки, которая тело открыла, а не у всей команды: тело принадлежит команде
262
+ # своего заголовка. Прежде он читался у всего текста разом, и слово из документа отключало
263
+ # вырезание целиком — строка «**Чем проверяется:** `bash projects/…`» в замысле делала запись
264
+ # `plan.md` правкой кода приложения. Гард отбивал тем самым запись того файла, отсутствием
265
+ # которого он же и отказывает.
263
266
  rt_shell_paths_default() {
264
267
  text="$(printf '%s' "$1" | tr "\"'\`" ' ')"
265
- if ! printf '%s' "$text" \
266
- | grep -Eq '(^|[|;&(]|[[:space:]])(python3?|node|ruby|perl|php|deno|bun|bash|sh|zsh)([[:space:]]|$)'; then
267
- text="$(printf '%s' "$text" | awk '
268
- function trim(s) { sub(/^[ \t]+/, "", s); sub(/[ \t]+$/, "", s); return s }
269
- tag != "" { if (trim($0) == tag) tag = ""; next }
270
- {
271
- print
272
- if (match($0, /<<-?[ \t]*[A-Za-z_][A-Za-z0-9_]*/)) {
273
- t = substr($0, RSTART, RLENGTH)
274
- sub(/^<<-?[ \t]*/, "", t)
275
- tag = t
276
- }
277
- }')"
278
- fi
268
+ text="$(printf '%s' "$text" | awk '
269
+ function trim(s) { sub(/^[ \t]+/, "", s); sub(/[ \t]+$/, "", s); return s }
270
+ tag != "" {
271
+ if (keep) { print }
272
+ if (trim($0) == tag) { tag = ""; keep = 0 }
273
+ next
274
+ }
275
+ {
276
+ print
277
+ if (match($0, /<<-?[ \t]*[A-Za-z_][A-Za-z0-9_]*/)) {
278
+ t = substr($0, RSTART, RLENGTH)
279
+ sub(/^<<-?[ \t]*/, "", t)
280
+ tag = t
281
+ keep = ($0 ~ /(^|[|;&(]|[ \t])(python3?|node|ruby|perl|php|deno|bun|bash|sh|zsh)([ \t]|$)/)
282
+ }
283
+ }')"
279
284
 
280
285
  # Пути берутся только у тех кусков команды, которые пишут. Прежде брались у всей строки
281
286
  # целиком, и команда чтения, сцепленная с записью, отдавала свои пути как цели записи:
@@ -9,19 +9,17 @@
9
9
 
10
10
  ## Состояния и обязательные действия
11
11
 
12
- | Состояние | Обязательное действие | Ведёт паттерн |
13
- | ------------------------- | ------------------------------------------------------ | ------------------ |
14
- | `просьба-не-разобрана` | разведка по дереву, затем вопросы | `task-flow-start` |
15
- | `разбор-закрыт` | договорённость о продукте либо причина её отсутствия | `task-flow-start` |
16
- | `договорённость-записана` | завести задачу, ветку и папку | `task-flow-start` |
17
- | `задача-взята` | написать замысел | `task-flow-start` |
18
- | `замысел-записан` | делать первый этап | `task-flow-start` |
19
- | `этап-идёт` | доделать этап и отметить в ходе работы | `task-flow-resume` |
20
- | `этапы-кончились` | прогнать набор и открыть PR черновиком | `task-flow-close` |
21
- | `работа-отдана` | взять следующую задачу | `task-flow-resume` |
22
- | `разбор-кончился` | влить договорённость, привести тексты, разобрать папку | `task-flow-close` |
23
- | `папка-разобрана` | снять черновик и попросить влить | `task-flow-archive` |
24
- | `влито` | разбор работы правилами и сверка очереди | `task-flow-archive` |
12
+ - `просьба-не-разобрана` разведка по дереву, затем вопросы; ведёт `task-flow-start`
13
+ - `разбор-закрыт` договорённость о продукте либо причина её отсутствия; ведёт `task-flow-start`
14
+ - `договорённость-записана` завести задачу, ветку и папку; ведёт `task-flow-start`
15
+ - `задача-взята` написать замысел; ведёт `task-flow-start`
16
+ - `замысел-записан` делать первый этап; ведёт `task-flow-start`
17
+ - `этап-идёт` доделать этап и отметить в ходе работы; ведёт `task-flow-resume`
18
+ - `этапы-кончились` прогнать набор и открыть PR черновиком; ведёт `task-flow-close`
19
+ - `работа-отдана` взять следующую задачу; ведёт `task-flow-resume`
20
+ - `разбор-кончился` влить договорённость, привести тексты, разобрать папку; ведёт `task-flow-close`
21
+ - `папка-разобрана` снять черновик и попросить влить; ведёт `task-flow-archive`
22
+ - `влито` разбор работы правилами и сверка очереди; ведёт `task-flow-archive`
25
23
 
26
24
  Ни у одного состояния обязательное действие не звучит как «ждать». Прогон, разбор владельцем и
27
25
  слияние идут без исполнителя и от взгляда быстрее не становятся.
@@ -30,12 +28,10 @@
30
28
 
31
29
  Способов четыре, и других нет.
32
30
 
33
- | Выход | Чем подтверждается |
34
- | -------------------------------------------------- | --------------------------------------------------------------- |
35
- | вопрос владельцу, ответа на который в правилах нет | вопрос задан, и за тот же ход правила читались |
36
- | отказ гарда | отказ назван владельцу, обход не искался |
37
- | заполненное окно там, где сжатия нет | ход работы дописан, передача написана |
38
- | работа отдана, и следующая начата | PR открыт, и по следующей задаче сделано действие, а не сказано |
31
+ - **вопрос владельцу, ответа на который в правилах нет** — вопрос задан, и за тот же ход правила читались
32
+ - **отказ гарда** отказ назван владельцу, обход не искался
33
+ - **заполненное окно там, где сжатия нет** ход работы дописан, передача написана
34
+ - **работа отдана, и следующая начата** PR открыт, и по следующей задаче сделано действие, а не сказано
39
35
 
40
36
  Там, где дерево объявило порог сжатия ниже порога остановки, заполненное окно ход не кончает:
41
37
  контекст сжимается, передача приходит входом, и работа идёт дальше тем же заходом. Порог
@@ -13,70 +13,64 @@
13
13
 
14
14
  ## Слой правил
15
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
- | Слово | Реплика человека посреди работы о слое правил: что мешает, чего не хватило, что сработало не так. Ложится блоком в файл предложений, а не живёт до конца захода |
16
+ - **Закон** файл в каталоге конституции: что должно быть верно, без путей и имён файлов. Верен для любого приложения этого класса
17
+ - **Законы приложения** слой законов, верных только для этого приложения: деньги, локали, доступ. Предметность в них законна — она их предмет
18
+ - **Правило** скил с `kind: rule`. Привязывает закон к этому дереву: чем это здесь названо и где лежит
19
+ - **Паттерн** скил с `kind: pattern`. Готовый код и порядок действий; стоит при правиле
20
+ - **Компаньон** файл `implementation.md` рядом с правилом: имена и пути этого дерева. Пакет знает приём, но не знает имён — их пишет проект
21
+ - **Спек** описание домена: как он работает. Говорит об установившемся, а не о предстоящем
22
+ - **Домен** предмет, у которого свой спек. Выросший домен делится на поддомены, а не на соседние домены
23
+ - **Сценарий** наблюдаемое поведение под номером `SC-<ПРЕФИКС>-<НОМЕР>`. Номер стоит в заголовке теста
24
+ - **Привязка** строка `` `файл:символ` `` в компаньоне или в спутнике спека место, где утверждение исполняется
25
+ - **Спутник** файл рядом со спеком или правилом: компаньон, перечень сценариев
26
+ - **Договорённость о продукте** как продукт себя поведёт, записанное до кода: единственное место, где спек говорит о будущем. После выкатки вливается в спек домена
27
+ - **Ресурс** единица того, что везёт пакет правил: закон, правило, паттерн, гард, проверка, роль, команда, конвейер, шаблон, умолчание, документ
28
+ - **Раскладка** перенос ресурса из пакета в дерево по его роду и настройке слоя
29
+ - **Разложенный файл** файл в дереве с шапкой пакета. Правится не на месте, а надстройкой: правка на месте теряется на следующей раскладке
30
+ - **Надстройка** файл дерева, который сливается с разложенным по заголовкам разделов
31
+ - **Выключенная роль** роль, вызов которой дерево перестало считать обязательным: гард при ней молчит. Названа списком в настройке дерева, из раскладки не убирается и зовётся руками
32
+ - **Наблюдение** строка о событии слоя правил: правило загружено, гейт отбил, гард отказал. Пишет гард, живёт в дереве, наружу уезжает счётчиками. Журналом не называется
33
+ - **Сводка** что наблюдения говорят за отрезок дней: чем пользовались, чем ни разу, обо что спотыкались
34
+ - **Предложение** готовая формулировка правки правил с адресом: пакет, компаньон или дерево. Отправляет её человек командой
35
+ - **Слово** реплика человека посреди работы о слое правил: что мешает, чего не хватило, что сработало не так. Ложится блоком в файл предложений, а не живёт до конца захода
38
36
 
39
37
  ## Работа
40
38
 
41
- | Термин | Что это |
42
- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
43
- | Задача | Единица работы в очереди работ. Заводится до ветки, и номер её стоит в имени ветки и в заголовке PR |
44
- | PR | Заявка на слияние: то же название, что у задачи, переведённое в сделанное. Отчётом, пул-реквестом и мёрдж-реквестом не называется ни в файлах, ни в разговоре |
45
- | Очередь работ | Доска, на которой видно состояние каждой задачи. Ветки она не видит |
46
- | Папка задачи | Одна работа от разбора до слияния: разбор просьбы, замысел, ход работы. Умирает со слияниемразбирается, и объясняющее решение уезжает в архив |
47
- | Разбор | Расспрос владельца до первой правки. Записывается его словами и задним числом не переписывается |
48
- | Замысел | Файл папки задачи: след задачи и этапы с признаками готовности. После написания не правится с ним сверяют результат при приёмке |
49
- | Ход работы | Файл папки задачи: «Где стоим», решения по ходу с причинами, записи заходов. Единственное место, где отмечается сделанное. Журналом не называется |
50
- | Состояние работы | Единица, которой работа ведётся: у каждого состояния названы вход, обязательное действие и выход. Объявляется машиночитаемой строкой в разделе «Где стоим» хода работы — гард судит объявленный переход, а не наличие файлов |
51
- | След задачи | Раздел замысла: какие спеки, законы, правила и части кода работа задевает |
52
- | Заход | Одна сессия работы над задачей. Работа живёт дольше одного захода, и между ними её состояние держит только ход работы |
53
- | Заполнение окна | Доля места захода, которую он уже занял: вход, запись в кэш, прочитанное из кэша и вывод последнего ответа, делённые на размер окна. Не «расход» и не «бюджет»: речь о месте, а не о деньгах |
54
- | Передача | Текст, которым заход закрывается: рабочее дерево, ветка, задача, где лежит ход работы, что сделано, следующий шаг, особенности захода. Кладётся вне дерева и не коммитится |
55
- | Эпик | Серия задач одной темы, выполняемых в назначенном порядке. Живёт в двух местах сразу: карточка в очереди работ с меткой эпика и замысел рядом с ней — что за возможность разрабатывается, какие задачи входят и в каком порядке. Шире одной ветки. Линией работ не называется |
56
- | Архив | Записи о состоявшемся: что объясняет закрытое решение. После выкатки не правится |
39
+ - **Задача** единица работы в очереди работ. Заводится до ветки, и номер её стоит в имени ветки и в заголовке PR
40
+ - **PR** заявка на слияние: то же название, что у задачи, переведённое в сделанное. Отчётом, пул-реквестом и мёрдж-реквестом не называется — ни в файлах, ни в разговоре
41
+ - **Очередь работ** доска, на которой видно состояние каждой задачи. Ветки она не видит
42
+ - **Папка задачи** одна работа от разбора до слияния: разбор просьбы, замысел, ход работы. Умирает со слиянием разбирается, и объясняющее решение уезжает в архив
43
+ - **Разбор** расспрос владельца до первой правки. Записывается его словами и задним числом не переписывается
44
+ - **Замысел** файл папки задачи: след задачи и этапы с признаками готовности. После написания не правитсяс ним сверяют результат при приёмке
45
+ - **Ход работы** файл папки задачи: «Где стоим», решения по ходу с причинами, записи заходов. Единственное место, где отмечается сделанное. Журналом не называется
46
+ - **Состояние работы** единица, которой работа ведётся: у каждого названы вход, обязательное действие и выход. Объявляется строкой в разделе «Где стоим» хода работы, и гард судит её, а не наличие файлов
47
+ - **След задачи** раздел замысла: какие спеки, законы, правила и части кода работа задевает
48
+ - **Заход** одна сессия работы над задачей. Работа живёт дольше захода, и между ними её состояние держит ход работы
49
+ - **Заполнение окна** доля места захода, которую он уже занял: вход, запись в кэш, прочитанное из кэша и вывод последнего ответа, делённые на размер окна. Не «расход» и не «бюджет»: речь о месте, а не о деньгах
50
+ - **Передача** текст, которым заход закрывается: рабочее дерево, ветка, задача, где лежит ход работы, что сделано, следующий шаг, особенности захода. Кладётся вне дерева и не коммитится
51
+ - **Эпик** серия задач одной темы в назначенном порядке, шире одной ветки. Живёт в двух местах: карточка с меткой эпика и замысел рядом с ней. Линией работ не называется
52
+ - **Архив** записи о состоявшемся: что объясняет закрытое решение. После выкатки не правится
57
53
 
58
54
  ## Проверки
59
55
 
60
- | Термин | Что это |
61
- | --------------------- | --------------------------------------------------------------------------------------------------------------------------- |
62
- | Гард | Хук агента, который отбивает действие до того, как оно сделано, и говорит, чем отказ снимается |
63
- | Гейт | Требование, которое пропускает действие один раз за сессию после того, как выполнено: загружено правило, пройдены проверки |
64
- | Отказ в пользу работы | Устройство гарда, при котором любая его поломка пропускает действие. Сломанный гард не имеет права остановить работу совсем |
65
- | Прогон | Запуск набора сценариев. «Тесты гоняются», а не «запускаются в работу» |
66
- | Сверка | Проверка, которая ничего не правит, а называет расхождения: раскладки с пакетом, спеков с кодом, очереди работ с ветками |
67
- | Замер | Число, снятое с работающего приложения. Взгляд на экран замером не является |
56
+ - **Гард** хук агента, который отбивает действие до того, как оно сделано, и говорит, чем отказ снимается
57
+ - **Гейт** требование, которое пропускает действие один раз за сессию после того, как выполнено: загружено правило, пройдены проверки
58
+ - **Отказ в пользу работы** устройство гарда, при котором любая его поломка пропускает действие. Сломанный гард не имеет права остановить работу совсем
59
+ - **Прогон** запуск набора сценариев. «Тесты гоняются», а не «запускаются в работу»
60
+ - **Сверка** проверка, которая ничего не правит, а называет расхождения: раскладки с пакетом, спеков с кодом, очереди работ с ветками
61
+ - **Замер** число, снятое с работающего приложения. Взгляд на экран замером не является
68
62
 
69
63
  ## Так не пишем
70
64
 
71
- | Так не пишем | Пишем так |
72
- | --------------------------- | ---------------------------------------------------------------------------------------------- |
73
- | спека (о тесте) | тест — файл рядом с исходником; спек — документ. Одна буква разницы, а значения противоположны |
74
- | таска, тикет | задача |
75
- | пул-реквест, мёрдж-реквест | PR, а действие — слияние |
76
- | отчёт (о заявке на слияние) | PR; отчёт — сводка данных, и слово занято ею |
77
- | джоба, пайплайн | конвейер и его шаг |
78
- | хендофф | передача |
79
- | бэклог | очередь работ |
80
- | линия работ | эпик |
81
- | контекст-виндоу | окно захода, а его доля — заполнение окна |
82
- | скилл, скилы | правило, паттерн или скил без закона — по тому, что это на самом деле |
65
+ Слева то, что не пишется и не произносится нигде; справа — чем это зовут здесь.
66
+
67
+ - **спека (о тесте)** тест — файл рядом с исходником; спек — документ. Одна буква разницы, а значения противоположны
68
+ - **таска, тикет** задача
69
+ - **пул-реквест, мёрдж-реквест** PR, а действие — слияние
70
+ - **отчёт (о заявке на слияние)** PR; отчёт — сводка данных, и слово занято ею
71
+ - **джоба, пайплайн** конвейер и его шаг
72
+ - **хендофф** передача
73
+ - **бэклог** очередь работ
74
+ - **линия работ** эпик
75
+ - **контекст-виндоу** окно захода, а его доля — заполнение окна
76
+ - **скилл, скилы** правило, паттерн или скил без закона — по тому, что это на самом деле
@@ -41,6 +41,36 @@ transcript="$(printf '%s' "$input" | jq -r '.transcript_path // empty' 2>/dev/nu
41
41
  [ -z "$transcript" ] && exit 0
42
42
  [ -f "$transcript" ] || exit 0
43
43
 
44
+ # Текст ответа ложится в запись хода не раньше, чем хост позовёт хук: гард, прочитавший файл
45
+ # первым, судит ход, у которого сказанного нет вовсе, — и молчит, будучи неотличим от гарда,
46
+ # который посмотрел и пропустил. Ждём его появления, и не дождавшись — возвращаем ход: пустая
47
+ # запись означает не «владельцу ничего не сказано», а «прочитать нечего».
48
+ #
49
+ # Отказ этот принадлежит одному гарду нарочно. Текст судят трое, и печатай они свои объекты
50
+ # подряд, вывод перестал бы разбираться целиком — то есть отбой пропал бы весь.
51
+ if ! rt_turn_has_text "$transcript"; then
52
+ reason="BLOCKED by claim-guard: запись хода не отдала ни одного текста ответа, и судить сказанное владельцу нечем.
53
+
54
+ Текст ложится в запись не раньше, чем хост зовёт хук. Прочитанная слишком рано запись выглядит ходом, в котором владельцу ничего не сказано, — и все гарды, судящие сказанное, проходят мимо молча.
55
+
56
+ Повтори завершение хода: к этой минуте текст в записи уже есть. Ход при этом ничего не теряет — сказанное владельцу остаётся тем же.
57
+
58
+ Гард судит один ход: следующий заход не отбивается."
59
+
60
+ # shellcheck disable=SC1090
61
+ [ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" ] \
62
+ && . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" 2>/dev/null
63
+ command -v rt_deny_tail >/dev/null 2>&1 || rt_deny_tail() { :; }
64
+ deny_tail_text="$(rt_deny_tail "")"
65
+ [ -n "$deny_tail_text" ] && reason="${reason}
66
+
67
+ ${deny_tail_text}"
68
+
69
+ jq -n --arg r "$reason" '{decision:"block",reason:$r}' 2>/dev/null \
70
+ || printf '{"decision":"block","reason":"claim-guard: запись хода не отдала текста ответа — повтори завершение хода."}\n'
71
+ exit 0
72
+ fi
73
+
44
74
  # Ход — всё, что записано после последней настоящей реплики владельца: ответ инструмента
45
75
  # приходит той же ролью и репликой не считается.
46
76
  turn="$(tail -n 400 "$transcript" 2>/dev/null | jq -s -r '
@@ -76,6 +106,10 @@ claims=(
76
106
  '(ветки|ветка|файлы|файл|папка|каталог)[^.]{0,40}(снят|удал|почищ|вычищ)|снят[оыа] с§git branch|git push .*--delete|git rm|gh api|rm §команду удаления — `git branch -d`, `git push --delete` или `git rm`'
77
107
  'прогон (зелёный|прошёл|кончился)|конвейер зелёный|проверки на PR зелёные§gh run§`gh run list` или `gh run view`'
78
108
  '(работа|правка|задача) готова|можно вливать|PR открыт|черновик снят§gh pr §`gh pr create`, `gh pr view` или `gh pr ready`'
109
+ # Ожидание чужого шага — тоже утверждение о состоянии, и врать ему есть чем: прогон бывает
110
+ # зелёным час, бывает не встав вовсе. Сказанное без команды оставляет готовую работу
111
+ # черновиком, и владелец узнаёт об этом последним — дважды за сутки так и вышло.
112
+ 'жд[уёя][^.]{0,20}прогон|дожида[ею][^.]{0,20}прогон|прогон[^.]{0,20}(ещё идёт|не встал|не кончился|не дошёл)|черновик[^.]{0,30}(не снимаю|сниму|снимется)§gh run|gh pr checks|check-runs|check:board|board\.mjs§команду о прогоне — `gh run list`, `gh pr checks` или сверку очереди работ'
79
113
  'задача заведена|задача (в|переведена в) колонк|колонка переведена§gh issue|gh api|task:new|task:move|board\.mjs§команду очереди работ — заведение задачи или перевод колонки'
80
114
  '(в дереве|в репозитории|здесь|такого файла|такой команды)[^.]{0,30}(нет|не бывает)|не заводили|нигде не встречается§grep|rg |ls |find |git ls-files|git grep|git log|cat §команду поиска — `grep`, `git ls-files` или обход каталога'
81
115
  )
@@ -46,24 +46,57 @@ export RT_HOOK_INPUT="$input"
46
46
  branches="$(grep -l '^# rt-hook:' "$here"/*.sh 2>/dev/null | sort)"
47
47
  [ -z "$branches" ] && exit 0
48
48
 
49
+ # Объявлений у файла бывает несколько: гард, стоящий и на вызове инструмента, и на завершении
50
+ # хода, называет оба события своими строками. Читалось прежде только первое — и вторая ветка не
51
+ # звалась ни разу, молча: снаружи это неотличимо от гарда, который посмотрел и пропустил.
52
+ collected=""
49
53
  for branch in $branches; do
50
- declaration="$(sed -n 's/^# rt-hook:[[:space:]]*//p' "$branch" 2>/dev/null | head -1)"
51
- [ -z "$declaration" ] && continue
52
-
53
- branch_event="${declaration%% *}"
54
- [ "$branch_event" = "$event" ] || continue
55
-
56
- # Образец вызова: его нет вовсе — гард зовётся на любом; есть — сверяется с именем
57
- # инструмента целиком, а не куском. Звёздочка и точка со звёздочкой значат одно: любой вызов.
58
- matcher="${declaration#"$branch_event"}"
59
- matcher="${matcher#"${matcher%%[![:space:]]*}"}"
60
- if [ -n "$matcher" ] && [ "$matcher" != '*' ] && [ "$matcher" != '.*' ]; then
61
- [[ "${RT_HOOK_TOOL:-}" =~ ^(${matcher})$ ]] || continue
62
- fi
54
+ matched=0
55
+ while IFS= read -r declaration; do
56
+ [ -z "$declaration" ] && continue
57
+
58
+ branch_event="${declaration%% *}"
59
+ [ "$branch_event" = "$event" ] || continue
60
+
61
+ # Образец вызова: его нет вовсе гард зовётся на любом; есть сверяется с именем
62
+ # инструмента целиком, а не куском. Звёздочка и точка со звёздочкой значат одно: любой
63
+ # вызов.
64
+ matcher="${declaration#"$branch_event"}"
65
+ matcher="${matcher#"${matcher%%[![:space:]]*}"}"
66
+ if [ -n "$matcher" ] && [ "$matcher" != '*' ] && [ "$matcher" != '.*' ]; then
67
+ [[ "${RT_HOOK_TOOL:-}" =~ ^(${matcher})$ ]] || continue
68
+ fi
69
+
70
+ matched=1
71
+ break
72
+ done <<EOF
73
+ $(sed -n 's/^# rt-hook:[[:space:]]*//p' "$branch" 2>/dev/null)
74
+ EOF
75
+
76
+ # Совпало хоть одно объявление — ветка зовётся один раз. Два объявления одного события в
77
+ # одном файле звали бы гард дважды на один ввод, и второй вызов судил бы то же самое.
78
+ [ "$matched" = 1 ] || continue
63
79
 
64
- printf '%s' "$input" | bash "$branch"
80
+ branch_out="$(printf '%s' "$input" | bash "$branch" 2>/dev/null)"
65
81
  code=$?
66
- [ "$code" -ne 0 ] && exit "$code"
82
+ if [ "$code" -ne 0 ]; then
83
+ [ -n "$branch_out" ] && printf '%s\n' "$branch_out"
84
+ exit "$code"
85
+ fi
86
+
87
+ # Отбой ветки приходит не кодом возврата, а решением в выводе: гарды завершения хода
88
+ # печатают его и выходят нулём. Не остановившись здесь, диспетчер склеил бы этот объект с
89
+ # выводом следующей ветки — а склеенное не разбирается, и отбой пропадает целиком.
90
+ if [ -n "$branch_out" ] && printf '%s' "$branch_out" | jq -e '.decision == "block"' >/dev/null 2>&1; then
91
+ printf '%s\n' "$branch_out"
92
+ exit 0
93
+ fi
94
+
95
+ [ -n "$branch_out" ] && collected="${collected}${branch_out}
96
+ "
67
97
  done
68
98
 
99
+ # Ни одна ветка не отбила: отдаётся то, что они напечатали, — подсказки и сводки.
100
+ [ -n "${collected:-}" ] && printf '%s' "$collected"
101
+
69
102
  exit 0
@@ -75,3 +75,42 @@ rt_hook_tool() { rt_hook_field RT_HOOK_TOOL '.tool_name'; }
75
75
  rt_hook_cmd() { rt_hook_field RT_HOOK_CMD '.tool_input.command'; }
76
76
  rt_hook_file() { rt_hook_field RT_HOOK_FILE '.tool_input.file_path'; }
77
77
  rt_hook_cwd() { rt_hook_field RT_HOOK_CWD '.cwd'; }
78
+
79
+ # ЖДЁТ ПОСЛЕДНИЙ ТЕКСТ ХОДА В ЗАПИСИ. Гарды завершения судят то, что сказано владельцу, а запись
80
+ # хода на этот момент бывает неполна: текст ответа ложится в файл не раньше, чем хост позовёт
81
+ # хук, и гард читает ход, у которого текста нет вовсе. Молчит он при этом честно — и снаружи
82
+ # неотличим от гарда, который посмотрел и пропустил. Ровно так ход, поставивший работу в
83
+ # зависимость от слова владельца, ушёл мимо трёх гардов сразу, а тот же ход, поданный им
84
+ # повторно, был отбит.
85
+ #
86
+ # Ждём короткими попытками: файл дописывается за миллисекунды, а ход и без того кончается не
87
+ # мгновенно. Дождались — ноль; текста так и нет — единица, и решает уже гард.
88
+ rt_turn_has_text() {
89
+ local transcript="$1" tries="${2:-20}" got
90
+
91
+ [ -n "$transcript" ] && [ -f "$transcript" ] || return 1
92
+ command -v jq >/dev/null 2>&1 || return 1
93
+
94
+ while [ "$tries" -gt 0 ]; do
95
+ got="$(tail -n 400 "$transcript" 2>/dev/null | jq -s -r '
96
+ def is_input:
97
+ .type == "user"
98
+ and (((.message.content // []) | if type == "array"
99
+ then ([.[] | select(.type == "tool_result")] | length)
100
+ else 0 end) == 0);
101
+
102
+ (map(is_input) | rindex(true)) as $i
103
+ | (if $i == null then . else .[$i + 1:] end)
104
+ | [.[] | select(.type == "assistant") | (.message.content // [])[]
105
+ | select(.type == "text") | .text]
106
+ | length
107
+ ' 2>/dev/null)"
108
+
109
+ [ -n "$got" ] && [ "$got" != '0' ] && return 0
110
+
111
+ tries=$((tries - 1))
112
+ [ "$tries" -gt 0 ] && sleep 0.05
113
+ done
114
+
115
+ return 1
116
+ }
@@ -75,6 +75,18 @@ rt_rule_article_marks() {
75
75
  # Текст статьи, внутри которой стоит строка с признаком. Статья начинается ближайшим сверху
76
76
  # пунктом списка верхнего уровня и кончается перед следующим таким пунктом или перед строкой без
77
77
  # отступа: продолжение статьи всегда идёт с отступом.
78
+ # Заголовок статьи — её жирная первая фраза. Отказ гейта называет статьи ею, а не пересказывает
79
+ # их телом: правило заход грузит следом, и тело придёт в контекст вторым разом. У правила текстов
80
+ # отказ печатал 4 134 знака одиннадцатью статьями, их заголовки — 718.
81
+ #
82
+ # Заголовок при этом не украшение: по нему идёт привязка утверждения к коду, и он же называет
83
+ # статью в отказе так, что её видно в правиле глазами.
84
+ rt_rule_article_heads() {
85
+ rule_file="$1"
86
+ edited="$2"
87
+ rt_rule_articles "$rule_file" "$edited" 2>/dev/null | grep -o '^- \*\*[^*]*\*\*' 2>/dev/null
88
+ }
89
+
78
90
  rt_rule_article_at() {
79
91
  awk -v mark="$2" '
80
92
  NR <= mark && /^- / { start = NR }
@@ -186,13 +186,14 @@ fi
186
186
  # Правило целиком остаётся вторым ходом — для того, кому статьи мало.
187
187
  # shellcheck disable=SC1090
188
188
  [ -f "$rt_hooks_dir/rule-article.sh" ] && . "$rt_hooks_dir/rule-article.sh" 2>/dev/null
189
- if [ "$kind" != "command" ] && [ "$kind" != "browser" ] && command -v rt_rule_articles >/dev/null 2>&1; then
190
- article="$(rt_rule_articles "$root/$rules_dir/${req}/SKILL.md" "$target" 2>/dev/null)"
189
+ if [ "$kind" != "command" ] && [ "$kind" != "browser" ] && command -v rt_rule_article_heads >/dev/null 2>&1; then
190
+ article="$(rt_rule_article_heads "$root/$rules_dir/${req}/SKILL.md" "$target" 2>/dev/null)"
191
191
  if [ -n "$article" ]; then
192
- reason="Отбито гейтом правил. Под эту правку подпадает статья правила «${req}»:
192
+ reason="Отбито гейтом правил. Под эту правку подпадают статьи правила «${req}»:
193
193
 
194
194
  ${article}
195
- Статья снимает чтение правила целиком, а не отказ: загрузи правило «${req}» инструментом Skill и повтори действие. ${fallback} Для этой области это происходит один раз за сессию."
195
+
196
+ Текст этих статей придёт в контекст вместе с правилом, и пересказывать его здесь значило бы платить за один текст дважды: загрузи правило «${req}» инструментом Skill и повтори действие. ${fallback} Для этой области это происходит один раз за сессию."
196
197
  fi
197
198
  fi
198
199
 
@@ -215,6 +215,26 @@ case "$state" in
215
215
  ;;
216
216
  esac
217
217
 
218
+ # Папка задачи едет в ветку коммитом, а не живёт в одном рабочем дереве. Четыре требования выше
219
+ # смотрят диск, и папка, ни разу не закоммиченная, проходит их все без единого отказа — а
220
+ # признак отданной работы гард берёт из истории, и там её нет. Отказ приходит в последней точке,
221
+ # на открытии заявки, когда папка уже разобрана своими руками: чинить нечего, замысел снят, и
222
+ # собирать его приходится заново по памяти. За одну задачу это стоило восьми вызовов и двух
223
+ # отказов подряд.
224
+ #
225
+ # Второе следствие тише: ход работы, живущий в рабочем дереве, не виден никому. Владелец видит
226
+ # ветку без единого следа того, что в ней делается, а следующий заход — пустоту вместо «Где
227
+ # стоим», если рабочее дерево между заходами сменилось.
228
+ #
229
+ # Спрашивается та же история, что и у признака отданной работы: папка стоит в `HEAD` либо её
230
+ # добавлял коммит ветки. Нет git — требования нет: спросить историю нечем.
231
+ if [ -n "$(git rev-parse --verify HEAD 2>/dev/null)" ]; then
232
+ in_tree="$(git ls-tree -d --name-only HEAD -- "$tasks_dir/$branch" 2>/dev/null | head -1)"
233
+ if [ -z "$in_tree" ]; then
234
+ deny "BLOCKED by task-flow: папка задачи '${tasks_dir}/${branch}' лежит в рабочем дереве, а в историю ветки не заведена. Заведи её коммитом (git add ${tasks_dir}/${branch} && git commit), затем повтори: признак отданной работы гард берёт из истории, и с некоммиченной папкой отказ придёт на открытии заявки — когда папка уже разобрана и чинить нечего. Правило — скил task-flow."
235
+ fi
236
+ fi
237
+
218
238
  # Строка обхода: поведение не меняется, договорённость о продукте не нужна. Причина обязана
219
239
  # стоять — без неё обход становится умолчанием.
220
240
  if grep -qE '^\*\*Поведение:\*\*[[:space:]]*не меняется[[:space:]]*—[[:space:]]*\S' "$plan" 2>/dev/null; then