@rt-tools/agent-kit 0.20.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 (73) 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 +3 -0
  12. package/assets/hooks/conscience-guard.sh +3 -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 +3 -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/override-write-guard.sh +4 -4
  25. package/assets/hooks/postmortem-guard.sh +3 -0
  26. package/assets/hooks/proposal-guard.sh +3 -0
  27. package/assets/hooks/prose-style-guard.sh +3 -0
  28. package/assets/hooks/qa-dataid-guard.sh +3 -0
  29. package/assets/hooks/rerun-guard.sh +3 -0
  30. package/assets/hooks/reuse-first-guard.sh +3 -0
  31. package/assets/hooks/rule-article.sh +1 -1
  32. package/assets/hooks/rule-source-guard.sh +4 -4
  33. package/assets/hooks/sql-guard.sh +3 -0
  34. package/assets/hooks/task-context-load.sh +1 -1
  35. package/assets/hooks/task-flow-draft-guard.sh +3 -0
  36. package/assets/hooks/task-flow-guard.sh +3 -0
  37. package/assets/hooks/turn-entry-load.sh +27 -2
  38. package/assets/hooks/turn-exit-guard.sh +3 -0
  39. package/assets/hooks/waiting-turn-guard.sh +31 -10
  40. package/assets/hooks/window-fill-guard.sh +3 -0
  41. package/assets/hooks/work-start-guard.sh +3 -0
  42. package/assets/hooks/write-targets.sh +59 -10
  43. package/assets/laws/delivery.md +5 -0
  44. package/assets/laws/work-conduct.md +5 -0
  45. package/assets/patterns/browser-verification-stand.md +24 -9
  46. package/assets/patterns/git-workflow-pr.github.md +29 -22
  47. package/assets/patterns/spec-driven-domain.md +5 -0
  48. package/assets/patterns/spec-driven-rule.md +41 -1
  49. package/assets/patterns/task-flow-archive.md +12 -0
  50. package/assets/patterns/task-flow-handoff.md +14 -1
  51. package/assets/patterns/task-flow-resume.md +11 -0
  52. package/assets/patterns/task-flow-start.md +40 -40
  53. package/assets/patterns/testing-e2e.md +15 -0
  54. package/assets/patterns/testing-unit.md +13 -0
  55. package/assets/pitfalls/git-workflow.github.md +13 -0
  56. package/assets/pitfalls/spec-driven.md +20 -0
  57. package/assets/pitfalls/task-flow.md +78 -48
  58. package/assets/rules/spec-driven.md +23 -24
  59. package/assets/rules/turn-conduct.md +13 -15
  60. package/assets/rules/turn-entry.md +6 -4
  61. package/assets/rules/typescript-conventions.md +7 -0
  62. package/assets/skills/agent-kit-extend.md +11 -0
  63. package/assets/skills/agent-kit.md +12 -2
  64. package/lib/commands.d.ts.map +1 -1
  65. package/lib/commands.js +36 -1
  66. package/lib/commands.js.map +1 -1
  67. package/lib/observations.d.ts +13 -1
  68. package/lib/observations.d.ts.map +1 -1
  69. package/lib/observations.js +7 -1
  70. package/lib/observations.js.map +1 -1
  71. package/package.json +1 -1
  72. package/rt-tools-agent-kit-0.21.0.tgz +0 -0
  73. 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
@@ -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
 
@@ -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
 
@@ -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
  - **Состояние задачи в очереди работ отвечает тому, что с ней происходит.** Взятая в работу видна
@@ -32,6 +32,11 @@
32
32
  «здесь нет», «этого не заводили», «такого файла не бывает» — произносится только как вывод
33
33
  команды. Не проверенное отрицание опаснее вопроса: вопрос владелец поправит, а факт от
34
34
  исполнителя примет на веру, потому что тот в дерево смотрит.
35
+ - **Пустой вывод утверждением не бывает.** Он значит либо «ноль строк», либо «спрошено не то», и
36
+ различает эти два только повторный вызов — без конвейера и без глушения потока ошибок: в
37
+ конвейере код возврата принадлежит последнему звену, а заглушённый поток уносит текст промаха.
38
+ Пока такой вызов не сделан, о дереве не сказано ничего, а «ничего нет» — самая дорогая из
39
+ возможных ошибок: она отменяет работу, которую владелец велел сделать.
35
40
  - **Отрицание, полученное одним источником, отрицанием не является.** Ответ «не найдено», отказ в
36
41
  доступе и пустой список говорят о правах спрашивающего, а не о предмете: тот же вопрос, заданный
37
42
  оттуда, где предмет виден, отвечает обратным. Сказать «этого нет» можно только после второго
@@ -54,15 +54,26 @@ npx nx build admin --base-href=/
54
54
  Angular DevTools, нужна ещё и dev-конфигурация (`--configuration=development`): прод-сборка не
55
55
  публикует `window.ng`. Выводы о размере бандла и минификации на такой сборке делать нельзя.
56
56
 
57
- Сессия кладётся в `localStorage['<ключ сессии админки>']` **строкой JSON** (`JSON.stringify(token)`),
58
- иначе приложение её не прочитает. Токен не подписывается руками, а берётся у живого API:
59
- `POST /<область>.v1.AuthService/Login`. Команду с паролем классификатор блокирует обходить не
60
- надо, спрашивать разрешение у владельца.
61
-
62
- Взять уже открытую сессию нельзя: чтение того же ключа из браузера
63
- блокируется. Оба пути к своему стенду упираются в пароль, поэтому остаётся третий — смотреть
64
- на админке владельца, где вход уже сделан. Свой стенд нужен, только когда проверяют
65
- прод-сборку, `--base-href` или конфиг nginx; чтобы просто посмотреть экраны, он не нужен.
57
+ Собранное отдаётся прокси дерева с его настоящим конфигом, а не статическим сервером. Причин
58
+ две, и обе всплывают в первую минуту: приложение ходит за процедурами на свой же origin, а по
59
+ глубокой ссылке статический сервер отвечает `404`маршрутов одностраничного приложения он не
60
+ знает и на его точку входа не откатывается. Гард дев-серверов такой сервер отбивает наравне с
61
+ `nx serve`, и это указание на верный ход, а не помеха: готовый вызов — раздел «Стенд под
62
+ настоящим nginx» ниже, он и отдаёт собранное, и проксирует процедуры.
63
+
64
+ Вход делается формой, как у человека. Учётная запись местной базы разработки не секрет
65
+ владельца: её кладёт туда засев того же дерева, а пароль лежит в файле окружения рядом.
66
+ Разрешения на неё не спрашивают — спрашивают про боевую запись, а её на местном стенде не
67
+ бывает вовсе. Там, где приёмника рядом нет, ответ входа подменяет сам стенд: путь у приложения
68
+ остаётся тот же, что у человека.
69
+
70
+ Токен руками не подписывается и в хранилище браузера руками не кладётся: положенный так, он
71
+ проверяет экран, миновав живой путь входа, — то есть отвечает не на тот вопрос. Просьба к
72
+ человеку набрать пароль, открыть вкладку или нажать кнопку означает неверно выбранный путь, а
73
+ не нехватку прав у исполнителя.
74
+
75
+ Стенд владельца запасным путём не бывает: он собирает главную ветку и о правке в рабочем дереве
76
+ не говорит ничего.
66
77
 
67
78
  ## Стенд API
68
79
 
@@ -163,6 +174,10 @@ done
163
174
  это по её длительности.
164
175
  - Свой дев-сервер не поднимать: сайт на {{sitePort}}, админка на {{adminPort}}, API на {{apiPort}} уже подняты
165
176
  владельцем, и второй экземпляр отбивается гардом.
177
+ - **Отбитый статический сервер читается как запрет проверки, а он указание на верный ход.**
178
+ Отдавать собранное им нельзя не из осторожности: приложение ходит на свой origin, а глубокая
179
+ ссылка отвечает `404`. Отсюда идут не к обходу гарда, а к прокси дерева с его настоящим
180
+ конфигом.
166
181
  - **Дев-сервер владельца — место, где смотрят, а не место, где проверяют.** Вывод о том, что
167
182
  экран верен, делается на прод-сборке за настоящим прокси: разметка от сервера, локали, кэш,
168
183
  перенаправления и заголовки на дев-сервере либо другие, либо отсутствуют вовсе. Взгляд на