@rt-tools/agent-kit 0.16.1 → 0.17.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 (105) hide show
  1. package/assets/checks/board-paths.github.mjs +88 -0
  2. package/assets/checks/board-runs.github.mjs +8 -0
  3. package/assets/checks/board-titles.github.mjs +66 -0
  4. package/assets/checks/check-board.github.mjs +17 -4
  5. package/assets/checks/check-file-size.mjs +10 -2
  6. package/assets/checks/check-glossary.mjs +170 -0
  7. package/assets/checks/check-push-gate.mjs +59 -1
  8. package/assets/checks/check-schema-drift.mjs +12 -5
  9. package/assets/checks/spec-contract.mjs +9 -0
  10. package/assets/defaults/gate-map.sh +13 -4
  11. package/assets/defaults/project.sh +22 -3
  12. package/assets/docs/GLOSSARY.md +1 -1
  13. package/assets/hooks/browser-device-id.sh +20 -4
  14. package/assets/hooks/browser-guard-device-id.sh +5 -2
  15. package/assets/hooks/git-guard-delivery-folder.sh +19 -0
  16. package/assets/hooks/git-guard-delivery.sh +39 -0
  17. package/assets/hooks/git-guard-push-tests.sh +21 -1
  18. package/assets/hooks/grill-gate-ask.sh +25 -0
  19. package/assets/hooks/grill-gate.sh +12 -5
  20. package/assets/hooks/lint-after-edit.sh +44 -16
  21. package/assets/hooks/skill-gate-layers.sh +7 -0
  22. package/assets/hooks/skill-gate.sh +29 -0
  23. package/assets/hooks/task-context-load.sh +10 -0
  24. package/assets/hooks/task-flow-context.sh +185 -0
  25. package/assets/hooks/task-flow-draft-guard.sh +92 -0
  26. package/assets/hooks/task-flow-guard.sh +27 -159
  27. package/assets/hooks/turn-exit-guard.sh +8 -1
  28. package/assets/hooks/window-fill-guard.sh +5 -1
  29. package/assets/laws/delivery.md +19 -0
  30. package/assets/laws/frontend-application.md +4 -0
  31. package/assets/laws/verifiability.md +21 -0
  32. package/assets/laws/work-conduct.md +15 -0
  33. package/assets/patterns/browser-verification-stand.md +7 -2
  34. package/assets/patterns/doc-style-write.md +41 -1
  35. package/assets/patterns/git-workflow-commit.github.md +15 -2
  36. package/assets/patterns/git-workflow-docker.md +14 -0
  37. package/assets/patterns/git-workflow-merge.md +18 -0
  38. package/assets/patterns/git-workflow-migration.md +11 -0
  39. package/assets/patterns/git-workflow-pr.github.md +6 -1
  40. package/assets/patterns/git-workflow-secrets.md +14 -0
  41. package/assets/patterns/git-workflow-stack.md +63 -1
  42. package/assets/patterns/spec-driven-domain.md +34 -1
  43. package/assets/patterns/spec-driven-rule.md +11 -0
  44. package/assets/patterns/spec-driven-sweep.md +57 -0
  45. package/assets/patterns/task-flow-archive.md +46 -14
  46. package/assets/patterns/task-flow-close.md +78 -2
  47. package/assets/patterns/task-flow-resume.md +17 -5
  48. package/assets/patterns/task-flow-start.md +42 -4
  49. package/assets/pitfalls/agent-kit.md +73 -3
  50. package/assets/pitfalls/doc-style.md +5 -0
  51. package/assets/pitfalls/git-workflow.github.md +68 -0
  52. package/assets/pitfalls/spec-driven.md +16 -0
  53. package/assets/pitfalls/task-flow.md +93 -0
  54. package/assets/pitfalls/turn-conduct.md +14 -0
  55. package/assets/rules/browser-verification.md +39 -0
  56. package/assets/rules/deploy-flow.azure.md +8 -0
  57. package/assets/rules/deploy-flow.github.md +18 -0
  58. package/assets/rules/deploy-flow.gitlab.md +8 -0
  59. package/assets/rules/doc-style.md +14 -0
  60. package/assets/rules/git-workflow.azure.md +5 -0
  61. package/assets/rules/git-workflow.github.md +85 -75
  62. package/assets/rules/git-workflow.gitlab.md +5 -0
  63. package/assets/rules/reuse-first.md +8 -0
  64. package/assets/rules/shared-code.md +5 -0
  65. package/assets/rules/spec-driven.md +14 -0
  66. package/assets/rules/task-flow.md +112 -112
  67. package/assets/rules/testing.md +14 -1
  68. package/assets/rules/turn-conduct.md +40 -27
  69. package/assets/rules/turn-entry.md +6 -0
  70. package/assets/samples/tasks/_template/grill.md +5 -0
  71. package/assets/samples/tasks/_template/plan.md +3 -0
  72. package/assets/skills/agent-kit.md +99 -76
  73. package/assets/templates/postmortem.md +5 -1
  74. package/bin/agent-kit.d.ts.map +1 -1
  75. package/bin/agent-kit.js +30 -6
  76. package/bin/agent-kit.js.map +1 -1
  77. package/lib/catalog.d.ts.map +1 -1
  78. package/lib/catalog.js +2 -1
  79. package/lib/catalog.js.map +1 -1
  80. package/lib/commands.d.ts.map +1 -1
  81. package/lib/commands.js +96 -7
  82. package/lib/commands.js.map +1 -1
  83. package/lib/hooks-map.d.ts +26 -0
  84. package/lib/hooks-map.d.ts.map +1 -1
  85. package/lib/hooks-map.js +58 -2
  86. package/lib/hooks-map.js.map +1 -1
  87. package/lib/sections.d.ts +6 -0
  88. package/lib/sections.d.ts.map +1 -1
  89. package/lib/sections.js +19 -0
  90. package/lib/sections.js.map +1 -1
  91. package/lib/shipment.d.ts +2 -0
  92. package/lib/shipment.d.ts.map +1 -1
  93. package/lib/shipment.fixture.d.ts +5 -0
  94. package/lib/shipment.fixture.d.ts.map +1 -1
  95. package/lib/shipment.fixture.js +7 -0
  96. package/lib/shipment.fixture.js.map +1 -1
  97. package/lib/shipment.js +13 -1
  98. package/lib/shipment.js.map +1 -1
  99. package/lib/sync.d.ts +35 -3
  100. package/lib/sync.d.ts.map +1 -1
  101. package/lib/sync.js +59 -8
  102. package/lib/sync.js.map +1 -1
  103. package/package.json +1 -1
  104. package/rt-tools-agent-kit-0.17.0.tgz +0 -0
  105. package/rt-tools-agent-kit-0.16.1.tgz +0 -0
@@ -0,0 +1,185 @@
1
+ #!/usr/bin/env bash
2
+ # Общий разбор для гардов хода работы. НЕ гард: объявления `rt-hook:` у него нет, к событиям
3
+ # агента он не подключается. Его источают сами гарды — тем же приёмом, каким они источают общий
4
+ # хвост отказа.
5
+ #
6
+ # Зачем он есть. Требования к ходу работы стоят двумя гардами — папка задачи с замыслом и
7
+ # состоянием отдельно, договорённость о продукте отдельно, — а разбор у обоих один и тот же:
8
+ # какой путь пишет вызов, код ли это приложения, в какой ветке идёт правка и где лежит папка
9
+ # задачи. Разложенный вторым разом, этот разбор расходится молча: правка одного гарда чинит
10
+ # половину случаев, и видно это только там, где второй промолчал.
11
+ #
12
+ # ЧТО ОН ДЕЛАЕТ. Читает ввод, поднимает профиль дерева, вынимает из вызова пути, отбирает среди
13
+ # них первый путь кода приложения, переходит в рабочий каталог правки и называет ветку, корень,
14
+ # каталог папок задач, саму папку и замысел в ней.
15
+ #
16
+ # ЧЕГО ОН НЕ ДЕЛАЕТ. Он ничего не судит и ничего не отбивает: имя ветки, наличие папки, состояние
17
+ # работы и договорённость — дело самих гардов, и отказ печатает тот, чьё это требование.
18
+ #
19
+ # FAIL-OPEN: нет jq, не git-репозиторий, битый ввод, чужой инструмент, нет функции профиля →
20
+ # ответ «судить нечего». Сломанный разбор не должен мешать работать.
21
+
22
+ # Разбор вызова. Возвращает 0 и ставит переменные, если правка касается кода приложения в ветке
23
+ # с историей; иначе — ненулевой код, и гард выходит молча.
24
+ #
25
+ # RT_TF_PATH — путь кода приложения, из-за которого гард вообще судит
26
+ # RT_TF_BRANCH — текущая ветка рабочего каталога правки
27
+ # RT_TF_ROOT — корень рабочего дерева
28
+ # RT_TF_TASKS_DIR — каталог папок задач, как он назван в дереве
29
+ # RT_TF_MAIN_BRANCH — главная ветка дерева
30
+ # RT_TF_DIR — папка этой задачи
31
+ # RT_TF_PLAN — замысел в ней
32
+ # Разложенный слой правил судится наравне с кодом приложения. Путями кода он не покрыт нигде —
33
+ # лежит в каталоге законов, в каталоге агента и среди проверок, — и полторы сотни его файлов
34
+ # легли без единого отклика гарда; отбил он двумя ходами позже, на записи в папку задачи. Признак
35
+ # тот же, по которому слой находит гейт правил: шапка раскладки в начале файла. Ресурс пакета, из
36
+ # которого раскладка идёт, шапки не несёт и судится по-прежнему своим путём.
37
+ rt_tf_laid_out() {
38
+ [ -f "$1" ] || return 1
39
+ head -n 3 "$1" 2>/dev/null | grep -q 'rt-kit v[^[:space:]]* ·'
40
+ }
41
+
42
+ rt_task_flow_context() {
43
+ rt_tf_hooks_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
44
+
45
+ # shellcheck disable=SC1090
46
+ . "$rt_tf_hooks_dir/utf8.sh" 2>/dev/null || true
47
+ # shellcheck disable=SC1090
48
+ . "$rt_tf_hooks_dir/hook-input.sh" 2>/dev/null || true
49
+
50
+ command -v rt_hook_read >/dev/null 2>&1 || return 1
51
+ rt_hook_read
52
+ [ -z "$RT_HOOK_INPUT" ] && return 1
53
+ command -v jq >/dev/null 2>&1 || return 1
54
+
55
+ # Профиль дерева: сперва умолчание пакета, поверх него — надстройка проекта, если она есть.
56
+ # Читается до разбора пути: пути из команды оболочки вынимает как раз профиль.
57
+ for rt_tf_profile in \
58
+ "$rt_tf_hooks_dir/../rt-kit/defaults/project.sh" \
59
+ "$rt_tf_hooks_dir/../defaults/project.sh" \
60
+ "${CLAUDE_PROJECT_DIR:-.}/.claude/rt-kit/defaults/project.sh" \
61
+ "${CLAUDE_PROJECT_DIR:-.}/.claude/rt-kit/project.sh"; do
62
+ # shellcheck disable=SC1090
63
+ [ -f "$rt_tf_profile" ] && . "$rt_tf_profile" 2>/dev/null
64
+ done
65
+
66
+ # Слово о нехватке функции профиля: хук, вышедший молча, неотличим от работающего. Файл может
67
+ # быть не разложен — тогда остаётся прежнее поведение, молчаливое.
68
+ # shellcheck disable=SC1090
69
+ [ -f "$rt_tf_hooks_dir/profile-check.sh" ] && . "$rt_tf_hooks_dir/profile-check.sh"
70
+ command -v rt_needs >/dev/null 2>&1 || rt_needs() { command -v "$1" >/dev/null 2>&1; }
71
+
72
+ rt_tf_tool="$(rt_hook_tool)"
73
+ rt_tf_candidates=""
74
+ case "$rt_tf_tool" in
75
+ # Инструмент редактора заводит файл теми же двумя данными, только называет их иначе —
76
+ # без этой ветки правка шла бы мимо гарда сменой инструмента.
77
+ Edit | Write | MultiEdit | mcp__webstorm__create_new_file)
78
+ rt_tf_candidates="$(printf '%s' "$RT_HOOK_INPUT" | jq -r '.tool_input.file_path // .tool_input.pathInProject // empty' 2>/dev/null)"
79
+ ;;
80
+ # Второй ярус: та же правка, положенная командой оболочки. Без него отказ гарда обходится
81
+ # сменой не инструмента, а способа записи — перенаправлением, `sed -i`, интерпретатором с
82
+ # heredoc.
83
+ #
84
+ # Терминал среды исполняет ту же командную строку и кладёт её в то же поле: без этих двух
85
+ # имён гард стоял бы объявленным на них и молча пропускал — состояние хуже необъявленного,
86
+ # потому что снаружи выглядит закрытым.
87
+ Bash | mcp__webstorm__execute_terminal_command | mcp__webstorm__execute_tool)
88
+ rt_tf_cmd="$(rt_hook_cmd)"
89
+ [ -z "$rt_tf_cmd" ] && return 1
90
+ # Универсальный исполнитель прячет настоящую команду во вложенной строке: без её
91
+ # разбора путь стоит за кавычкой, и до него не дотягивается ни один образец.
92
+ if [ "$rt_tf_tool" = "mcp__webstorm__execute_tool" ] && command -v perl >/dev/null 2>&1; then
93
+ rt_tf_inner="$(printf '%s' "$rt_tf_cmd" | perl -0ne '
94
+ if (/--command(?:=|\s+)(?:"((?:[^"\\]|\\.)*)"|\x27([^\x27]*)\x27|(.+))/s) {
95
+ print defined $1 ? $1 : (defined $2 ? $2 : $3);
96
+ }
97
+ ' 2>/dev/null)"
98
+ [ -n "$rt_tf_inner" ] && rt_tf_cmd="$rt_tf_inner"
99
+ fi
100
+ rt_needs rt_shell_writes task-flow-guard || return 1
101
+ rt_needs rt_shell_paths task-flow-guard || return 1
102
+ rt_shell_writes "$rt_tf_cmd" || return 1
103
+ rt_tf_candidates="$(rt_shell_paths "$rt_tf_cmd")"
104
+ ;;
105
+ *) return 1 ;;
106
+ esac
107
+ [ -z "$rt_tf_candidates" ] && return 1
108
+
109
+ # Признак «правка меняет поведение» — путь, а не оценка на глаз: оценку назначает тот, кому
110
+ # она мешает, и порог плывёт. Где живёт код приложения, знает профиль: правила, тексты,
111
+ # обвязка и зависимости под требование не попадают — иначе разбор задачи нельзя было бы
112
+ # вести до заведения ветки.
113
+ rt_needs rt_is_app_code task-flow-guard || return 1
114
+
115
+ # Удаление отличается от записи одним: снимаемого может не быть в истории вовсе. Свой
116
+ # временный каталог под корнем приложений правкой продукта не бывает — снимать его,
117
+ # восстанавливая ради этого замысел на диске, значит исполнять требование, написанное про
118
+ # другое действие. Отслеживаемый путь судится по-прежнему: снятый файл кода меняет поведение
119
+ # так же, как переписанный.
120
+ rt_tf_removes=0
121
+ case "${rt_tf_cmd:-}" in
122
+ *"rm "*) rt_tf_removes=1 ;;
123
+ esac
124
+ # Каталог правки: ветка и история смотрятся ниже, а спросить историю нужно уже здесь.
125
+ rt_tf_askdir="$(rt_hook_cwd)"
126
+ [ -z "$rt_tf_askdir" ] && rt_tf_askdir="${CLAUDE_PROJECT_DIR:-.}"
127
+
128
+ # Судится каждый названный путь: команда пишет столько файлов, сколько в ней стоит, и одного
129
+ # под требованием довольно, чтобы отбить её целиком.
130
+ RT_TF_PATH=""
131
+ while IFS= read -r rt_tf_candidate; do
132
+ [ -z "$rt_tf_candidate" ] && continue
133
+ # Снятие того, чего в истории нет, — не правка продукта, а уборка за собой. Спрашивается
134
+ # путь, как он назван в команде: приклеенный корень уводит вопрос в чужое дерево.
135
+ if [ "$rt_tf_removes" = 1 ] && git -C "$rt_tf_askdir" rev-parse --is-inside-work-tree >/dev/null 2>&1 &&
136
+ ! git -C "$rt_tf_askdir" ls-files --error-unmatch -- "$rt_tf_candidate" >/dev/null 2>&1; then
137
+ continue
138
+ fi
139
+ case "$rt_tf_candidate" in
140
+ /*) ;;
141
+ *) rt_tf_candidate="${CLAUDE_PROJECT_DIR:-.}/$rt_tf_candidate" ;;
142
+ esac
143
+ if rt_is_app_code "$rt_tf_candidate" || rt_tf_laid_out "$rt_tf_candidate"; then
144
+ RT_TF_PATH="$rt_tf_candidate"
145
+ break
146
+ fi
147
+ done <<EOF
148
+ $rt_tf_candidates
149
+ EOF
150
+ [ -z "$RT_TF_PATH" ] && return 1
151
+
152
+ # Ветку смотрим там, где пойдёт правка: у worktree она своя.
153
+ rt_tf_workdir="$(rt_hook_cwd)"
154
+ [ -z "$rt_tf_workdir" ] && rt_tf_workdir="${CLAUDE_PROJECT_DIR:-.}"
155
+ cd "$rt_tf_workdir" 2>/dev/null || return 1
156
+ git rev-parse --is-inside-work-tree >/dev/null 2>&1 || return 1
157
+
158
+ RT_TF_BRANCH="$(git branch --show-current 2>/dev/null)"
159
+ [ -z "$RT_TF_BRANCH" ] && return 1 # detached HEAD — не про наш случай
160
+
161
+ RT_TF_ROOT="$(git rev-parse --show-toplevel 2>/dev/null)"
162
+ [ -z "$RT_TF_ROOT" ] && return 1
163
+
164
+ # Каталог папок задач: у дерева он свой, но имя обычно общее.
165
+ RT_TF_TASKS_DIR="${RT_TASKS_DIR:-docs/tasks}"
166
+ RT_TF_MAIN_BRANCH="${RT_MAIN_BRANCH:-main}"
167
+ RT_TF_DIR="$RT_TF_ROOT/$RT_TF_TASKS_DIR/$RT_TF_BRANCH"
168
+ RT_TF_PLAN="$RT_TF_DIR/plan.md"
169
+
170
+ return 0
171
+ }
172
+
173
+ # Отказ гарда хода работы: причина первым параметром, законная форма обхода — вторым. Хвост
174
+ # дописывается здесь, а не в каждом тексте: пропущенный в одном месте, он читается как «у этого
175
+ # отказа ходов нет». Хвост может быть не разложен — тогда его нет, а причина остаётся прежней.
176
+ rt_task_flow_deny() {
177
+ rt_tf_reason="$1"
178
+ if command -v rt_deny_tail >/dev/null 2>&1; then
179
+ rt_tf_tail="$(rt_deny_tail "$2")"
180
+ [ -n "$rt_tf_tail" ] && rt_tf_reason="$1 ${rt_tf_tail}"
181
+ fi
182
+ jq -n --arg r "$rt_tf_reason" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$r}}' 2>/dev/null \
183
+ || printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"%s"}}\n' "$rt_tf_reason"
184
+ exit 0
185
+ }
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env bash
2
+ # rt-hook: PreToolUse Edit|Write|MultiEdit|Bash|mcp__webstorm__create_new_file|mcp__webstorm__execute_terminal_command|mcp__webstorm__execute_tool
3
+ # Требует: hooks/task-flow-context.sh, hooks/task-flow-guard.sh, hooks/deny-tail.sh
4
+ # PreToolUse guard for Edit|Write|MultiEdit: код не пишется раньше договорённости о продукте.
5
+ #
6
+ # Замысел говорит, как работа будет сделана, и молчит о том, о чём договорились с владельцем.
7
+ # Договорённость о продукте — единственный текст, который называет обещанное поведение до кода;
8
+ # написанная после, она пересказывает состоявшееся решение, а не проверяет его.
9
+ #
10
+ # Гард требует одно и ровно одно: замысел называет договорённость строкой `**Драфт:** \`путь\``,
11
+ # и названное существует. Папку задачи, сам замысел и объявленное состояние требует
12
+ # `task-flow-guard`, и до его отказов этот гард не судит ничего: замысла нет на диске — молчит.
13
+ #
14
+ # Требования разведены затем, чтобы дерево могло отказаться от одного, сохранив второе. Пока
15
+ # оба ехали одним файлом, отказ от требования договорённости снимал заодно требование папки
16
+ # задачи и замысла — то есть всё, ради чего гард заводился.
17
+ #
18
+ # Осознанный выход есть: строка `**Поведение:** не меняется — <причина>` в замысле снимает
19
+ # требование договорённости. Пустая причина не принимается, как и у `Docs-skip:`.
20
+ #
21
+ # Правило целиком — скил `task-flow`.
22
+ #
23
+ # FAIL-OPEN: нет jq, не git-репозиторий, битый ввод, чужой инструмент → пропуск. Сломанный
24
+ # гард не должен мешать работать.
25
+
26
+ rt_hooks_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
27
+
28
+ # Разбор вызова — общий у обоих гардов хода работы. Файл может быть не разложен: тогда судить
29
+ # нечем, и гард молчит.
30
+ # shellcheck disable=SC1090
31
+ [ -f "$rt_hooks_dir/task-flow-context.sh" ] && . "$rt_hooks_dir/task-flow-context.sh"
32
+ command -v rt_task_flow_context >/dev/null 2>&1 || exit 0
33
+
34
+ # Общий хвост отказа: два законных хода и законная форма обхода, если она у отказа есть. Файл
35
+ # может быть не разложен — тогда хвоста нет, а причина отказа остаётся прежней.
36
+ # shellcheck disable=SC1090
37
+ [ -f "$rt_hooks_dir/deny-tail.sh" ] && . "$rt_hooks_dir/deny-tail.sh"
38
+
39
+ rt_task_flow_context || exit 0
40
+
41
+ branch="$RT_TF_BRANCH"
42
+ root="$RT_TF_ROOT"
43
+ tasks_dir="$RT_TF_TASKS_DIR"
44
+ plan="$RT_TF_PLAN"
45
+
46
+ deny() { rt_task_flow_deny "$@"; }
47
+
48
+ # Замысла нет — это чужое требование, и отказ о нём печатает гард хода работы. Отсюда же уходит
49
+ # и работа с разобранной папкой: замысел с диска к этой минуте снят намеренно.
50
+ [ -f "$plan" ] || exit 0
51
+
52
+ # Строка обхода: поведение не меняется, договорённость о продукте не нужна. Причина обязана
53
+ # стоять — без неё обход становится умолчанием.
54
+ if grep -qE '^\*\*Поведение:\*\*[[:space:]]*не меняется[[:space:]]*—[[:space:]]*\S' "$plan" 2>/dev/null; then
55
+ exit 0
56
+ fi
57
+
58
+ # Договорённость называется одним из двух видов. Первый — отдельный документ в каталоге
59
+ # «предложено»: он переезжает в спек домена, когда работа закрывается. Второй — сам спек домена:
60
+ # дерево, у которого отдельного каталога нет, пишет договорённость прямо в него, и переезжать там
61
+ # нечему — черновик совпадает со спеком с первого дня. Требовать один вид значит навязывать способ
62
+ # записи договорённостей вместе с проверкой того, что работа идёт по замыслу: дерево со вторым
63
+ # способом отказывается от гарда целиком и остаётся без единственной машинной проверки под правило.
64
+ draft="$(sed -n 's/^\*\*Драфт:\*\*[[:space:]]*`\([^`]*\)`.*/\1/p' "$plan" 2>/dev/null | head -1)"
65
+ [ -z "$draft" ] && draft="$(sed -n 's/^\*\*Спек:\*\*[[:space:]]*`\([^`]*\)`.*/\1/p' "$plan" 2>/dev/null | head -1)"
66
+
67
+ if [ -z "$draft" ]; then
68
+ deny "BLOCKED by task-flow: в '${tasks_dir}/${branch}/plan.md' не названа договорённость о продукте. Назови её одной из двух строк: '**Драфт:** \`путь\`' — отдельный документ в docs/specs/<домен>/proposed/<фича>/, либо '**Спек:** \`путь\`' — спек домена, в который договорённость пишется прямо. Правило — скил task-flow." \
69
+ "строка '**Поведение:** не меняется — <причина владельца>' в замысле; пустая причина не принимается"
70
+ fi
71
+
72
+ case "$draft" in
73
+ /*) draft_path="$draft" ;;
74
+ *) draft_path="$root/$draft" ;;
75
+ esac
76
+
77
+ if [ -e "$draft_path" ]; then
78
+ exit 0
79
+ fi
80
+
81
+ # Договорённость, влитая в спек домена, с диска уходит — так и задумано: в главной ветке
82
+ # директории «предложено» быть не должно. Но замысел на неё ссылается до конца работы, и без
83
+ # этой развилки последний коммит PR запирал бы ветку: ни правки по замечаниям разбора, ни
84
+ # записи в журнал изменений после вливания уже не сделать.
85
+ #
86
+ # Влитое от незаведённого отличает история ветки: путь, которого в ней никогда не было,
87
+ # договорённостью не был. Спросить об этом нечем, кроме git, поэтому нет git — отказ остаётся.
88
+ if git -C "$root" log --oneline -1 -- "$draft" 2>/dev/null | grep -q .; then
89
+ exit 0
90
+ fi
91
+
92
+ deny "BLOCKED by task-flow: замысел называет договорённость '${draft}', а её на диске нет и в истории ветки не было. Заведи её с образца (docs/specs/_template) или поправь путь в '${tasks_dir}/${branch}/plan.md'. Правило — скил task-flow."
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bash
2
2
  # rt-hook: PreToolUse Edit|Write|MultiEdit|Bash|mcp__webstorm__create_new_file|mcp__webstorm__execute_terminal_command|mcp__webstorm__execute_tool
3
- # Требует: hooks/profile-check.sh, hooks/deny-tail.sh
3
+ # Требует: hooks/task-flow-context.sh, hooks/profile-check.sh, hooks/deny-tail.sh
4
4
  # PreToolUse guard for Edit|Write|MultiEdit: код не пишется раньше замысла.
5
5
  #
6
6
  # Работа идёт много заходов, и между ними исполнитель не помнит ничего. Замысел, лежащий на
@@ -8,145 +8,48 @@
8
8
  # закоммичены, PR не открыт, а очередь работ показывает задачу начатой и молчит о том, что
9
9
  # внутри неё сделано.
10
10
  #
11
- # Гард требует четыре вещи и ровно их: папку задачи по имени ветки, замысел в ней, объявленное
12
- # в ходе работы состояние — из тех, в которых код правится, и названную в замысле
13
- # договорённость о продукте. Полноту написанного он не судит — это за владельцем (решения в
14
- # законе `docs/constitution/work-conduct.md`).
11
+ # Гард требует три вещи и ровно их: папку задачи по имени ветки, замысел в ней и объявленное в
12
+ # ходе работы состояние — из тех, в которых код правится. Полноту написанного он не судит — это
13
+ # за владельцем (решения в законе `docs/constitution/work-conduct.md`).
15
14
  #
16
- # Состояние судится раньше договорённости и её обхода: обход снимает требование договорённости,
17
- # а не требование дойти до правки кода. Судится объявленный переход, а не наличие файлов —
18
- # артефакт, положенный ради снятия отказа, лежит так же, как написанный.
15
+ # Договорённость о продукте требует свой гард `task-flow-draft-guard`, объявленный на те же
16
+ # события. Разведены они затем, чтобы дерево могло отказаться от одного требования, сохранив
17
+ # второе: пока оба ехали одним файлом, отказ от требования договорённости снимал заодно и
18
+ # требование папки задачи, а держать их вместе дерево не просило никогда.
19
19
  #
20
20
  # Правило целиком — скил `task-flow`.
21
21
  #
22
- # Осознанный выход есть: строка `**Поведение:** не меняется — <причина>` в замысле снимает
23
- # требование договорённости. Пустая причина не принимается, как и у `Docs-skip:`.
24
- #
25
22
  # FAIL-OPEN: нет jq, не git-репозиторий, битый ввод, чужой инструмент → пропуск. Сломанный
26
23
  # гард не должен мешать работать.
27
24
 
28
- . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
29
- . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
30
-
31
- rt_hook_read
32
- input="$RT_HOOK_INPUT"
33
- [ -z "$input" ] && exit 0
34
- command -v jq >/dev/null 2>&1 || exit 0
35
-
36
- # Профиль дерева: сперва умолчание пакета, поверх него — надстройка проекта, если она есть.
37
- # Читается до разбора пути: пути из команды оболочки вынимает как раз профиль.
38
25
  rt_hooks_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
39
- for profile in "$rt_hooks_dir/../rt-kit/defaults/project.sh" "$rt_hooks_dir/../defaults/project.sh" "${CLAUDE_PROJECT_DIR:-.}/.claude/rt-kit/defaults/project.sh" "${CLAUDE_PROJECT_DIR:-.}/.claude/rt-kit/project.sh"; do
40
- # shellcheck disable=SC1090
41
- [ -f "$profile" ] && . "$profile" 2>/dev/null
42
- done
43
26
 
44
- # Слово о нехватке функции профиля: хук, вышедший молча, неотличим от работающего. Файл может
45
- # быть не разложен — тогда остаётся прежнее поведение, молчаливое.
27
+ # Разбор вызова общий у обоих гардов хода работы. Файл может быть не разложен: тогда судить
28
+ # нечем, и гард молчит.
46
29
  # shellcheck disable=SC1090
47
- [ -f "$rt_hooks_dir/profile-check.sh" ] && . "$rt_hooks_dir/profile-check.sh"
48
- command -v rt_needs >/dev/null 2>&1 || rt_needs() { command -v "$1" >/dev/null 2>&1; }
49
-
50
- tool="$(rt_hook_tool)"
51
- candidates=""
52
- case "$tool" in
53
- # Инструмент редактора заводит файл теми же двумя данными, только называет их иначе —
54
- # без этой ветки правка шла бы мимо гарда сменой инструмента.
55
- Edit | Write | MultiEdit | mcp__webstorm__create_new_file)
56
- candidates="$(printf '%s' "$input" | jq -r '.tool_input.file_path // .tool_input.pathInProject // empty' 2>/dev/null)"
57
- ;;
58
- # Второй ярус: та же правка, положенная командой оболочки. Без него отказ гарда обходится
59
- # сменой не инструмента, а способа записи — перенаправлением, `sed -i`, интерпретатором с
60
- # heredoc. Разбор — `2026-08-15-guard-denied-shell-wrote-anyway.md`.
61
- #
62
- # Терминал среды исполняет ту же командную строку и кладёт её в то же поле: без этих двух
63
- # имён гард стоял бы объявленным на них и молча пропускал — состояние хуже необъявленного,
64
- # потому что снаружи выглядит закрытым.
65
- Bash | mcp__webstorm__execute_terminal_command | mcp__webstorm__execute_tool)
66
- cmd="$(rt_hook_cmd)"
67
- [ -z "$cmd" ] && exit 0
68
- # Универсальный исполнитель прячет настоящую команду во вложенной строке: без её разбора
69
- # путь стоит за кавычкой, и до него не дотягивается ни один образец.
70
- if [ "$tool" = "mcp__webstorm__execute_tool" ] && command -v perl >/dev/null 2>&1; then
71
- inner="$(printf '%s' "$cmd" | perl -0ne '
72
- if (/--command(?:=|\s+)(?:"((?:[^"\\]|\\.)*)"|\x27([^\x27]*)\x27|(.+))/s) {
73
- print defined $1 ? $1 : (defined $2 ? $2 : $3);
74
- }
75
- ' 2>/dev/null)"
76
- [ -n "$inner" ] && cmd="$inner"
77
- fi
78
- rt_needs rt_shell_writes task-flow-guard || exit 0
79
- rt_needs rt_shell_paths task-flow-guard || exit 0
80
- rt_shell_writes "$cmd" || exit 0
81
- candidates="$(rt_shell_paths "$cmd")"
82
- ;;
83
- *) exit 0 ;;
84
- esac
85
- [ -z "$candidates" ] && exit 0
86
-
87
- # Признак «правка меняет поведение» — путь, а не оценка на глаз: оценку назначает тот, кому
88
- # она мешает, и порог плывёт. Где живёт код приложения, знает профиль: правила, тексты, обвязка
89
- # и зависимости под требование не попадают — иначе разбор задачи нельзя было бы вести до
90
- # заведения ветки.
91
- rt_needs rt_is_app_code task-flow-guard || exit 0
92
-
93
- # Судится каждый названный путь: команда пишет столько файлов, сколько в ней стоит, и одного
94
- # под требованием довольно, чтобы отбить её целиком.
95
- path=""
96
- while IFS= read -r candidate; do
97
- [ -z "$candidate" ] && continue
98
- case "$candidate" in
99
- /*) ;;
100
- *) candidate="${CLAUDE_PROJECT_DIR:-.}/$candidate" ;;
101
- esac
102
- if rt_is_app_code "$candidate"; then
103
- path="$candidate"
104
- break
105
- fi
106
- done <<EOF
107
- $candidates
108
- EOF
109
- [ -z "$path" ] && exit 0
110
-
111
- # Каталог папок задач: у дерева он свой, но имя обычно общее.
112
- tasks_dir="${RT_TASKS_DIR:-docs/tasks}"
113
- main_branch="${RT_MAIN_BRANCH:-main}"
30
+ [ -f "$rt_hooks_dir/task-flow-context.sh" ] && . "$rt_hooks_dir/task-flow-context.sh"
31
+ command -v rt_task_flow_context >/dev/null 2>&1 || exit 0
114
32
 
115
33
  # Общий хвост отказа: два законных хода и законная форма обхода, если она у отказа есть. Файл
116
34
  # может быть не разложен — тогда хвоста нет, а причина отказа остаётся прежней.
117
35
  # shellcheck disable=SC1090
118
36
  [ -f "$rt_hooks_dir/deny-tail.sh" ] && . "$rt_hooks_dir/deny-tail.sh"
119
- command -v rt_deny_tail >/dev/null 2>&1 || rt_deny_tail() { :; }
120
37
 
121
- # Отказ: причина первым параметром, законная форма обхода — вторым. Хвост дописывается здесь, а
122
- # не в каждом тексте: пропущенный в одном месте, он читается как «у этого отказа ходов нет».
123
- deny() {
124
- reason="$1"
125
- tail_text="$(rt_deny_tail "$2")"
126
- [ -n "$tail_text" ] && reason="$1 ${tail_text}"
127
- jq -n --arg r "$reason" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$r}}' 2>/dev/null \
128
- || printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"%s"}}\n' "$reason"
129
- exit 0
130
- }
38
+ rt_task_flow_context || exit 0
131
39
 
132
- # Ветку смотрим там же, где пойдёт правка: у worktree она своя.
133
- workdir="$(rt_hook_cwd)"
134
- [ -z "$workdir" ] && workdir="${CLAUDE_PROJECT_DIR:-.}"
135
- cd "$workdir" 2>/dev/null || exit 0
136
- git rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0
40
+ branch="$RT_TF_BRANCH"
41
+ root="$RT_TF_ROOT"
42
+ tasks_dir="$RT_TF_TASKS_DIR"
43
+ main_branch="$RT_TF_MAIN_BRANCH"
44
+ dir="$RT_TF_DIR"
45
+ plan="$RT_TF_PLAN"
137
46
 
138
- branch="$(git branch --show-current 2>/dev/null)"
139
- [ -z "$branch" ] && exit 0 # detached HEAD — не про наш случай
47
+ deny() { rt_task_flow_deny "$@"; }
140
48
 
141
49
  if rt_needs rt_task_branch_ok task-flow-guard && ! rt_task_branch_ok "$branch"; then
142
50
  deny "BLOCKED by task-flow: правка кода идёт в ветке под задачу, а текущая ветка — '${branch}'. Заведи задачу (npm run task:new -- --title '…' --slug <slug>) и ветку под её номером, затем повтори. Правило — скил task-flow."
143
51
  fi
144
52
 
145
- root="$(git rev-parse --show-toplevel 2>/dev/null)"
146
- [ -z "$root" ] && exit 0
147
- dir="$root/$tasks_dir/$branch"
148
- plan="$dir/plan.md"
149
-
150
53
  # Папка, разобранная коммитом этой ветки, — признак того, что работа отдана. Замысел с диска к
151
54
  # этой минуте снят намеренно: уборка стоит до открытия заявки, потому что кнопку слияния
152
55
  # нажимает человек на хостинге и закрывающему коммиту места после одобрения не остаётся.
@@ -154,11 +57,11 @@ plan="$dir/plan.md"
154
57
  # бы запирать ветку собственным порядком. Признак берётся из истории ветки, а не с диска:
155
58
  # снесённая, но не закоммиченная папка отданной работы не означает.
156
59
  folder_archived() {
157
- [ -n "$(git ls-tree -d --name-only HEAD -- "$tasks_dir/$branch" 2>/dev/null | head -1)" ] && return 1
158
- base="$(git merge-base "$main_branch" HEAD 2>/dev/null)"
60
+ [ -n "$(git -C "$root" ls-tree -d --name-only HEAD -- "$tasks_dir/$branch" 2>/dev/null | head -1)" ] && return 1
61
+ base="$(git -C "$root" merge-base "$main_branch" HEAD 2>/dev/null)"
159
62
  [ -z "$base" ] && return 1
160
- had="$(git ls-tree -d --name-only "$base" -- "$tasks_dir/$branch" 2>/dev/null | head -1)"
161
- [ -z "$had" ] && had="$(git log "$base..HEAD" --diff-filter=A --name-only --pretty=format: -- "$tasks_dir/$branch" 2>/dev/null | head -1)"
63
+ had="$(git -C "$root" ls-tree -d --name-only "$base" -- "$tasks_dir/$branch" 2>/dev/null | head -1)"
64
+ [ -z "$had" ] && had="$(git -C "$root" log "$base..HEAD" --diff-filter=A --name-only --pretty=format: -- "$tasks_dir/$branch" 2>/dev/null | head -1)"
162
65
  [ -n "$had" ]
163
66
  }
164
67
 
@@ -215,7 +118,7 @@ case "$state" in
215
118
  ;;
216
119
  esac
217
120
 
218
- # Папка задачи едет в ветку коммитом, а не живёт в одном рабочем дереве. Четыре требования выше
121
+ # Папка задачи едет в ветку коммитом, а не живёт в одном рабочем дереве. Три требования выше
219
122
  # смотрят диск, и папка, ни разу не закоммиченная, проходит их все без единого отказа — а
220
123
  # признак отданной работы гард берёт из истории, и там её нет. Отказ приходит в последней точке,
221
124
  # на открытии заявки, когда папка уже разобрана своими руками: чинить нечего, замысел снят, и
@@ -228,44 +131,9 @@ esac
228
131
  #
229
132
  # Спрашивается та же история, что и у признака отданной работы: папка стоит в `HEAD` либо её
230
133
  # добавлял коммит ветки. Нет 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)"
134
+ if [ -n "$(git -C "$root" rev-parse --verify HEAD 2>/dev/null)" ]; then
135
+ in_tree="$(git -C "$root" ls-tree -d --name-only HEAD -- "$tasks_dir/$branch" 2>/dev/null | head -1)"
233
136
  if [ -z "$in_tree" ]; then
234
137
  deny "BLOCKED by task-flow: папка задачи '${tasks_dir}/${branch}' лежит в рабочем дереве, а в историю ветки не заведена. Заведи её коммитом (git add ${tasks_dir}/${branch} && git commit), затем повтори: признак отданной работы гард берёт из истории, и с некоммиченной папкой отказ придёт на открытии заявки — когда папка уже разобрана и чинить нечего. Правило — скил task-flow."
235
138
  fi
236
139
  fi
237
-
238
- # Строка обхода: поведение не меняется, договорённость о продукте не нужна. Причина обязана
239
- # стоять — без неё обход становится умолчанием.
240
- if grep -qE '^\*\*Поведение:\*\*[[:space:]]*не меняется[[:space:]]*—[[:space:]]*\S' "$plan" 2>/dev/null; then
241
- exit 0
242
- fi
243
-
244
- draft="$(sed -n 's/^\*\*Драфт:\*\*[[:space:]]*`\([^`]*\)`.*/\1/p' "$plan" 2>/dev/null | head -1)"
245
-
246
- if [ -z "$draft" ]; then
247
- deny "BLOCKED by task-flow: в '${tasks_dir}/${branch}/plan.md' не названа договорённость о продукте. Заведи её в docs/specs/<домен>/proposed/<фича>/ и укажи строкой '**Драфт:** \`путь\`'. Правило — скил task-flow." \
248
- "строка '**Поведение:** не меняется — <причина владельца>' в замысле; пустая причина не принимается"
249
- fi
250
-
251
- case "$draft" in
252
- /*) draft_path="$draft" ;;
253
- *) draft_path="$root/$draft" ;;
254
- esac
255
-
256
- if [ -e "$draft_path" ]; then
257
- exit 0
258
- fi
259
-
260
- # Договорённость, влитая в спек домена, с диска уходит — так и задумано: в главной ветке
261
- # директории «предложено» быть не должно. Но замысел на неё ссылается до конца работы, и без
262
- # этой развилки последний коммит PR запирал бы ветку: ни правки по замечаниям разбора, ни
263
- # записи в журнал изменений после вливания уже не сделать.
264
- #
265
- # Влитое от незаведённого отличает история ветки: путь, которого в ней никогда не было,
266
- # договорённостью не был. Спросить об этом нечем, кроме git, поэтому нет git — отказ остаётся.
267
- if git -C "$root" log --oneline -1 -- "$draft" 2>/dev/null | grep -q .; then
268
- exit 0
269
- fi
270
-
271
- deny "BLOCKED by task-flow: замысел называет договорённость '${draft}', а её на диске нет и в истории ветки не было. Заведи её с образца (docs/specs/_template) или поправь путь в '${tasks_dir}/${branch}/plan.md'. Правило — скил task-flow."
@@ -145,7 +145,14 @@ next_step=""
145
145
 
146
146
  # Команда, меняющая дерево или его состояние. Чтение и поиск сюда не входят намеренно: ими и
147
147
  # заполняется ход, который встал.
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:]]*[^|&]'
148
+ #
149
+ # Перенаправления вывода здесь нет, и снято оно по разбору: страж отбил два хода из семи,
150
+ # простоявших без работы, а пять засчитал работой ровно по нему. Тело будущей заявки писалось во
151
+ # временный каталог захода — вне рабочего дерева, — а одно из ожиданий несло только отвод потока
152
+ # ошибок в устройство пустоты. Ни то ни другое дерева не меняет, а образцу они были неотличимы
153
+ # от записи в рабочий файл: он видел саму стрелку и не спрашивал, куда она ведёт. Признаком
154
+ # остаются правка файла инструментом правки и перечисленные здесь команды.
155
+ 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 '
149
156
 
150
157
  # Разведка. Те же слова, что и в образце работы, но подкоманда читающая: переключение ветки,
151
158
  # подтягивание, просмотр истории, чтение заявок и прогонов. Образец работы называет их работой,
@@ -180,7 +180,11 @@ reason="BLOCKED by window-fill-guard: заполнение окна ${pct}% (${f
180
180
  2. Закоммить проверенное: незакоммиченное не переживёт перерыв.
181
181
  3. Напиши передачу в ${handoff_dir}/ и отдай владельцу путь к ней — что в неё входит, говорит паттерн task-flow-handoff.
182
182
 
183
- Пропускаются при этом: правка ${tasks_dir}/**, запись передачи, команды поставки и сверки, чтение файлов и вопрос владельцу."
183
+ Что после порога проходит:
184
+ - правка ${tasks_dir}/** и ${handoff_dir}/**, чтение любого файла, вопрос владельцу;
185
+ - команда, начинающаяся со слова поставки или сверки: git, клиент хостинга, перевод колонки, npm run check:*.
186
+
187
+ Команда судится по началу строки: вход в каталог перед ней снимает совпадение, и отбит будет тот же коммит, который прошёл бы без него. Начинай команду с самого слова поставки."
184
188
 
185
189
  # Общий хвост отказа: два законных хода и законная форма обхода, если она у отказа есть.
186
190
  # Файл может быть не разложен — тогда хвоста нет, а причина отказа остаётся прежней.
@@ -27,6 +27,20 @@
27
27
  Правка одного рода остаётся одной задачей, сколько бы файлов она ни задела; объём захода
28
28
  говорит, какого размера задачу заводить среди тех, что делятся законно, и делить неделимое не
29
29
  даёт.
30
+ - **Работы, идущие одна за другой, ветвятся одна от другой.** Ветка следующей отводится от
31
+ предыдущей, а не от главной: тогда правка предыдущей лежит в общем предке, и сводить два разных
32
+ изменения одного файла уже не нужно. Ветки, заведённые от главной подряд, друг о друге не знают,
33
+ и первое же слияние делает остальные расходящимися — тем вернее, чем ближе работы по предмету:
34
+ расходятся не сами правки, а строки, куда обе дописали. Цена не исчезает, а переносится: платит
35
+ её тот, кто ветвится, один раз — вместо того, кто вливает, столько раз, сколько заявок открыто.
36
+ От главной ветвится первая работа череды и всякая, не связанная с предыдущей.
37
+ - **Череда веток отдаётся снизу вверх, и порядок называется владельцу.** Заявка каждой стоит на
38
+ предыдущей, а не на главной, поэтому влитая не по порядку тащит за собой всё, что под ней.
39
+ Порядок известен тому, кто ветвился, и не виден тому, кто вливает: несказанный, он читается как
40
+ его отсутствие.
41
+ - **Ветка, стоящая под другой, историю не переписывает.** Силовая отправка нижней делает вершину
42
+ верхней достижимой из её основания, и хостинг закрывает заявку верхней как слитую — при том что
43
+ в главной ветке её правок нет. Расхождение чинится вливанием, а не переносом истории.
30
44
  - **Правка самой поставки проверяется её прогоном, а не рассуждением.** Проверить её иначе нечем:
31
45
  она исполняется только там, где выкатывает, и в среде, которой на месте работы нет — с чужими
32
46
  правами, чужим хранилищем ключей и чужой сетью. «Проверю после слияния» решением исполнителя не
@@ -60,6 +74,11 @@
60
74
  - **Признак режима исполнения объявлен в самом артефакте развёртывания, а не только в составе его
61
75
  запуска.** Артефакт поднимают и мимо состава — руками, при разборе, на чужой машине, — и без
62
76
  объявления он в этот момент считает себя отладочным, не сказав об этом ничего.
77
+ - **Артефакт развёртывания несёт всё, чем настраивается запускаемое им.** Сборка проверяет, что
78
+ артефакт собрался, а не что его собственные шаги запуска исполнимы: недостающий файл настройки
79
+ не виден ни линтеру, ни сборке, ни шагу сборки образа в конвейере — он отказывает на первом
80
+ подъёме, то есть уже там, куда выкатили. Файл, лежащий рядом в репозитории, внутрь артефакта
81
+ сам не попадает.
63
82
  - **Выкатывается образ того коммита, который выкатывают.** Умолчание «последний» отстаёт от
64
83
  главной ветки, и приложение молча возвращается к прежней версии, продолжая отвечать.
65
84
  - **Из одного и того же коммита всегда ставятся одни и те же зависимости.** Если версия задана
@@ -18,6 +18,10 @@
18
18
  - **Оформление берётся из общего набора значений, а не пишется числом на месте.** Написанное
19
19
  числом расходится с остальным при первой же смене оформления, и найти все такие места
20
20
  нечем.
21
+ - **Управляющий элемент не меняет размер, пока идёт начатая им работа.** Готовые наборы
22
+ элементов на время работы подменяют подпись и значок индикатором ожидания, и элемент
23
+ сжимается до собственных отступов: он прыгает, сдвигает соседей, а нажатие, которое человек
24
+ уже начал, уходит мимо.
21
25
  - **У каждого класса в разметке есть своё правило стилей.** Класс без правила выглядит
22
26
  рабочим и молча ничего не делает.
23
27
  - **Набор записей приходит с сервера страницей, а не целиком.** Список, растущий вместе с