@rt-tools/agent-kit 0.14.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.
- package/README.md +17 -0
- package/assets/checks/check-board.github.mjs +16 -0
- package/assets/checks/check-descriptions.mjs +123 -0
- package/assets/checks/check-dupes.mjs +31 -3
- package/assets/checks/check-file-size.mjs +47 -2
- package/assets/checks/check-turn-map.mjs +20 -3
- package/assets/checks/lib-common.mjs +12 -1
- package/assets/checks/lib-domains.mjs +1 -1
- package/assets/checks/rt-kit-checks.config.mjs +17 -1
- package/assets/checks/spec-anchors.mjs +18 -3
- package/assets/checks/spec-common.mjs +5 -1
- package/assets/defaults/project.sh +21 -16
- package/assets/defaults/turn-map.md +15 -19
- package/assets/docs/GLOSSARY.md +52 -58
- package/assets/hooks/rule-article.sh +12 -0
- package/assets/hooks/skill-gate.sh +5 -4
- package/assets/hooks/task-flow-guard.sh +20 -0
- package/assets/hooks/turn-exit-guard.sh +229 -4
- package/assets/laws/delivery.md +92 -104
- package/assets/laws/frontend-application.md +4 -0
- package/assets/laws/project-documentation.md +64 -68
- package/assets/laws/verifiability.md +32 -33
- package/assets/laws/work-conduct.md +167 -157
- package/assets/patterns/doc-style-sweep.md +1 -1
- package/assets/patterns/doc-style-trace.md +1 -1
- package/assets/patterns/git-workflow-commit.azure.md +1 -1
- package/assets/patterns/git-workflow-commit.github.md +7 -1
- package/assets/patterns/git-workflow-commit.gitlab.md +1 -1
- package/assets/patterns/git-workflow-docker.md +1 -1
- package/assets/patterns/git-workflow-merge.md +14 -3
- package/assets/patterns/git-workflow-pr.azure.md +1 -1
- package/assets/patterns/git-workflow-pr.github.md +1 -1
- package/assets/patterns/git-workflow-pr.gitlab.md +1 -1
- package/assets/patterns/git-workflow-restart.md +1 -1
- package/assets/patterns/git-workflow-secrets.md +1 -1
- package/assets/patterns/git-workflow-stack.md +93 -0
- package/assets/patterns/seo-page.md +1 -1
- package/assets/patterns/spec-driven-rule.md +55 -0
- package/assets/patterns/status-report-table.github.md +88 -0
- package/assets/patterns/task-flow-archive.md +3 -4
- package/assets/patterns/task-flow-close.md +6 -1
- package/assets/patterns/task-flow-start.md +17 -5
- package/assets/patterns/ts-procedure.md +1 -1
- package/assets/pitfalls/doc-style.md +5 -0
- package/assets/pitfalls/git-workflow.github.md +47 -0
- package/assets/pitfalls/task-flow.md +28 -0
- package/assets/pitfalls/testing.md +14 -0
- package/assets/pitfalls/turn-conduct.md +33 -0
- package/assets/rules/angular-patterns.md +1 -1
- package/assets/rules/api-layer.md +3 -3
- package/assets/rules/browser-verification.md +15 -1
- package/assets/rules/dependencies.md +1 -1
- package/assets/rules/deploy-flow.azure.md +1 -1
- package/assets/rules/deploy-flow.github.md +1 -1
- package/assets/rules/deploy-flow.gitlab.md +1 -1
- package/assets/rules/doc-style.md +7 -0
- package/assets/rules/entity-conventions.needs-admin.md +1 -1
- package/assets/rules/entity-models.md +1 -1
- package/assets/rules/git-workflow.azure.md +1 -1
- package/assets/rules/git-workflow.github.md +154 -181
- package/assets/rules/git-workflow.gitlab.md +1 -1
- package/assets/rules/lib-layers.md +1 -1
- package/assets/rules/observability.needs-app.md +1 -1
- package/assets/rules/platform-access.md +1 -1
- package/assets/rules/reuse-first.md +1 -1
- package/assets/rules/seo.md +4 -3
- package/assets/rules/shared-code.md +1 -1
- package/assets/rules/spec-driven.md +68 -1
- package/assets/rules/status-report.md +97 -0
- package/assets/rules/styling-bem.md +12 -0
- package/assets/rules/task-flow.md +102 -100
- package/assets/rules/testing.md +67 -66
- package/assets/rules/turn-conduct.md +146 -105
- package/assets/rules/turn-entry.md +7 -1
- package/assets/rules/typescript-conventions.md +1 -1
- package/assets/skills/agent-kit-extend.md +1 -1
- package/assets/skills/agent-kit.md +18 -1
- package/bin/agent-kit.d.ts.map +1 -1
- package/bin/agent-kit.js +25 -0
- package/bin/agent-kit.js.map +1 -1
- package/lib/cost.d.ts +44 -0
- package/lib/cost.d.ts.map +1 -0
- package/lib/cost.js +181 -0
- package/lib/cost.js.map +1 -0
- package/package.json +1 -1
- package/rt-tools-agent-kit-0.15.0.tgz +0 -0
- package/rt-tools-agent-kit-0.14.0.tgz +0 -0
package/assets/docs/GLOSSARY.md
CHANGED
|
@@ -13,70 +13,64 @@
|
|
|
13
13
|
|
|
14
14
|
## Слой правил
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
44
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
65
|
+
Слева — то, что не пишется и не произносится нигде; справа — чем это зовут здесь.
|
|
66
|
+
|
|
67
|
+
- **спека (о тесте)** — тест — файл рядом с исходником; спек — документ. Одна буква разницы, а значения противоположны
|
|
68
|
+
- **таска, тикет** — задача
|
|
69
|
+
- **пул-реквест, мёрдж-реквест** — PR, а действие — слияние
|
|
70
|
+
- **отчёт (о заявке на слияние)** — PR; отчёт — сводка данных, и слово занято ею
|
|
71
|
+
- **джоба, пайплайн** — конвейер и его шаг
|
|
72
|
+
- **хендофф** — передача
|
|
73
|
+
- **бэклог** — очередь работ
|
|
74
|
+
- **линия работ** — эпик
|
|
75
|
+
- **контекст-виндоу** — окно захода, а его доля — заполнение окна
|
|
76
|
+
- **скилл, скилы** — правило, паттерн или скил без закона — по тому, что это на самом деле
|
|
@@ -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
|
|
190
|
-
article="$(
|
|
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="Отбито гейтом правил. Под эту правку
|
|
192
|
+
reason="Отбито гейтом правил. Под эту правку подпадают статьи правила «${req}»:
|
|
193
193
|
|
|
194
194
|
${article}
|
|
195
|
-
|
|
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
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
# нельзя, и запрет должна держать машина.
|
|
13
13
|
#
|
|
14
14
|
# Что считается работой: правка файла и команда, меняющая дерево или его состояние. Чтение,
|
|
15
|
-
# поиск и разговор работой не считаются — именно ими и заполняется ход, который встал.
|
|
15
|
+
# поиск и разговор работой не считаются — именно ими и заполняется ход, который встал. Читающая
|
|
16
|
+
# подкоманда `git` и клиента хостинга работой не считается тоже, и судится это по частям
|
|
17
|
+
# составной команды: чтение, соединённое с правкой через `&&`, работой остаётся.
|
|
16
18
|
#
|
|
17
19
|
# Что отпускает ход:
|
|
18
20
|
# 1. Работа отдана либо влита — состояние работы говорит об этом само.
|
|
@@ -74,6 +76,43 @@ case "$state" in
|
|
|
74
76
|
работа-отдана | влито) exit 0 ;;
|
|
75
77
|
esac
|
|
76
78
|
|
|
79
|
+
# Записанный замысел концом хода не бывает вовсе. Обязательное действие этого состояния — делать
|
|
80
|
+
# первый этап, а начавший его переводит состояние той же правкой: ход, оставшийся в прежнем
|
|
81
|
+
# состоянии, первого этапа не начал по определению. Второй признак сюда не годится — заведение
|
|
82
|
+
# задачи, ветки, колонки и папки он считает работой, и ход, где сделана одна подготовка,
|
|
83
|
+
# проходит его насквозь. За один заход так вышло трижды; дважды это поймал соседний гард по
|
|
84
|
+
# открытой заявке, третий раз не поймало ничто, а владельцу отчёт о взятой задаче неотличим от
|
|
85
|
+
# остановки.
|
|
86
|
+
if [ "$state" = "замысел-записан" ]; then
|
|
87
|
+
plan="$root/$tasks_dir/$branch/plan.md"
|
|
88
|
+
first_stage="$(grep -m1 '^### ' "$plan" 2>/dev/null | sed 's/^### //')"
|
|
89
|
+
[ -z "$first_stage" ] && first_stage="первый этап замысла"
|
|
90
|
+
reason="BLOCKED by turn-exit-guard: работа стоит в состоянии 'замысел-записан', а обязательное действие этого состояния — делать первый этап — за ход не начато.
|
|
91
|
+
|
|
92
|
+
Заведение задачи, ветки, колонки и папки этим шагом не считается: всё это подготовка к работе, а не работа. Владельцу отчёт о взятой задаче неотличим от остановки — он видит исполнителя стоящим.
|
|
93
|
+
|
|
94
|
+
Первый этап замысла: ${first_stage}
|
|
95
|
+
|
|
96
|
+
Начни его этим же ходом и перепиши состояние на '- **Состояние:** \`этап-идёт\`'. Владелец сказал остановиться — так и напиши: страж читает его слово, а не пересказ.
|
|
97
|
+
|
|
98
|
+
Страж судит один ход: следующий заход не отбивается."
|
|
99
|
+
|
|
100
|
+
# Общий хвост отказа: два законных хода. Файл может быть не разложен — тогда хвоста нет,
|
|
101
|
+
# а причина отказа остаётся прежней.
|
|
102
|
+
# shellcheck disable=SC1090
|
|
103
|
+
[ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" ] \
|
|
104
|
+
&& . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" 2>/dev/null
|
|
105
|
+
command -v rt_deny_tail >/dev/null 2>&1 || rt_deny_tail() { :; }
|
|
106
|
+
deny_tail_text="$(rt_deny_tail "")"
|
|
107
|
+
[ -n "$deny_tail_text" ] && reason="${reason}
|
|
108
|
+
|
|
109
|
+
${deny_tail_text}"
|
|
110
|
+
|
|
111
|
+
jq -n --arg r "$reason" '{decision:"block",reason:$r}' 2>/dev/null \
|
|
112
|
+
|| printf '{"decision":"block","reason":"turn-exit-guard: замысел записан, а первый этап не начат."}\n'
|
|
113
|
+
exit 0
|
|
114
|
+
fi
|
|
115
|
+
|
|
77
116
|
# То же самое, но объявить это на диске уже нечем: папка задачи разбирается до открытия заявки,
|
|
78
117
|
# и ход работы уезжает вместе с ней. Признак берётся из истории ветки — папка, снятая её
|
|
79
118
|
# коммитом.
|
|
@@ -108,7 +147,36 @@ next_step=""
|
|
|
108
147
|
# заполняется ход, который встал.
|
|
109
148
|
work_re='git (add|commit|push|checkout|merge|rm)|npm run|pnpm (run|exec)|nx (build|test|run)|gh (pr|issue|api|run)|task:(new|move)|mkdir|cp |mv |rm |sed -i|tee |>>?[[:space:]]*[^|&]'
|
|
110
149
|
|
|
111
|
-
|
|
150
|
+
# Разведка. Те же слова, что и в образце работы, но подкоманда читающая: переключение ветки,
|
|
151
|
+
# подтягивание, просмотр истории, чтение заявок и прогонов. Образец работы называет их работой,
|
|
152
|
+
# потому что знает только первое слово — `git` и `gh` стоят в нём целиком, — и ход, в котором
|
|
153
|
+
# исполнитель перешёл на главную ветку, прочитал историю и написал владельцу отчёт, выходил
|
|
154
|
+
# отсюда нулём. Разбор — `docs/postmortems/2026-08-25-read-only-turn-counted-as-work.md`.
|
|
155
|
+
#
|
|
156
|
+
# Разведка выглядит работой лучше всего остального: в ней команды, числа и точные ответы. Тем
|
|
157
|
+
# она и опасна — ход, набитый ею, читается как полный и владельцем, и самим заходом.
|
|
158
|
+
read_re='^[[:space:]]*(([^[:space:]]*/)?git[[:space:]]+(show|log|ls-tree|ls-files|ls-remote|diff|status|branch|tag|rev-parse|remote|describe|blame|fetch|pull|(checkout|switch)(?![[:space:]]+-[bc][[:space:]]))|([^[:space:]]*/)?gh[[:space:]]+(pr|issue|run|repo)[[:space:]]+(list|view|status|checks|diff|download|logs))([[:space:]]|$)'
|
|
159
|
+
|
|
160
|
+
# Части составной команды судятся по одной: ход собирает чтение и работу в одну строку через
|
|
161
|
+
# `&&`, и суждение целиком отпускало бы разведку по первой же меняющей части.
|
|
162
|
+
part_re='&&|\|\||;|\n'
|
|
163
|
+
|
|
164
|
+
# Ожидание чужого шага. Прогон, разбор владельцем и слияние идут без исполнителя и от взгляда
|
|
165
|
+
# быстрее не становятся — правило прямо говорит, что состоянием работы это не бывает. Судится
|
|
166
|
+
# только ПОСЛЕДНЕЕ действие хода: ожидание в середине законно, а запуск работы в фоне работой
|
|
167
|
+
# остаётся. Разбор — `docs/postmortems/2026-08-25-turn-ended-on-waiting.md`.
|
|
168
|
+
#
|
|
169
|
+
# Прежний признак спрашивал одно: была ли за ход работа. Ход, где разобран конфликт, сделаны
|
|
170
|
+
# коммит и пуш, а последним действием стал цикл до готовности прогона, проходил его целиком —
|
|
171
|
+
# работа была, и много. Именно эта полнота и обманывает: пустоты за таким ходом не видно.
|
|
172
|
+
wait_re='gh[[:space:]]+(run[[:space:]]+watch|pr[[:space:]]+checks[^|]*--watch)|until[[:space:]].*sleep|while[[:space:]].*sleep|^[[:space:]]*sleep[[:space:]]'
|
|
173
|
+
|
|
174
|
+
# Отдача работы и начало следующей. Правило зовёт законным концом хода отданную работу — но с
|
|
175
|
+
# условием: следующая начата, и по ней сделано ДЕЙСТВИЕ, а не сказано.
|
|
176
|
+
handover_re='gh[[:space:]]+pr[[:space:]]+create'
|
|
177
|
+
started_re='task:new|task:move|board\.mjs[[:space:]]+move|git[[:space:]]+checkout[[:space:]]+-b|git[[:space:]]+switch[[:space:]]+-c'
|
|
178
|
+
|
|
179
|
+
verdict="$(tail -n 400 "$transcript" 2>/dev/null | jq -s -r --arg work "$work_re" --arg read "$read_re" --arg part "$part_re" --arg wait "$wait_re" --arg handover "$handover_re" --arg started "$started_re" '
|
|
112
180
|
def is_input:
|
|
113
181
|
.type == "user"
|
|
114
182
|
and (((.message.content // []) | if type == "array"
|
|
@@ -122,7 +190,29 @@ verdict="$(tail -n 400 "$transcript" 2>/dev/null | jq -s -r --arg work "$work_re
|
|
|
122
190
|
| ($uses | map(.name // "") | any(test("^(Edit|Write|MultiEdit|NotebookEdit)$"))) as $edited
|
|
123
191
|
| ($uses | map(.name // "") | any(test("AskUserQuestion"))) as $asked
|
|
124
192
|
| ($uses | map((.input.command // "")) | join("\n")) as $ran
|
|
125
|
-
|
|
193
|
+
# Работой считается часть команды, совпавшая с образцом работы и не совпавшая с образцом
|
|
194
|
+
# разведки: переключение ветки и чтение истории тем же ходом работой не становятся.
|
|
195
|
+
| ([$ran | splits($part)] | map(test($work) and (test($read) | not)) | any) as $ran_work
|
|
196
|
+
# Последнее действие хода. Ожидание чужого шага концом хода не бывает, сколько бы работы ни
|
|
197
|
+
# было раньше: работа остаётся ровно там, где стояла.
|
|
198
|
+
| ([$uses[] | select((.name // "") == "Bash") | (.input.command // "")] | last // "") as $last
|
|
199
|
+
| ($last | test($wait)) as $waited
|
|
200
|
+
# Отдача работы: хвост хода после открытия заявки. Всё, что было до неё, сделано по сданной
|
|
201
|
+
# задаче и о следующей не говорит ничего.
|
|
202
|
+
| ([$uses[] | select((.name // "") == "Bash") | (.input.command // "")]) as $cmds
|
|
203
|
+
| (($cmds | map(test($handover)) | index(true))) as $handover_at
|
|
204
|
+
| ($handover_at != null) as $handed_over
|
|
205
|
+
| (if $handover_at == null then [] else $cmds[$handover_at:] end) as $tail
|
|
206
|
+
| (($tail | map(test($started)) | any)
|
|
207
|
+
or ($uses | map(.name // "") | any(test("^(Edit|Write|MultiEdit|NotebookEdit)$")))) as $started_next
|
|
208
|
+
# ПОСЛЕДНЕЕ ДЕЙСТВИЕ ХОДА — общий признак, из которого частные ярусы ниже только выводят
|
|
209
|
+
# понятный отказ. Девять разборов происшествий за сутки описывают девять разных остановок, и
|
|
210
|
+
# во всех девяти последним действием хода был текст владельцу: отчёт, сводка, объявление
|
|
211
|
+
# намерения. Ярус на каждый вид остановки — гонка без конца: видов столько, сколько бывает
|
|
212
|
+
# поводов заговорить. Признак поэтому один — работой должно быть ПОСЛЕДНЕЕ действие.
|
|
213
|
+
| ([$uses[] | (.name // "")] | last // "") as $last_tool
|
|
214
|
+
| (($last_tool | test("^(Edit|Write|MultiEdit|NotebookEdit)$"))
|
|
215
|
+
or ([$last | splits($part)] | map(test($work) and (test($read) | not)) | any)) as $ended_working
|
|
126
216
|
# Отказ гарда и передача захода — оба кончают ход по правилу.
|
|
127
217
|
| ([$turn[] | select(.type == "user") | .message.content // [] | select(type == "array") | .[]
|
|
128
218
|
| select(.type == "tool_result") | .content
|
|
@@ -136,17 +226,65 @@ verdict="$(tail -n 400 "$transcript" 2>/dev/null | jq -s -r --arg work "$work_re
|
|
|
136
226
|
| if type == "string" then . elif type == "array"
|
|
137
227
|
then (map(if type == "object" then (.text // "") else "" end) | join("\n")) else "" end] | join("\n")) as $said
|
|
138
228
|
| ($said | test("останов|стоп|хватит|подожди|не надо|прерв|отложи")) as $told_stop
|
|
139
|
-
| { worked: ($edited or $ran_work), released: ($asked or $denied or $handed or $told_stop), ran: $ran }
|
|
229
|
+
| { worked: ($edited or $ran_work), released: ($asked or $denied or $handed or $told_stop), waited: $waited, handed_over: $handed_over, started_next: $started_next, ended_working: $ended_working, ran: $ran }
|
|
140
230
|
' 2>/dev/null)"
|
|
141
231
|
|
|
142
232
|
[ -z "$verdict" ] && exit 0
|
|
143
233
|
|
|
144
234
|
worked="$(printf '%s' "$verdict" | jq -r '.worked // false' 2>/dev/null)"
|
|
235
|
+
waited="$(printf '%s' "$verdict" | jq -r '.waited // false' 2>/dev/null)"
|
|
236
|
+
handed_over="$(printf '%s' "$verdict" | jq -r '.handed_over // false' 2>/dev/null)"
|
|
237
|
+
started_next="$(printf '%s' "$verdict" | jq -r '.started_next // false' 2>/dev/null)"
|
|
238
|
+
ended_working="$(printf '%s' "$verdict" | jq -r '.ended_working // false' 2>/dev/null)"
|
|
145
239
|
released="$(printf '%s' "$verdict" | jq -r '.released // false' 2>/dev/null)"
|
|
146
240
|
commands="$(printf '%s' "$verdict" | jq -r '.ran // ""' 2>/dev/null)"
|
|
147
241
|
|
|
148
242
|
[ "$released" = "true" ] && exit 0
|
|
149
243
|
|
|
244
|
+
# Взятая, но не начатая работа. Ветка по номеру задачи заведена, а каталога задачи при ней нет
|
|
245
|
+
# вовсе — значит работа объявлена взятой и не начата ни одной строкой. Ход здесь не кончается, сколько бы
|
|
246
|
+
# работы в нём ни было: заведение ветки, перевод колонки и уборка соседних веток — всё это
|
|
247
|
+
# команды, меняющие дерево, и второй признак отпускает такой ход целиком.
|
|
248
|
+
#
|
|
249
|
+
# Ровно так ход и вставал: задача взята, номер назван владельцу, отчёт написан — и следующее
|
|
250
|
+
# действие состояния «задача-взята», написать замысел, не сделано. Отчёт выглядит работой лучше
|
|
251
|
+
# всякой другой, а страж, знающий только «была ли за ход работа», подтверждает это: работа была.
|
|
252
|
+
# Разбор — `docs/postmortems/2026-08-25-task-taken-and-turn-ended.md`.
|
|
253
|
+
#
|
|
254
|
+
# Папка, разобранная коммитом ветки, сюда не попадает: `archived` означает отданную работу, и её
|
|
255
|
+
# судит прежний ярус. Ветка без номера задачи не судится вовсе — под пробу заводят и такие.
|
|
256
|
+
if [ "$archived" != "true" ] && [ -z "$progress" ] && [ -n "$branch" ] && [ ! -d "$root/$tasks_dir/$branch" ]; then
|
|
257
|
+
task_key="${RT_TASK_KEY:-}"
|
|
258
|
+
if [ -z "$task_key" ] && [ -f "$root/.claude/rt-kit/checks.json" ]; then
|
|
259
|
+
task_key="$(jq -r '.board.taskKey // empty' "$root/.claude/rt-kit/checks.json" 2>/dev/null)"
|
|
260
|
+
fi
|
|
261
|
+
[ -z "$task_key" ] && task_key='[A-Za-z][A-Za-z0-9]*'
|
|
262
|
+
if printf '%s' "$branch" | grep -qE "^${task_key}-[0-9]+-" 2>/dev/null; then
|
|
263
|
+
reason="BLOCKED by turn-exit-guard: работа взята и не начата — ветка \`$branch\` заведена, а папки задачи при ней нет.
|
|
264
|
+
|
|
265
|
+
Заведённая ветка означает состояние «задача-взята», и обязательное действие у него одно — написать замысел. Ход, кончившийся здесь, оставляет работу объявленной и не начатой: номер назван, колонка сдвинута, а на диске нет ни разбора просьбы, ни этапов. Команды заведения ветки и перевода колонки этого не заменяют — ими такой ход и заполняется.
|
|
266
|
+
|
|
267
|
+
npm run task:new -- <номер> # если папки нет вовсе
|
|
268
|
+
|
|
269
|
+
Собери \`$tasks_dir/$branch/\` и напиши замысел этим же ходом.
|
|
270
|
+
|
|
271
|
+
Страж судит один ход: следующий заход не отбивается."
|
|
272
|
+
|
|
273
|
+
# shellcheck disable=SC1090
|
|
274
|
+
[ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" ] \
|
|
275
|
+
&& . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" 2>/dev/null
|
|
276
|
+
command -v rt_deny_tail >/dev/null 2>&1 || rt_deny_tail() { :; }
|
|
277
|
+
deny_tail_text="$(rt_deny_tail "")"
|
|
278
|
+
[ -n "$deny_tail_text" ] && reason="${reason}
|
|
279
|
+
|
|
280
|
+
${deny_tail_text}"
|
|
281
|
+
|
|
282
|
+
jq -n --arg r "$reason" '{decision:"block",reason:$r}' 2>/dev/null \
|
|
283
|
+
|| printf '{"decision":"block","reason":"turn-exit-guard: работа взята и не начата — напиши замысел."}\n'
|
|
284
|
+
exit 0
|
|
285
|
+
fi
|
|
286
|
+
fi
|
|
287
|
+
|
|
150
288
|
# Контракт этапа. Отметка «этап сделан» — утверждение о дереве, и подтверждается оно выводом
|
|
151
289
|
# команды, а не словами: этап, отмеченный по памяти, через заход неотличим от проверенного.
|
|
152
290
|
# Страж сравнивает номер этапа с тем, что лежит в истории ветки, и на выросшем номере требует
|
|
@@ -199,6 +337,93 @@ ${deny_tail_text}"
|
|
|
199
337
|
fi
|
|
200
338
|
fi
|
|
201
339
|
|
|
340
|
+
# Работа отдана, а следующая только названа. Работы в таком ходе больше, чем в любом другом, — и
|
|
341
|
+
# вся она по сданной задаче: отдача завершает прошлую работу, а не ход. Девять разборов
|
|
342
|
+
# происшествий за сутки описывают девять разных остановок, и во всех девяти последним действием
|
|
343
|
+
# хода был текст владельцу: отчёт, сводка, объявление намерения.
|
|
344
|
+
# Разбор — `docs/postmortems/2026-08-25-handover-turn-ends-on-intent.md`.
|
|
345
|
+
if [ "$handed_over" = "true" ] && [ "$started_next" != "true" ]; then
|
|
346
|
+
reason="BLOCKED by turn-exit-guard: заявка открыта, а по следующей работе за этот ход не сделано ничего.
|
|
347
|
+
|
|
348
|
+
Отданная работа кончает ход только вместе с начатой следующей — по ней должно быть сделано действие, а не сказано. «Беру такую-то» выходом не является: правило зовёт это объявлением намерения.
|
|
349
|
+
|
|
350
|
+
Всё, что было до открытия заявки, сделано по сданной задаче и о следующей не говорит ничего.
|
|
351
|
+
|
|
352
|
+
npm run task:new -- <заголовок> # завести следующую
|
|
353
|
+
git checkout -b <КЛЮЧ>-<номер>-<slug> # взять её в работу
|
|
354
|
+
|
|
355
|
+
Сделай первый шаг по следующей работе этим же ходом. Владелец сказал остановиться — так и напиши: страж читает его слово, а не пересказ.
|
|
356
|
+
|
|
357
|
+
Страж судит один ход: следующий заход не отбивается."
|
|
358
|
+
|
|
359
|
+
# shellcheck disable=SC1090
|
|
360
|
+
[ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" ] \
|
|
361
|
+
&& . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" 2>/dev/null
|
|
362
|
+
command -v rt_deny_tail >/dev/null 2>&1 || rt_deny_tail() { :; }
|
|
363
|
+
deny_tail_text="$(rt_deny_tail "")"
|
|
364
|
+
[ -n "$deny_tail_text" ] && reason="${reason}
|
|
365
|
+
|
|
366
|
+
${deny_tail_text}"
|
|
367
|
+
|
|
368
|
+
jq -n --arg r "$reason" '{decision:"block",reason:$r}' 2>/dev/null \
|
|
369
|
+
|| printf '{"decision":"block","reason":"turn-exit-guard: работа отдана, а следующая не начата."}\n'
|
|
370
|
+
exit 0
|
|
371
|
+
fi
|
|
372
|
+
|
|
373
|
+
# Ход кончился ожиданием чужого шага. Работа в нём была — тем он и обманчив: полон, и пустоты за
|
|
374
|
+
# ним не видно. Судится последнее действие, а не наличие работы.
|
|
375
|
+
if [ "$waited" = "true" ]; then
|
|
376
|
+
reason="BLOCKED by turn-exit-guard: последним действием хода стало ожидание чужого шага, а оно состоянием работы не бывает.
|
|
377
|
+
|
|
378
|
+
Прогон, разбор владельцем и слияние идут без исполнителя и от взгляда быстрее не становятся. Работы за ход могло быть много — она остаётся ровно там, где стояла, и владелец видит исполнителя стоящим.
|
|
379
|
+
|
|
380
|
+
Следующий шаг записан в ходе работы: ${next_step}
|
|
381
|
+
|
|
382
|
+
Сделай его этим же ходом либо возьми следующую задачу. Ожидание в середине хода законно — отбит именно конец.
|
|
383
|
+
|
|
384
|
+
Страж судит один ход: следующий заход не отбивается."
|
|
385
|
+
|
|
386
|
+
# shellcheck disable=SC1090
|
|
387
|
+
[ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" ] \
|
|
388
|
+
&& . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" 2>/dev/null
|
|
389
|
+
command -v rt_deny_tail >/dev/null 2>&1 || rt_deny_tail() { :; }
|
|
390
|
+
deny_tail_text="$(rt_deny_tail "")"
|
|
391
|
+
[ -n "$deny_tail_text" ] && reason="${reason}
|
|
392
|
+
|
|
393
|
+
${deny_tail_text}"
|
|
394
|
+
|
|
395
|
+
jq -n --arg r "$reason" '{decision:"block",reason:$r}' 2>/dev/null \
|
|
396
|
+
|| printf '{"decision":"block","reason":"turn-exit-guard: ход кончился ожиданием чужого шага."}\n'
|
|
397
|
+
exit 0
|
|
398
|
+
fi
|
|
399
|
+
|
|
400
|
+
# Общий рубеж. Работа за ход была — но последним действием стал не она, а текст владельцу.
|
|
401
|
+
# Частные ярусы выше называют вид остановки точнее; сюда доходит то, чего они не знают по имени.
|
|
402
|
+
if [ "$worked" = "true" ] && [ "$ended_working" != "true" ]; then
|
|
403
|
+
reason="BLOCKED by turn-exit-guard: работа за ход была, но последним действием хода стала не она.
|
|
404
|
+
|
|
405
|
+
Ход кончается работой, а не рассказом о ней. Отчёт, сводка и объявление намерения выходом не являются: они выглядят завершением тем убедительнее, чем больше сделано, — и ровно на это место встаёт следующее действие.
|
|
406
|
+
|
|
407
|
+
Следующий шаг записан в ходе работы: ${next_step}
|
|
408
|
+
|
|
409
|
+
Сделай его этим же ходом, а сказать о сделанном можно после. Владелец сказал остановиться — так и напиши: страж читает его слово, а не пересказ.
|
|
410
|
+
|
|
411
|
+
Страж судит один ход: следующий заход не отбивается."
|
|
412
|
+
|
|
413
|
+
# shellcheck disable=SC1090
|
|
414
|
+
[ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" ] \
|
|
415
|
+
&& . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" 2>/dev/null
|
|
416
|
+
command -v rt_deny_tail >/dev/null 2>&1 || rt_deny_tail() { :; }
|
|
417
|
+
deny_tail_text="$(rt_deny_tail "")"
|
|
418
|
+
[ -n "$deny_tail_text" ] && reason="${reason}
|
|
419
|
+
|
|
420
|
+
${deny_tail_text}"
|
|
421
|
+
|
|
422
|
+
jq -n --arg r "$reason" '{decision:"block",reason:$r}' 2>/dev/null \
|
|
423
|
+
|| printf '{"decision":"block","reason":"turn-exit-guard: последним действием хода стала не работа."}\n'
|
|
424
|
+
exit 0
|
|
425
|
+
fi
|
|
426
|
+
|
|
202
427
|
[ "$worked" = "true" ] && exit 0
|
|
203
428
|
|
|
204
429
|
if [ "$archived" = "true" ]; then
|