@rt-tools/agent-kit 0.19.0 → 0.21.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 (85) hide show
  1. package/assets/checks/check-board.github.mjs +2 -2
  2. package/assets/checks/check-reuse.mjs +11 -1
  3. package/assets/checks/check-specs.mjs +8 -23
  4. package/assets/checks/check-states.mjs +1 -1
  5. package/assets/checks/signals/core.json +12 -2
  6. package/assets/checks/signals.mjs +17 -0
  7. package/assets/checks/spec-proposed.mjs +122 -0
  8. package/assets/defaults/gate-map.sh +6 -1
  9. package/assets/defaults/shell.sh +8 -0
  10. package/assets/hooks/browser-guard-device-id.sh +3 -0
  11. package/assets/hooks/browser-guard-no-asking.sh +3 -0
  12. package/assets/hooks/browser-guard-no-listing.sh +3 -0
  13. package/assets/hooks/browser-guard-no-other-drivers.sh +3 -0
  14. package/assets/hooks/browser-guard-require-select.sh +3 -0
  15. package/assets/hooks/claim-guard.sh +3 -0
  16. package/assets/hooks/conscience-guard.sh +3 -0
  17. package/assets/hooks/deny-tail.sh +12 -0
  18. package/assets/hooks/dev-server-guard.sh +3 -0
  19. package/assets/hooks/docs-guard.sh +4 -11
  20. package/assets/hooks/exam-guard.sh +3 -0
  21. package/assets/hooks/git-guard-delivery.sh +4 -4
  22. package/assets/hooks/git-guard-main.sh +4 -9
  23. package/assets/hooks/git-guard-push-tests.sh +3 -0
  24. package/assets/hooks/grill-gate.sh +3 -0
  25. package/assets/hooks/guard-note.sh +45 -0
  26. package/assets/hooks/handoff-entry-guard.sh +3 -0
  27. package/assets/hooks/handoff-write.sh +58 -7
  28. package/assets/hooks/override-write-guard.sh +4 -4
  29. package/assets/hooks/postmortem-guard.sh +15 -3
  30. package/assets/hooks/proposal-guard.sh +3 -0
  31. package/assets/hooks/prose-style-guard.sh +3 -0
  32. package/assets/hooks/qa-dataid-guard.sh +3 -0
  33. package/assets/hooks/rerun-guard.sh +3 -0
  34. package/assets/hooks/reuse-first-guard.sh +3 -0
  35. package/assets/hooks/rule-article.sh +1 -1
  36. package/assets/hooks/rule-source-guard.sh +4 -4
  37. package/assets/hooks/sql-guard.sh +3 -0
  38. package/assets/hooks/task-context-load.sh +1 -1
  39. package/assets/hooks/task-flow-draft-guard.sh +3 -0
  40. package/assets/hooks/task-flow-guard.sh +3 -0
  41. package/assets/hooks/turn-entry-load.sh +27 -2
  42. package/assets/hooks/turn-exit-guard.sh +8 -5
  43. package/assets/hooks/waiting-turn-guard.sh +31 -10
  44. package/assets/hooks/window-fill-guard.sh +3 -0
  45. package/assets/hooks/work-start-guard.sh +3 -0
  46. package/assets/hooks/write-targets.sh +59 -10
  47. package/assets/laws/delivery.md +5 -0
  48. package/assets/laws/work-conduct.md +5 -0
  49. package/assets/patterns/browser-verification-stand.md +24 -9
  50. package/assets/patterns/git-workflow-merge.md +17 -0
  51. package/assets/patterns/git-workflow-pr.github.md +29 -22
  52. package/assets/patterns/spec-driven-domain.md +5 -0
  53. package/assets/patterns/spec-driven-rule.md +41 -1
  54. package/assets/patterns/task-flow-archive.md +39 -0
  55. package/assets/patterns/task-flow-handoff.md +25 -2
  56. package/assets/patterns/task-flow-resume.md +16 -0
  57. package/assets/patterns/task-flow-start.md +40 -40
  58. package/assets/patterns/testing-e2e.md +15 -0
  59. package/assets/patterns/testing-unit.md +13 -0
  60. package/assets/pitfalls/git-workflow.github.md +19 -1
  61. package/assets/pitfalls/spec-driven.md +28 -0
  62. package/assets/pitfalls/task-flow.md +81 -38
  63. package/assets/pitfalls/testing.md +9 -0
  64. package/assets/pitfalls/turn-conduct.md +8 -0
  65. package/assets/rules/spec-driven.md +23 -24
  66. package/assets/rules/turn-conduct.md +13 -15
  67. package/assets/rules/turn-entry.md +6 -4
  68. package/assets/rules/typescript-conventions.md +7 -0
  69. package/assets/skills/agent-kit-extend.md +11 -0
  70. package/assets/skills/agent-kit.md +17 -3
  71. package/assets/templates/findings.md +27 -0
  72. package/bin/agent-kit.d.ts.map +1 -1
  73. package/bin/agent-kit.js +3 -0
  74. package/bin/agent-kit.js.map +1 -1
  75. package/lib/commands.d.ts +5 -0
  76. package/lib/commands.d.ts.map +1 -1
  77. package/lib/commands.js +94 -1
  78. package/lib/commands.js.map +1 -1
  79. package/lib/observations.d.ts +13 -1
  80. package/lib/observations.d.ts.map +1 -1
  81. package/lib/observations.js +7 -1
  82. package/lib/observations.js.map +1 -1
  83. package/package.json +1 -1
  84. package/rt-tools-agent-kit-0.21.0.tgz +0 -0
  85. package/rt-tools-agent-kit-0.19.0.tgz +0 -0
@@ -12,6 +12,16 @@
12
12
  # дерево — ветку, незакоммиченное и коммиты сверх главной. Ничего от себя он не добавляет:
13
13
  # передача пересказывает записанное, а не заменяет его.
14
14
  #
15
+ # Кладётся она разделом в ход работы задачи — туда же, где лежит состояние. Свой файл вне дерева
16
+ # передачу теряет при переходе на другую машину: каталог закрыт от истории, и работу, оборванную
17
+ # заполнением окна, оттуда не подхватить — состояние есть в ходе работы, а особенности захода,
18
+ # следующий шаг и то, чего делать не надо, остаются на прежней машине. Второй записи об одном и
19
+ # том же при этом не заводится: раздел живёт в том же файле, что и «Где стоим», и уезжает в
20
+ # ветку тем же коммитом. Коммитит его исполнитель — хук не знает, что ещё лежит в индексе.
21
+ #
22
+ # Файл вне дерева остаётся запасным путём: у работы без папки задачи — на отсоединённой голове,
23
+ # на ветке без заведённой задачи — разделу лечь некуда, а молчание здесь стоит захода целиком.
24
+ #
15
25
  # Написанное руками не затирается молча: файл один, и последняя запись побеждает. Исполнитель,
16
26
  # закрывающий заход по правилу, пишет поверх — его передача полнее, потому что он знает то,
17
27
  # чего на диске нет.
@@ -93,8 +103,15 @@ ahead="$(git log --oneline origin/main..HEAD 2>/dev/null | head -20)"
93
103
 
94
104
  target="$root/$handoff_dir/$handoff_name.md"
95
105
 
106
+ # Заголовок раздела в ходе работы: по нему прежняя передача и находится.
107
+ section='## Передача захода'
108
+
109
+ # Собирается передача во временный файл: и раздел, и запасной путь пишут одно и то же, и второй
110
+ # сборки для второго места не заводится.
111
+ scratch="$(mktemp 2>/dev/null)" || exit 0
112
+
96
113
  {
97
- printf '# Передача захода сжатие контекста (%s)\n\n' "$trigger"
114
+ printf 'Собрана хуком перед сжатием контекста (%s).\n\n' "$trigger"
98
115
  printf '**Рабочее дерево:** %s\n' "$root"
99
116
  if [ -n "$branch" ]; then
100
117
  printf '**Ветка:** %s\n\n' "$branch"
@@ -103,22 +120,56 @@ target="$root/$handoff_dir/$handoff_name.md"
103
120
  fi
104
121
 
105
122
  if [ -n "$state" ]; then
106
- printf '## Где стоим\n\n'
123
+ printf '### Где стоим на минуту сжатия\n\n'
107
124
  printf -- '- **Состояние:** %s\n' "$state"
108
125
  [ -n "$stage" ] && printf -- '- **Этап:** %s\n' "$stage"
109
126
  [ -n "$next_step" ] && printf -- '- **Следующий шаг:** %s\n' "$next_step"
110
127
  [ -n "$pull" ] && printf -- '- **PR:** %s\n' "$pull"
111
128
  printf '\nХод работы целиком — `%s/%s/progress.md`; замысел рядом с ним.\n\n' "$tasks_dir" "$branch"
112
129
  elif [ -z "$branch" ]; then
113
- printf '## Где стоим\n\nИмени у головы нет, папки задачи при ней тоже: состояние работы взять неоткуда. Читающая сторона ищет передачу по имени ветки, а не найдя его — по последней записи каталога.\n\n'
130
+ printf '### Где стоим на минуту сжатия\n\nИмени у головы нет, папки задачи при ней тоже: состояние работы взять неоткуда. Читающая сторона ищет передачу по имени ветки, а не найдя его — по последней записи каталога.\n\n'
114
131
  else
115
- printf '## Где стоим\n\nПапки задачи у этой ветки нет: состояние работы взять неоткуда.\n\n'
132
+ printf '### Где стоим на минуту сжатия\n\nПапки задачи у этой ветки нет: состояние работы взять неоткуда.\n\n'
116
133
  fi
117
134
 
118
- printf '## Незакоммиченное\n\n```\n%s\n```\n\n' "$uncommitted"
119
- printf '## Коммиты сверх главной\n\n```\n%s\n```\n\n' "$ahead"
135
+ printf '### Незакоммиченное\n\n```\n%s\n```\n\n' "$uncommitted"
136
+ printf '### Коммиты сверх главной\n\n```\n%s\n```\n\n' "$ahead"
120
137
  printf 'Написана хуком перед сжатием контекста. Всё, что здесь стоит, проверяется деревом:\n'
121
138
  printf 'передача пересказывает записанное и описывает минуту, когда её собрали.\n'
122
- } > "$target" 2>/dev/null
139
+ } > "$scratch" 2>/dev/null
140
+
141
+ [ -s "$scratch" ] || exit 0
142
+
143
+ # Есть ход работы — передача ложится его разделом, и прежний такой раздел заменяется целиком:
144
+ # дописанный вторым, он оставил бы в одном файле две правды об одной работе.
145
+ if [ -n "$progress" ] && [ -f "$progress" ]; then
146
+ kept="$scratch.kept"
147
+ # Сравнение идёт байтами. Под локалью с национальными настройками `awk` этой системы считает
148
+ # разные кириллические строки равными — «## Где стоим» совпадало с «## Передача захода», — и
149
+ # выемка прежнего раздела сносила состояние работы вместе с ним. Виден промах только там, где
150
+ # локаль объявлена: у исполнителя с `C.UTF-8` набор зелёный, на прогоне конвейера — красный.
151
+ LC_ALL=C awk -v mark="$section" '
152
+ $0 == mark { skip = 1; next }
153
+ skip && /^## / && $0 != mark { skip = 0 }
154
+ !skip { print }
155
+ ' "$progress" > "$kept" 2>/dev/null || { rm -f "$scratch" "$kept"; exit 0; }
156
+
157
+ {
158
+ # Хвостовые пустые строки прежнего текста снимаются: раздел приписывается к нему через
159
+ # одну пустую строку, а не через сколько их там осталось.
160
+ awk 'BEGIN { blanks = 0 }
161
+ { if ($0 ~ /^[[:space:]]*$/) { blanks++; next }
162
+ while (blanks > 0) { print ""; blanks-- }
163
+ print }' "$kept"
164
+ printf '\n%s\n\n' "$section"
165
+ cat "$scratch"
166
+ } > "$progress.rt-handoff" 2>/dev/null \
167
+ && mv "$progress.rt-handoff" "$progress" 2>/dev/null
168
+
169
+ rm -f "$scratch" "$kept"
170
+ exit 0
171
+ fi
172
+
173
+ mv "$scratch" "$target" 2>/dev/null || rm -f "$scratch"
123
174
 
124
175
  exit 0
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bash
2
2
  # rt-hook: PreToolUse Write|Bash|mcp__webstorm__create_new_file|mcp__webstorm__execute_terminal_command|mcp__webstorm__execute_tool
3
- # Требует: hooks/deny-tail.sh
3
+ # Требует: hooks/deny-tail.sh, hooks/guard-note.sh
4
4
  # Гард затирания надстройки: запись поверх — не то же самое, что правка.
5
5
  #
6
6
  # Надстройка дерева сливается с ресурсом пакета по разделу «## »: раздел, который дерево
@@ -20,6 +20,9 @@
20
20
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: нет `jq`, битый ввод, чужой инструмент, файла нет, файл пуст —
21
21
  # правка РАЗРЕШАЕТСЯ. Сломанный гард не имеет права заклинить работу.
22
22
 
23
+ # Своё имя в наблюдениях: отбой пишет общий хвост отказа, а не сам гард.
24
+ RT_GUARD_NAME=override-write-guard
25
+
23
26
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
24
27
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
25
28
 
@@ -35,9 +38,6 @@ rt_hooks_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
35
38
  command -v rt_deny_tail >/dev/null 2>&1 || rt_deny_tail() { printf ''; }
36
39
 
37
40
  deny() {
38
- # shellcheck disable=SC1090
39
- [ -f "$rt_hooks_dir/observe.sh" ] && . "$rt_hooks_dir/observe.sh" 2>/dev/null
40
- command -v rt_note >/dev/null 2>&1 && rt_note guard-deny res=override-write-guard
41
41
 
42
42
  reason="$1 $(rt_deny_tail "$2")"
43
43
  jq -n --arg r "$reason" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$r}}' 2>/dev/null \
@@ -21,6 +21,9 @@
21
21
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: при любой ошибке, отсутствии записи хода и повторном заходе ход
22
22
  # РАЗРЕШАЕТСЯ (exit 0). Сломанный гард не имеет права заклинить разговор.
23
23
 
24
+ # Своё имя в наблюдениях: отбой пишет общий хвост отказа, а не сам гард.
25
+ RT_GUARD_NAME=postmortem-guard
26
+
24
27
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
25
28
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
26
29
 
@@ -40,9 +43,18 @@ transcript="$(printf '%s' "$input" | jq -r '.transcript_path // empty' 2>/dev/nu
40
43
 
41
44
  root="${CLAUDE_PROJECT_DIR:-.}"
42
45
 
43
- # Каталог записей у дерева свой. Заданный пустым отказ дерева от требования: дереву, которое
44
- # записей не ведёт, гард не навязывается.
45
- notes_dir="${RT_POSTMORTEMS_DIR-docs/postmortems}"
46
+ # Каталог записей у дерева свой, и называет его настройка дерева тем же ключом, каким его
47
+ # читает отправка груза. Одно имя на обе стороны: дерево, унёсшее каталог из истории, иначе
48
+ # получало бы гард, ищущий записи по прежнему адресу, — и тот молчал бы всегда. Переменная
49
+ # остаётся обходом на один запуск, а пустая строка в любой из двух — отказ дерева от требования:
50
+ # дереву, которое записей не ведёт, гард не навязывается.
51
+ if [ -n "${RT_POSTMORTEMS_DIR+set}" ]; then
52
+ notes_dir="$RT_POSTMORTEMS_DIR"
53
+ elif [ -f "$root/.claude/rt-kit.json" ]; then
54
+ notes_dir="$(jq -r '.postmortems // "docs/postmortems"' "$root/.claude/rt-kit.json" 2>/dev/null)"
55
+ else
56
+ notes_dir="docs/postmortems"
57
+ fi
46
58
  [ -z "$notes_dir" ] && exit 0
47
59
  [ -d "$root/$notes_dir" ] || exit 0
48
60
 
@@ -20,6 +20,9 @@
20
20
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: при любой ошибке, нехватке `jq`, отсутствии записи хода и повторном
21
21
  # заходе ход РАЗРЕШАЕТСЯ (exit 0). Сломанный гард не имеет права заклинить разговор.
22
22
 
23
+ # Своё имя в наблюдениях: отбой пишет общий хвост отказа, а не сам гард.
24
+ RT_GUARD_NAME=proposal-guard
25
+
23
26
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
24
27
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
25
28
 
@@ -12,6 +12,9 @@
12
12
  #
13
13
  # FAIL-OPEN: нет узла, нет проверки, чужой инструмент, не `.md` → пропуск.
14
14
 
15
+ # Своё имя в наблюдениях: отбой пишет общий хвост отказа, а не сам гард.
16
+ RT_GUARD_NAME=prose-style-guard
17
+
15
18
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
16
19
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
17
20
 
@@ -25,6 +25,9 @@
25
25
  #
26
26
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: нет разборщика, битый ввод, чужой инструмент — пропуск.
27
27
 
28
+ # Своё имя в наблюдениях: отбой пишет общий хвост отказа, а не сам гард.
29
+ RT_GUARD_NAME=qa-dataid-guard
30
+
28
31
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
29
32
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
30
33
 
@@ -20,6 +20,9 @@
20
20
  # похож на перезапуск — пропуск. Гард без номера судить не берётся: перезапуск последнего
21
21
  # упавшего прогона зовут и без него, а угадывать, о каком задании речь, значит отбивать наугад.
22
22
 
23
+ # Своё имя в наблюдениях: отбой пишет общий хвост отказа, а не сам гард.
24
+ RT_GUARD_NAME=rerun-guard
25
+
23
26
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
24
27
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
25
28
 
@@ -32,6 +32,9 @@
32
32
  #
33
33
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: нет разборщика, битый ввод, чужой инструмент — пропуск.
34
34
 
35
+ # Своё имя в наблюдениях: отбой пишет общий хвост отказа, а не сам гард.
36
+ RT_GUARD_NAME=reuse-first-guard
37
+
35
38
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
36
39
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
37
40
 
@@ -88,7 +88,7 @@ rt_rule_article_heads() {
88
88
  }
89
89
 
90
90
  rt_rule_article_at() {
91
- awk -v mark="$2" '
91
+ LC_ALL=C awk -v mark="$2" '
92
92
  NR <= mark && /^- / { start = NR }
93
93
  { line[NR] = $0 }
94
94
  END {
@@ -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, hooks/write-targets.sh
3
+ # Требует: hooks/profile-check.sh, hooks/deny-tail.sh, hooks/write-targets.sh, hooks/guard-note.sh
4
4
  # Гард места правки: слой правил чинится там, где сломано, а не там, где виден.
5
5
  #
6
6
  # Слой правил правит тот же исполнитель, которым слой правил управляет, и разницы между
@@ -20,6 +20,9 @@
20
20
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: нет `jq`, битый ввод, чужой инструмент, файла нет, шапки в нём нет,
21
21
  # не git-репозиторий — правка РАЗРЕШАЕТСЯ. Сломанный гард не имеет права заклинить работу.
22
22
 
23
+ # Своё имя в наблюдениях: отбой пишет общий хвост отказа, а не сам гард.
24
+ RT_GUARD_NAME=rule-source-guard
25
+
23
26
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
24
27
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
25
28
 
@@ -43,9 +46,6 @@ command -v rt_needs >/dev/null 2>&1 || rt_needs() { command -v "$1" >/dev/null 2
43
46
  command -v rt_deny_tail >/dev/null 2>&1 || rt_deny_tail() { printf ''; }
44
47
 
45
48
  deny() {
46
- # shellcheck disable=SC1090
47
- [ -f "$rt_hooks_dir/observe.sh" ] && . "$rt_hooks_dir/observe.sh" 2>/dev/null
48
- command -v rt_note >/dev/null 2>&1 && rt_note guard-deny res=rule-source-guard
49
49
 
50
50
  reason="$1 $(rt_deny_tail "$2")"
51
51
  jq -n --arg r "$reason" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$r}}' 2>/dev/null \
@@ -40,6 +40,9 @@
40
40
  # пропускаются целиком — иначе слова «drop» и «update» в описании коммита читались бы как
41
41
  # запрос. Доставки запроса внутри такой команды не бывает, так что цена послабления нулевая.
42
42
 
43
+ # Своё имя в наблюдениях: отбой пишет общий хвост отказа, а не сам гард.
44
+ RT_GUARD_NAME=sql-guard
45
+
43
46
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
44
47
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
45
48
 
@@ -139,7 +139,7 @@ done
139
139
  cat "$PROGRESS"
140
140
  else
141
141
  # Раздел «Где стоим» перезаписывается каждым заходом и переживает любой объём.
142
- awk '/^## Где стоим/{f=1} f&&/^## /&&!/^## Где стоим/{exit} f' "$PROGRESS"
142
+ LC_ALL=C awk '/^## Где стоим/{f=1} f&&/^## /&&!/^## Где стоим/{exit} f' "$PROGRESS"
143
143
  printf '\n<обрезано по объёму. Последние записи:>\n\n'
144
144
  tail -40 "$PROGRESS"
145
145
  printf '\n<читается целиком: %s>\n' "$PROGRESS"
@@ -23,6 +23,9 @@
23
23
  # FAIL-OPEN: нет jq, не git-репозиторий, битый ввод, чужой инструмент → пропуск. Сломанный
24
24
  # гард не должен мешать работать.
25
25
 
26
+ # Своё имя в наблюдениях: отбой пишет общий хвост отказа, а не сам гард.
27
+ RT_GUARD_NAME=task-flow-draft-guard
28
+
26
29
  rt_hooks_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
27
30
 
28
31
  # Разбор вызова — общий у обоих гардов хода работы. Файл может быть не разложен: тогда судить
@@ -22,6 +22,9 @@
22
22
  # FAIL-OPEN: нет jq, не git-репозиторий, битый ввод, чужой инструмент → пропуск. Сломанный
23
23
  # гард не должен мешать работать.
24
24
 
25
+ # Своё имя в наблюдениях: отбой пишет общий хвост отказа, а не сам гард.
26
+ RT_GUARD_NAME=task-flow-guard
27
+
25
28
  rt_hooks_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
26
29
 
27
30
  # Разбор вызова — общий у обоих гардов хода работы. Файл может быть не разложен: тогда судить
@@ -39,11 +39,36 @@ map="$rt_hooks_dir/../rt-kit/defaults/turn-map.md"
39
39
  [ -f "$map" ] || map="$ROOT/.claude/rt-kit/defaults/turn-map.md"
40
40
 
41
41
  # Передача берётся по имени текущей ветки. Чужая, поданная как своя, описывает работу, которой
42
- # в этом дереве нет, — поэтому имя файла собирается из ветки, а не выбирается из каталога.
42
+ # в этом дереве нет, — поэтому и раздел, и файл ищутся по ветке, а не выбираются из каталога.
43
+ #
44
+ # Мест два, и порядок между ними один. Передача ветки с папкой задачи лежит разделом её хода
45
+ # работы: он коммитится и едет в ветку, поэтому переживает переход на другую машину. Файл вне
46
+ # дерева — запасной путь: у работы без папки задачи разделу лечь некуда. Найдя раздел, файл не
47
+ # читают: это одна и та же передача, и вторая копия старше первой.
48
+ tasks_dir="${RT_TASKS_DIR:-docs/tasks}"
49
+ progress=''
50
+ [ -n "$branch" ] && progress="$ROOT/$tasks_dir/$branch/progress.md"
51
+
52
+ section=''
53
+ if [ -n "$progress" ] && [ -r "$progress" ]; then
54
+ # Сравнение байтами: под локалью с национальными настройками `awk` этой системы считает
55
+ # разные кириллические заголовки равными, и вход читал бы разделом передачи первый попавшийся.
56
+ section="$(LC_ALL=C awk '
57
+ $0 == "## Передача захода" { skip = 1; next }
58
+ skip && /^## / { skip = 0 }
59
+ skip { print }
60
+ ' "$progress" 2>/dev/null)"
61
+ fi
62
+
43
63
  handoff=''
44
64
  [ -n "$branch" ] && handoff="$ROOT/$handoff_dir/$branch.md"
45
65
 
46
- if [ -n "$handoff" ] && [ -r "$handoff" ]; then
66
+ if [ -n "$section" ]; then
67
+ printf 'ПЕРЕДАЧА ПРОШЛОГО ЗАХОДА — раздел «Передача захода» в `%s/%s/progress.md`, ниже целиком.\n\n' "$tasks_dir" "$branch"
68
+ printf 'Она написана прошлым заходом и описывает минуту, когда её собрали. Всё, что в ней\n'
69
+ printf 'стоит о дереве, проверяется деревом: команда этого хода главнее записанной вчера.\n\n'
70
+ printf '%s\n\n' "$section"
71
+ elif [ -n "$handoff" ] && [ -r "$handoff" ]; then
47
72
  printf 'ПЕРЕДАЧА ПРОШЛОГО ЗАХОДА — `%s/%s.md`, ниже целиком.\n\n' "$handoff_dir" "$branch"
48
73
  printf 'Она написана прошлым заходом и описывает минуту, когда её собрали. Всё, что в ней\n'
49
74
  printf 'стоит о дереве, проверяется деревом: команда этого хода главнее записанной вчера.\n\n'
@@ -34,6 +34,9 @@
34
34
  # или строки состояния ход РАЗРЕШАЕТСЯ (exit 0). Сломанный страж не имеет права заклинить
35
35
  # разговор.
36
36
 
37
+ # Своё имя в наблюдениях: отбой пишет общий хвост отказа, а не сам гард.
38
+ RT_GUARD_NAME=turn-exit-guard
39
+
37
40
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
38
41
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
39
42
 
@@ -166,7 +169,7 @@ work_re='git (add|commit|push|checkout|merge|rm)|npm run|pnpm (run|exec)|nx (bui
166
169
  # подтягивание, просмотр истории, чтение заявок и прогонов. Образец работы называет их работой,
167
170
  # потому что знает только первое слово — `git` и `gh` стоят в нём целиком, — и ход, в котором
168
171
  # исполнитель перешёл на главную ветку, прочитал историю и написал владельцу отчёт, выходил
169
- # отсюда нулём. Разбор — `docs/postmortems/2026-08-25-read-only-turn-counted-as-work.md`.
172
+ # отсюда нулём. Разбор — запись «2026-08-25-read-only-turn-counted-as-work» в приёме.
170
173
  #
171
174
  # Разведка выглядит работой лучше всего остального: в ней команды, числа и точные ответы. Тем
172
175
  # она и опасна — ход, набитый ею, читается как полный и владельцем, и самим заходом.
@@ -179,7 +182,7 @@ part_re='&&|\|\||;|\n'
179
182
  # Ожидание чужого шага. Прогон, разбор владельцем и слияние идут без исполнителя и от взгляда
180
183
  # быстрее не становятся — правило прямо говорит, что состоянием работы это не бывает. Судится
181
184
  # только ПОСЛЕДНЕЕ действие хода: ожидание в середине законно, а запуск работы в фоне работой
182
- # остаётся. Разбор — `docs/postmortems/2026-08-25-turn-ended-on-waiting.md`.
185
+ # остаётся. Разбор — запись «2026-08-25-turn-ended-on-waiting» в приёме.
183
186
  #
184
187
  # Прежний признак спрашивал одно: была ли за ход работа. Ход, где разобран конфликт, сделаны
185
188
  # коммит и пуш, а последним действием стал цикл до готовности прогона, проходил его целиком —
@@ -264,7 +267,7 @@ commands="$(printf '%s' "$verdict" | jq -r '.ran // ""' 2>/dev/null)"
264
267
  # Ровно так ход и вставал: задача взята, номер назван владельцу, отчёт написан — и следующее
265
268
  # действие состояния «задача-взята», написать замысел, не сделано. Отчёт выглядит работой лучше
266
269
  # всякой другой, а страж, знающий только «была ли за ход работа», подтверждает это: работа была.
267
- # Разбор — `docs/postmortems/2026-08-25-task-taken-and-turn-ended.md`.
270
+ # Разбор — запись «2026-08-25-task-taken-and-turn-ended» в приёме.
268
271
  #
269
272
  # Папка, разобранная коммитом ветки, сюда не попадает: `archived` означает отданную работу, и её
270
273
  # судит прежний ярус. Ветка без номера задачи не судится вовсе — под пробу заводят и такие.
@@ -332,7 +335,7 @@ fi
332
335
  # вся она по сданной задаче: отдача завершает прошлую работу, а не ход. Девять разборов
333
336
  # происшествий за сутки описывают девять разных остановок, и во всех девяти последним действием
334
337
  # хода был текст владельцу: отчёт, сводка, объявление намерения.
335
- # Разбор — `docs/postmortems/2026-08-25-handover-turn-ends-on-intent.md`.
338
+ # Разбор — запись «2026-08-25-handover-turn-ends-on-intent» в приёме.
336
339
  if [ "$handed_over" = "true" ] && [ "$started_next" != "true" ]; then
337
340
  reason="BLOCKED by turn-exit-guard: заявка открыта, а по следующей работе за этот ход не сделано ничего.
338
341
 
@@ -385,7 +388,7 @@ fi
385
388
  # Работа, оставшаяся в рабочем дереве. Ветка ушла вперёд своей удалённой ссылки, а заявки при
386
389
  # ней в этом ходе не открывали: сделанное лежит там, где его не видит никто, кроме сделавшего.
387
390
  # Признак читается без сети и молчит там, где ссылки нет вовсе — под пробу заводят и такие ветки.
388
- # Разбор — `docs/postmortems/2026-08-25-fifteen-branches-over-one-index.md`.
391
+ # Разбор — запись «2026-08-25-fifteen-branches-over-one-index» в приёме.
389
392
  if [ "$worked" = "true" ] && [ "$handed_over" != "true" ]; then
390
393
  unpushed="$(git -C "$root" rev-list --count '@{u}..HEAD' 2>/dev/null)"
391
394
  if [ -n "$unpushed" ] && [ "$unpushed" -gt 0 ] 2>/dev/null; then
@@ -19,9 +19,10 @@
19
19
  # Спросить хостинг было бы точнее, но сетевой вызов на завершении хода падает вместе со связью и
20
20
  # отбивал бы работу вместо промаха.
21
21
  #
22
- # Что считается действием: заведение задачи, заведение ветки, перевод колонки очереди работ,
23
- # заведение папки задачи. Набор открыт и пополняется правкой полнота его открытый вопрос, а не
24
- # обещание.
22
+ # Что считается действием: заведение задачи, заведение ветки, перевод задачи в колонку работы,
23
+ # заведение папки задачи. Перевод в колонку разбора и снятие папки сюда не входят это шаги
24
+ # закрытия прежней работы, а не начало следующей. Набор открыт и пополняется правкой — полнота
25
+ # его открытый вопрос, а не обещание.
25
26
  #
26
27
  # Чего гард не судит. Ход, в котором о чужом шаге не сказано ничего, — здесь он молчит: пустой
27
28
  # ход неотличим от хода, которому нечего было делать. Это известная его граница. Зелёный прогон
@@ -30,6 +31,9 @@
30
31
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: при любой ошибке, нехватке `jq`, отсутствии записи хода и повторном
31
32
  # заходе ход РАЗРЕШАЕТСЯ (exit 0). Сломанный гард не имеет права заклинить разговор.
32
33
 
34
+ # Своё имя в наблюдениях: отбой пишет общий хвост отказа, а не сам гард.
35
+ RT_GUARD_NAME=waiting-turn-guard
36
+
33
37
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
34
38
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
35
39
 
@@ -58,10 +62,23 @@ opened_re='gh[^|;&]*pr[[:space:]]+create|api[^|;&]*-X[[:space:]]+POST[^|;&]*/pul
58
62
  read_re='run[[:space:]]+(list|view|watch)|pipelines[[:space:]]+runs'
59
63
  red_re='completed[[:space:]]+failure|"conclusion"[[:space:]]*:[[:space:]]*"failure"|conclusion:[[:space:]]*failure|completed with .failure.|run[[:space:]]+failed'
60
64
 
61
- # Первое действие по следующей задаче. Заведение папки стоит здесь наравне с командами: работа
62
- # по уже заведённому номеру начинается именно с неё. Правка кода в ветке того же PR действием
63
- # по следующей задаче не считается она чинит прежнюю, а не двигает работу дальше.
64
- moved_re='task:new|task:move|checkout[[:space:]]+-b|docs/tasks/'
65
+ # Первое действие по следующей задаче. Перечисляется то, чем взятие БЫВАЕТ, а не то, что
66
+ # действием считается: список шагов закрытия работы открыт и растёт, а список действий по
67
+ # следующей задаче закрыту перечня «что бывает» дыры закрывающего шага нет.
68
+ #
69
+ # Перевода закрываемой задачи в колонку разбора и снятия её папки здесь нет намеренно: оба —
70
+ # обязательные шаги закрытия работы, и оба стоят в том же ходе, которым открыт PR. Пока признак
71
+ # перечислял их наравне со взятием, он совпадал всегда, и гард отпускал ход, ради которого
72
+ # заведён, — работа так вставала трижды за один заход.
73
+ #
74
+ # Правка кода в ветке того же PR действием по следующей задаче не считается — она чинит
75
+ # прежнюю, а не двигает работу дальше.
76
+ taken_re='task:new|checkout[[:space:]]+-b|task:move[^|;&]*in-progress|(cp|mkdir)[^|;&]*/tasks/'
77
+
78
+ # Заведение папки задачи идёт и не оболочкой: инструмент правки файла пишет её первым файлом, и
79
+ # командной строки за этим нет вовсе. Поэтому рядом с командами судится путь правки — у него
80
+ # свой образец, потому что имени команды в нём нет.
81
+ taken_path_re='/tasks/'
65
82
 
66
83
  # Чем ход показывает, что отданную работу он довёл до конца, а не бросил черновиком. Снятие
67
84
  # черновика — очевидный случай; чтение прогона — тот, где снимать ещё нечего, но исполнитель
@@ -81,8 +98,8 @@ vow_re='дальше беру|дальше возьму|дальше иду|сл
81
98
  #
82
99
  # Хвост в 400 строк: запись хода растёт всю сессию, а судится только последний ход.
83
100
  verdict="$(tail -n 400 "$transcript" 2>/dev/null | jq -s -r \
84
- --arg opened "$opened_re" --arg moved "$moved_re" --arg read "$read_re" --arg red "$red_re" \
85
- --arg ready "$ready_re" --arg vow "$vow_re" '
101
+ --arg opened "$opened_re" --arg taken "$taken_re" --arg taken_path "$taken_path_re" \
102
+ --arg read "$read_re" --arg red "$red_re" --arg ready "$ready_re" --arg vow "$vow_re" '
86
103
  def is_input:
87
104
  .type == "user"
88
105
  and (((.message.content // []) | if type == "array"
@@ -93,6 +110,10 @@ verdict="$(tail -n 400 "$transcript" 2>/dev/null | jq -s -r \
93
110
  | (if $i == null then [] else .[$i:] end) as $turn
94
111
  | [$turn[] | select(.type == "assistant") | (.message.content // [])[] | select(.type == "tool_use")] as $uses
95
112
  | ($uses | map((.input.command // "")) | join("\n")) as $ran
113
+ # Путь правки — второй способ завести папку задачи. Судятся только инструменты письма: у
114
+ # чтения путь тот же, а работой оно не является.
115
+ | ($uses | map(select(.name == "Write" or .name == "Edit" or .name == "MultiEdit")
116
+ | (.input.file_path // "")) | join("\n")) as $wrote
96
117
  # Ответ инструмента берётся текстом, а не в записи JSON: у той таб и кавычка уезжают
97
118
  # экранированными, и образец по ним не попадает вовсе. Приходит он и строкой, и списком
98
119
  # блоков — обе формы сводятся к одному тексту.
@@ -103,7 +124,7 @@ verdict="$(tail -n 400 "$transcript" 2>/dev/null | jq -s -r \
103
124
  else tostring end] | join("\n")) as $out
104
125
  | ($ran | test($opened; "i")) as $opened_pr
105
126
  | (($ran | test($read; "i")) and ($out | test($red; "i"))) as $red_run
106
- | ($ran | test($moved; "i")) as $went_on
127
+ | (($ran | test($taken; "i")) or ($wrote | test($taken_path; "i"))) as $went_on
107
128
  | ($ran | test($ready; "i")) as $checked
108
129
  | ([$turn[] | select(.type == "assistant") | (.message.content // [])[]
109
130
  | select(.type == "text") | .text] | join("\n")) as $said
@@ -29,6 +29,9 @@
29
29
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: нет размера окна, нет записи захода, нет разборщика, битый разбор —
30
30
  # работа РАЗРЕШАЕТСЯ (exit 0). Сломанный гард не имеет права заклинить работу.
31
31
 
32
+ # Своё имя в наблюдениях: отбой пишет общий хвост отказа, а не сам гард.
33
+ RT_GUARD_NAME=window-fill-guard
34
+
32
35
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
33
36
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
34
37
 
@@ -28,6 +28,9 @@
28
28
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: нет `jq`, нет записи хода, нет признака дерева, повторный заход, любая
29
29
  # своя ошибка — ход РАЗРЕШАЕТСЯ (exit 0). Сломанный гард не имеет права заклинить работу.
30
30
 
31
+ # Своё имя в наблюдениях: отбой пишет общий хвост отказа, а не сам гард.
32
+ RT_GUARD_NAME=work-start-guard
33
+
31
34
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
32
35
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
33
36
 
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bash
2
2
  # Цели записи, названные командой оболочки прямо: перенаправление, `tee`, правка на месте,
3
- # копирование поверх. Печатает по одной в строке.
3
+ # копирование поверх, а у интерпретатора — пути из его тела. Печатает по одной в строке.
4
4
  #
5
5
  # Строки `# rt-hook:` здесь нет намеренно: это помощник, а не хук. Он ничего не решает сам и
6
6
  # зовётся оттуда, где судят правку файла, — гардом места правки и гардом экзамена. Общий он
@@ -10,15 +10,64 @@
10
10
  # Общий признак записи здесь не берётся намеренно — он широк, в нём и имя интерпретатора, и
11
11
  # запуск разложенной проверки читался бы как правка её самой. Снятие копии сюда тоже не входит:
12
12
  # снятый файл раскладка кладёт заново, и так чинят копию, которую переписал форматтер.
13
+ #
14
+ # Интерпретатор с кодом в доводе или в теле — особый случай, и без него у гардов была дыра
15
+ # ровно в ту сторону, которой правят чаще всего. Путь записи стоит там внутри кода — в вызове
16
+ # записи файла, — и ни одним из образцов выше не ловится: команда снаружи выглядит запуском
17
+ # интерпретатора, а не записью. Разложенную копию так правили молча, и ловила это уже раскладка
18
+ # — на сборке, когда правка написана целиком и переносить её приходится руками.
19
+ #
20
+ # Разбирать чужой язык нечем, и здесь не разбирают: из тела берётся всё путеподобное, а решает
21
+ # по нему зовущий — гард места правки отбивает лишь то, что несёт шапку раскладки. Цена названа
22
+ # прямо: чтение файла интерпретатором судится наравне с записью в него. Читают в оболочке
23
+ # другим — `cat`, `grep`, `sed -n`, — а интерпретатор, которому назвали путь разложенной копии,
24
+ # почти всегда её и правит.
13
25
 
14
26
  rt_write_targets() {
15
- tr "\"'\`" ' ' \
16
- | sed -E 's/>>?/\n>/g' \
17
- | sed -nE '
18
- s/^>[[:space:]]*([^[:space:]|&;]+).*/\1/p
19
- s/(^|.*[[:space:]])tee[[:space:]]+(-a[[:space:]]+)?([^[:space:]|&;]+).*/\3/p
20
- s/(^|.*[[:space:]])sed[[:space:]]+-i[[:space:]]+([^[:space:]]+[[:space:]]+)*([^[:space:]|&;]+)$/\3/p
21
- s/(^|.*[[:space:]])(cp|mv|install)[[:space:]]+([^[:space:]]+[[:space:]]+)+([^[:space:]|&;]+).*/\4/p
22
- ' \
23
- | sort -u
27
+ rt_wt_text="$(cat)"
28
+
29
+ {
30
+ printf '%s' "$rt_wt_text" \
31
+ | tr "\"'\`" ' ' \
32
+ | sed -E 's/>>?/\n>/g' \
33
+ | sed -nE '
34
+ s/^>[[:space:]]*([^[:space:]|&;]+).*/\1/p
35
+ s/(^|.*[[:space:]])tee[[:space:]]+(-a[[:space:]]+)?([^[:space:]|&;]+).*/\3/p
36
+ s/(^|.*[[:space:]])sed[[:space:]]+-i[[:space:]]+([^[:space:]]+[[:space:]]+)*([^[:space:]|&;]+)$/\3/p
37
+ s/(^|.*[[:space:]])(cp|mv|install)[[:space:]]+([^[:space:]]+[[:space:]]+)+([^[:space:]|&;]+).*/\4/p
38
+ '
39
+
40
+ # Тело интерпретатора: код приходит доводом `-c`, доводом `-e` либо телом heredoc, и
41
+ # путь записи стоит внутри него. Берётся именно тело, а не вся команда: составная
42
+ # строка, в которой heredoc пишет один файл, а рядом запускается проверка, отдала бы
43
+ # путь этой проверки как цель записи — и запуск разложенной проверки снова читался бы
44
+ # правкой её самой.
45
+ printf '%s\n' "$rt_wt_text" | awk '
46
+ function emit(s, n, i, parts) {
47
+ n = split(s, parts, /[^A-Za-z0-9_.@\/-]+/)
48
+ for (i = 1; i <= n; i++) {
49
+ if (parts[i] ~ /\// && parts[i] ~ /\.[A-Za-z0-9]+$/) { print parts[i] }
50
+ }
51
+ }
52
+ function trim(s) { sub(/^[ \t]+/, "", s); sub(/[ \t]+$/, "", s); return s }
53
+ tag != "" {
54
+ if (trim($0) == tag) { tag = ""; next }
55
+ if (keep) { emit($0) }
56
+ next
57
+ }
58
+ {
59
+ # Код доводом: всё, что стоит за `-c` или `-e` у интерпретатора.
60
+ if ($0 ~ /(^|[|;&(]|[ \t])(python3?|node|ruby|perl|php|deno|bun)([ \t]|$)/ \
61
+ && match($0, /[ \t]-[ce][ \t]/)) {
62
+ emit(substr($0, RSTART + RLENGTH))
63
+ }
64
+ # Тело heredoc: путь записи интерпретатора стоит именно там.
65
+ if (match($0, /<<-?[ \t]*[\047\"]?[A-Za-z_][A-Za-z0-9_]*/)) {
66
+ t = substr($0, RSTART, RLENGTH)
67
+ sub(/^<<-?[ \t]*[\047\"]?/, "", t)
68
+ tag = t
69
+ keep = ($0 ~ /(^|[|;&(]|[ \t])(python3?|node|ruby|perl|php|deno|bun)([ \t]|$)/)
70
+ }
71
+ }'
72
+ } | sort -u
24
73
  }
@@ -63,6 +63,11 @@
63
63
  ни умолчания: работа с очередью отказывает и говорит, где он задаётся. Пустой ключ хуже
64
64
  отсутствующей проверки — от имени остаётся огрызок, которому ничто не отвечает, и правильно
65
65
  названной не выглядит ни одна задача.
66
+ - **Заведённое в очереди работ видно той стороне, для которой заводилось.** Учётная запись, чьи
67
+ объекты хостинг показывает не всем, оставляет за собой работу, которой для остальных не
68
+ существует, — и снаружи это неотличимо от сделанного: задача заведена, PR открыт, а владелец
69
+ видит пустую очередь. Видимость проверяется чужими глазами — чтением очереди тем, кому она
70
+ адресована, — а не успешным ответом команды заведения.
66
71
  - **У задачи есть исполнитель с момента её заведения.** Задача без исполнителя выглядит ничьей: по
67
72
  очереди работ не видно, кто её взял, и заведённая по ходу правка теряется среди чужих.
68
73
  - **Состояние задачи в очереди работ отвечает тому, что с ней происходит.** Взятая в работу видна