@rt-tools/agent-kit 0.20.0 → 0.22.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 (78) hide show
  1. package/assets/checks/check-reuse.mjs +11 -1
  2. package/assets/checks/check-specs.mjs +8 -23
  3. package/assets/checks/signals/core.json +12 -2
  4. package/assets/checks/signals.mjs +17 -0
  5. package/assets/checks/spec-proposed.mjs +122 -0
  6. package/assets/hooks/browser-guard-device-id.sh +3 -0
  7. package/assets/hooks/browser-guard-no-asking.sh +3 -0
  8. package/assets/hooks/browser-guard-no-listing.sh +3 -0
  9. package/assets/hooks/browser-guard-no-other-drivers.sh +3 -0
  10. package/assets/hooks/browser-guard-require-select.sh +3 -0
  11. package/assets/hooks/claim-guard.sh +4 -0
  12. package/assets/hooks/conscience-guard.sh +4 -0
  13. package/assets/hooks/deny-tail.sh +12 -0
  14. package/assets/hooks/dev-server-guard.sh +3 -0
  15. package/assets/hooks/docs-guard.sh +4 -11
  16. package/assets/hooks/exam-guard.sh +3 -0
  17. package/assets/hooks/git-guard-delivery.sh +4 -4
  18. package/assets/hooks/git-guard-main.sh +4 -9
  19. package/assets/hooks/git-guard-push-tests.sh +3 -0
  20. package/assets/hooks/grill-gate.sh +5 -0
  21. package/assets/hooks/guard-note.sh +45 -0
  22. package/assets/hooks/handoff-entry-guard.sh +3 -0
  23. package/assets/hooks/handoff-write.sh +58 -7
  24. package/assets/hooks/hook-input.sh +1 -0
  25. package/assets/hooks/override-write-guard.sh +4 -4
  26. package/assets/hooks/postmortem-guard.sh +4 -0
  27. package/assets/hooks/proposal-guard.sh +11 -0
  28. package/assets/hooks/prose-style-guard.sh +3 -0
  29. package/assets/hooks/qa-dataid-guard.sh +3 -0
  30. package/assets/hooks/rerun-guard.sh +3 -0
  31. package/assets/hooks/reuse-first-guard.sh +3 -0
  32. package/assets/hooks/rule-article.sh +1 -1
  33. package/assets/hooks/rule-source-guard.sh +4 -4
  34. package/assets/hooks/sql-guard.sh +3 -0
  35. package/assets/hooks/task-context-load.sh +1 -1
  36. package/assets/hooks/task-flow-draft-guard.sh +3 -0
  37. package/assets/hooks/task-flow-guard.sh +3 -0
  38. package/assets/hooks/turn-entry-load.sh +27 -2
  39. package/assets/hooks/turn-exit-guard.sh +4 -0
  40. package/assets/hooks/waiting-turn-guard.sh +32 -10
  41. package/assets/hooks/window-fill-guard.sh +3 -0
  42. package/assets/hooks/work-start-guard.sh +5 -0
  43. package/assets/hooks/write-targets.sh +59 -10
  44. package/assets/laws/delivery.md +5 -0
  45. package/assets/laws/frontend-application.md +10 -0
  46. package/assets/laws/work-conduct.md +8 -0
  47. package/assets/patterns/browser-verification-stand.md +24 -9
  48. package/assets/patterns/git-workflow-pr.github.md +29 -22
  49. package/assets/patterns/spec-driven-domain.md +5 -0
  50. package/assets/patterns/spec-driven-rule.md +41 -1
  51. package/assets/patterns/task-flow-archive.md +12 -0
  52. package/assets/patterns/task-flow-handoff.md +14 -1
  53. package/assets/patterns/task-flow-resume.md +11 -0
  54. package/assets/patterns/task-flow-start.md +40 -40
  55. package/assets/patterns/testing-e2e.md +15 -0
  56. package/assets/patterns/testing-unit.md +13 -0
  57. package/assets/pitfalls/git-workflow.github.md +13 -0
  58. package/assets/pitfalls/spec-driven.md +20 -0
  59. package/assets/pitfalls/task-flow.md +78 -48
  60. package/assets/rules/git-workflow.azure.md +5 -0
  61. package/assets/rules/git-workflow.github.md +26 -23
  62. package/assets/rules/git-workflow.gitlab.md +5 -0
  63. package/assets/rules/spec-driven.md +23 -24
  64. package/assets/rules/turn-conduct.md +13 -15
  65. package/assets/rules/turn-entry.md +6 -4
  66. package/assets/rules/typescript-conventions.md +7 -0
  67. package/assets/skills/agent-kit-extend.md +11 -0
  68. package/assets/skills/agent-kit.md +12 -2
  69. package/lib/commands.d.ts.map +1 -1
  70. package/lib/commands.js +36 -1
  71. package/lib/commands.js.map +1 -1
  72. package/lib/observations.d.ts +13 -1
  73. package/lib/observations.d.ts.map +1 -1
  74. package/lib/observations.js +7 -1
  75. package/lib/observations.js.map +1 -1
  76. package/package.json +1 -1
  77. package/rt-tools-agent-kit-0.22.0.tgz +0 -0
  78. package/rt-tools-agent-kit-0.20.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
@@ -95,6 +95,7 @@ rt_turn_has_text() {
95
95
  got="$(tail -n 400 "$transcript" 2>/dev/null | jq -s -r '
96
96
  def is_input:
97
97
  .type == "user"
98
+ and ((.isCompactSummary // false) | not)
98
99
  and (((.message.content // []) | if type == "array"
99
100
  then ([.[] | select(.type == "tool_result")] | length)
100
101
  else 0 end) == 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
 
@@ -66,6 +69,7 @@ admitted_re='был неправ|был не прав|ошибс|моя ошиб
66
69
  verdict="$(tail -n 400 "$transcript" 2>/dev/null | jq -s -r --arg re "$admitted_re" '
67
70
  def is_input:
68
71
  .type == "user"
72
+ and ((.isCompactSummary // false) | not)
69
73
  and (((.message.content // []) | if type == "array"
70
74
  then ([.[] | select(.type == "tool_result")] | length)
71
75
  else 0 end) == 0);
@@ -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
 
@@ -60,10 +63,18 @@ context_re='пропозал|слою правил|слоя правил|сло
60
63
  # Ход — это всё, что записано после последнего настоящего ввода владельца. Ответ инструмента
61
64
  # приходит той же ролью, поэтому строки с `tool_result` вводом не считаются.
62
65
  #
66
+ # Сводка сжатия — тоже не ввод, хотя приходит ролью владельца и ответом инструмента не является.
67
+ # Она пересказывает ходы, которые уже кончились, и просьба, исполненная вчера, читается в ней как
68
+ # сказанная сейчас: ход, где о предложениях не было сказано ни слова, отбивался на пересказе
69
+ # чужой просьбы — каталог предложений при этом был пуст, и отправлять было нечего вовсе. По
70
+ # словам сводка от речи владельца неотличима и богаче её, потому что пересказывает всю сессию
71
+ # разом; узнаётся она признаком записи, а не текстом.
72
+ #
63
73
  # Хвост в 400 строк: запись хода растёт всю сессию, а судится только последний ход.
64
74
  verdict="$(tail -n 400 "$transcript" 2>/dev/null | jq -s -r --arg asked "$asked_re" --arg ctx "$context_re" '
65
75
  def is_input:
66
76
  .type == "user"
77
+ and ((.isCompactSummary // false) | not)
67
78
  and (((.message.content // []) | if type == "array"
68
79
  then ([.[] | select(.type == "tool_result")] | length)
69
80
  else 0 end) == 0);
@@ -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
 
@@ -194,6 +197,7 @@ started_re='task:new|task:move|board\.mjs[[:space:]]+move|git[[:space:]]+checkou
194
197
  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" '
195
198
  def is_input:
196
199
  .type == "user"
200
+ and ((.isCompactSummary // false) | not)
197
201
  and (((.message.content // []) | if type == "array"
198
202
  then ([.[] | select(.type == "tool_result")] | length)
199
203
  else 0 end) == 0);
@@ -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,10 +98,11 @@ 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"
105
+ and ((.isCompactSummary // false) | not)
88
106
  and (((.message.content // []) | if type == "array"
89
107
  then ([.[] | select(.type == "tool_result")] | length)
90
108
  else 0 end) == 0);
@@ -93,6 +111,10 @@ verdict="$(tail -n 400 "$transcript" 2>/dev/null | jq -s -r \
93
111
  | (if $i == null then [] else .[$i:] end) as $turn
94
112
  | [$turn[] | select(.type == "assistant") | (.message.content // [])[] | select(.type == "tool_use")] as $uses
95
113
  | ($uses | map((.input.command // "")) | join("\n")) as $ran
114
+ # Путь правки — второй способ завести папку задачи. Судятся только инструменты письма: у
115
+ # чтения путь тот же, а работой оно не является.
116
+ | ($uses | map(select(.name == "Write" or .name == "Edit" or .name == "MultiEdit")
117
+ | (.input.file_path // "")) | join("\n")) as $wrote
96
118
  # Ответ инструмента берётся текстом, а не в записи JSON: у той таб и кавычка уезжают
97
119
  # экранированными, и образец по ним не попадает вовсе. Приходит он и строкой, и списком
98
120
  # блоков — обе формы сводятся к одному тексту.
@@ -103,7 +125,7 @@ verdict="$(tail -n 400 "$transcript" 2>/dev/null | jq -s -r \
103
125
  else tostring end] | join("\n")) as $out
104
126
  | ($ran | test($opened; "i")) as $opened_pr
105
127
  | (($ran | test($read; "i")) and ($out | test($red; "i"))) as $red_run
106
- | ($ran | test($moved; "i")) as $went_on
128
+ | (($ran | test($taken; "i")) or ($wrote | test($taken_path; "i"))) as $went_on
107
129
  | ($ran | test($ready; "i")) as $checked
108
130
  | ([$turn[] | select(.type == "assistant") | (.message.content // [])[]
109
131
  | 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
 
@@ -66,6 +69,7 @@ rt_needs rt_is_app_code work-start-guard || exit 0
66
69
  asked="$(tail -n 400 "$transcript" 2>/dev/null | jq -s -r '
67
70
  def is_input:
68
71
  .type == "user"
72
+ and ((.isCompactSummary // false) | not)
69
73
  and (((.message.content // []) | if type == "array"
70
74
  then ([.[] | select(.type == "tool_result")] | length)
71
75
  else 0 end) == 0);
@@ -88,6 +92,7 @@ asked="$(tail -n 400 "$transcript" 2>/dev/null | jq -s -r '
88
92
  edited="$(tail -n 400 "$transcript" 2>/dev/null | jq -s -r '
89
93
  def is_input:
90
94
  .type == "user"
95
+ and ((.isCompactSummary // false) | not)
91
96
  and (((.message.content // []) | if type == "array"
92
97
  then ([.[] | select(.type == "tool_result")] | length)
93
98
  else 0 end) == 0);
@@ -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
  - **Состояние задачи в очереди работ отвечает тому, что с ней происходит.** Взятая в работу видна