@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
@@ -33,7 +33,17 @@ const SOURCE_ROOTS = CONFIG.sourceRoots;
33
33
  const SKIPPED_DIRS = CONFIG.skippedDirs;
34
34
  const BACKEND_ROOTS = CONFIG.backendRoots;
35
35
 
36
- const SIGNALS = loadSignals(CONFIG.reuse ?? {}, ROOT);
36
+ // Нехватка признаков отказ, а не пустая работа: проверка стоит в гейте пуша, и зелёный ответ
37
+ // без единого прочитанного файла неотличим от честного нуля. Отказ печатается строкой, а не
38
+ // стеком вызовов: читает его тот, кто настраивает дерево, а не тот, кто правит эту проверку.
39
+ let SIGNALS;
40
+ try {
41
+ SIGNALS = loadSignals(CONFIG.reuse ?? {}, ROOT);
42
+ } catch (failure) {
43
+ console.error(`check-reuse: ${failure.message}`);
44
+ console.error('\nПравило единообразия — скил `reuse-first`.');
45
+ process.exit(1);
46
+ }
37
47
 
38
48
  function count(text, expression) {
39
49
  return [...text.matchAll(expression)].length;
@@ -52,6 +52,7 @@ import { ROOT } from './rt-kit-checks.config.mjs';
52
52
  import { checkRuleImplementation, checkSpecLaws, checkTracedAnchors } from './spec-anchors.mjs';
53
53
  import { CONSTITUTION_DIR, REQUIRED_HEADINGS, SPECS_DIR, collectDomains, exists, problems, read, report, walk } from './spec-common.mjs';
54
54
  import { checkContract, checkRefusalCodes, procedureRootsOf } from './spec-contract.mjs';
55
+ import { proposedGroups, staleProposed } from './spec-proposed.mjs';
55
56
  import { collectReferences, parseScenarios, promisesScreen } from './spec-scenarios.mjs';
56
57
 
57
58
  // ── прогон ────────────────────────────────────────────────────────────────────
@@ -404,32 +405,16 @@ if (debts.length > 0) {
404
405
  uncovered.forEach((scenario) => console.log(` нет теста ${scenario.id} — ${scenario.title} (${scenario.file}:${scenario.line})`));
405
406
  }
406
407
 
407
- /**
408
- * Договорённость, по которой код уже написан, вливается в спек домена, а её директория
409
- * удаляется. Оставленная в главной ветке, она читается как предложенное и не выкаченное —
410
- * то есть как ложь о работающем месяц приложении.
411
- *
412
- * Признак — все сценарии директории покрыты тестами: пока хоть один помечен «Не покрыто»,
413
- * фича не дописана. Падением это не делается: на середине работы часть тестов уже есть, и
414
- * такая проверка краснела бы всю дорогу.
415
- */
416
- const proposed = new Map();
417
- for (const scenario of byId.values()) {
418
- const at = scenario.file.indexOf('/proposed/');
419
- if (at === -1) {
420
- continue;
421
- }
422
- const dir = scenario.file.slice(0, scenario.file.indexOf('/', at + '/proposed/'.length));
423
- const group = proposed.get(dir) ?? { total: 0, ready: 0 };
424
- group.total += 1;
425
- if (references.has(scenario.id) && !scenario.uncovered && !scenario.partial) {
426
- group.ready += 1;
427
- }
428
- proposed.set(dir, group);
429
- }
408
+ const proposed = proposedGroups(byId.values(), references);
430
409
 
431
410
  const ripe = [...proposed].filter(([, group]) => group.total > 0 && group.total === group.ready);
432
411
  if (ripe.length > 0) {
433
412
  console.log('\nПора вливать — сценарии закрыты тестами, договорённость ждёт переезда в спек домена:');
434
413
  ripe.forEach(([dir, group]) => console.log(` ${dir} — сценариев ${group.total}`));
435
414
  }
415
+
416
+ const stale = staleProposed([...proposed.keys()]);
417
+ if (stale.length > 0) {
418
+ console.log('\nЖдёт дольше месяца — привязка в договорённости стареет вместе с кодом, на который показывает:');
419
+ stale.forEach((record) => console.log(` ${record.dir} — ${Math.floor(record.ageDays)} суток без правок`));
420
+ }
@@ -8,11 +8,21 @@
8
8
  "find": "(window\\.)?(local|session)Storage\\.",
9
9
  "instead": "службы хранилища @rt-tools/core"
10
10
  },
11
- { "key": "raw-media-query", "ext": ".ts", "find": "matchMedia\\(", "instead": "BreakpointsService" },
11
+ {
12
+ "key": "raw-media-query",
13
+ "ext": ".ts",
14
+ "find": "(^|[^.\\w#$])matchMedia\\(",
15
+ "strip": "'[^'\\n]*'|\"[^\"\\n]*\"|`[^`]*`",
16
+ "skipBackendRoots": true,
17
+ "instead": "BreakpointsService"
18
+ },
12
19
  {
13
20
  "key": "raw-window",
14
21
  "ext": ".ts",
15
- "find": "(^|[^.\\w])(window|document)\\.",
22
+ "find": "(^|[^.\\w#$])(window|document)\\.",
23
+ "strip": "'[^'\\n]*'|\"[^\"\\n]*\"|`[^`]*`",
24
+ "cancel": "(const|let|var)\\s+(window|document)\\b|\\b(window|document)\\s*:\\s*[A-Za-z{(]|\\((window|document)\\s*:",
25
+ "skipBackendRoots": true,
16
26
  "instead": "PlatformService и токены среды @rt-tools/core"
17
27
  }
18
28
  ]
@@ -54,6 +54,14 @@ function readBundle(name) {
54
54
  * Совпавший ключ замещает пакетный: дерево вправе сказать о своём готовом точнее, чем пакет,
55
55
  * который его не видел. Файла своих признаков может не быть — это не отказ: пропуск дешевле
56
56
  * остановки работы, и объявляют его раньше, чем заводят.
57
+ *
58
+ * А вот пустой итог — отказ. Проверка стоит в гейте пуша и отвечает «расхождений 0 в 0
59
+ * признаках» с нулевым кодом: она объявляет успех, не прочитав ни одного файла, и отличить это
60
+ * от честного нуля по выводу нельзя — число признаков стоит в той же строке, что и число
61
+ * расхождений, и читается как подробность. При переезде с редакции, где признаки лежали в самой
62
+ * проверке, настройка не заведена ещё ни у кого: первый же прогон после обновления врёт молча у
63
+ * всех потребителей сразу. Судится итог, а не список наборов: дерево, ведущее только свои
64
+ * признаки, наборов пакета не берёт законно.
57
65
  */
58
66
  export function loadSignals(config, root) {
59
67
  const declared = config.bundles ?? [];
@@ -67,5 +75,14 @@ export function loadSignals(config, root) {
67
75
  byKey.set(signal.key, signal);
68
76
  }
69
77
 
78
+ if (byKey.size === 0) {
79
+ const known = bundleNames().join(', ') || 'ни одного';
80
+ throw new Error(
81
+ `признаков не объявлено ни одного: ни набора в «reuse.bundles», ни своего файла признаков. ` +
82
+ `Проверка прошла бы, не прочитав ни одного файла, и зелёный ответ означал бы только это. ` +
83
+ `Наборы при пакете: ${known}; свои признаки называются полем «reuse.signals».`
84
+ );
85
+ }
86
+
70
87
  return [...byKey.values()];
71
88
  }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Договорённости, ждущие переезда в спек домена.
3
+ *
4
+ * Договорённость, по которой код уже написан, вливается в спек домена, а её директория
5
+ * удаляется. Оставленная в главной ветке, она читается как предложенное и не выкаченное — то
6
+ * есть как ложь о работающем месяц приложении.
7
+ *
8
+ * Второй вопрос к ней — возраст. Привязка в договорённости стареет молча: объявление, на которое
9
+ * она показывает, переезжает вместе с соседней работой, а сверять договорённость с кодом никто
10
+ * не станет, пока она не вольётся. Три такие пролежали месяц, и промах нашёлся ровно при
11
+ * вливании — то есть в тот час, когда чинить его дороже всего.
12
+ *
13
+ * Ни то, ни другое падением не делается: на середине работы часть тестов уже есть, а сама работа
14
+ * законно идёт неделями — красная сверка отбивала бы пуш каждой ветки, включая ту, которая
15
+ * договорённость и дописывает.
16
+ */
17
+ import { execFileSync } from 'node:child_process';
18
+
19
+ import { ROOT } from './rt-kit-checks.config.mjs';
20
+ import { SPECS_DIR } from './spec-common.mjs';
21
+
22
+ /** За сколько суток без правок договорённость считается залежавшейся. */
23
+ const STALE_PROPOSED_DAYS = 30;
24
+
25
+ /** Сутки в миллисекундах — считать возраст удобнее в них. */
26
+ const DAY_MS = 24 * 60 * 60 * 1000;
27
+
28
+ /**
29
+ * Каталоги договорённостей со счётом их сценариев.
30
+ *
31
+ * Готовой считается та, у которой все сценарии закрыты тестами: пока хоть один помечен «Не
32
+ * покрыто», фича не дописана.
33
+ */
34
+ export function proposedGroups(scenarios, references) {
35
+ const proposed = new Map();
36
+
37
+ for (const scenario of scenarios) {
38
+ const at = scenario.file.indexOf('/proposed/');
39
+
40
+ if (at === -1) {
41
+ continue;
42
+ }
43
+
44
+ const dir = scenario.file.slice(0, scenario.file.indexOf('/', at + '/proposed/'.length));
45
+ const group = proposed.get(dir) ?? { total: 0, ready: 0 };
46
+ group.total += 1;
47
+
48
+ if (references.has(scenario.id) && !scenario.uncovered && !scenario.partial) {
49
+ group.ready += 1;
50
+ }
51
+
52
+ proposed.set(dir, group);
53
+ }
54
+
55
+ return proposed;
56
+ }
57
+
58
+ /**
59
+ * Дата последнего коммита у каждого каталога договорённости.
60
+ *
61
+ * Один проход по истории вместо вызова на каталог: лог идёт новыми вперёд, поэтому первая
62
+ * встреченная дата каталога и есть последняя. Каталог, которого в истории ещё нет, приехал этой
63
+ * же веткой и старым быть не может.
64
+ *
65
+ * Возраст меряется историей, а не временем файла на диске: свежий чекаут делает все каталоги
66
+ * одновременными, а дня в тексте договорённости нет вовсе. Тот же довод стоит у возраста записей
67
+ * описания прошлого.
68
+ */
69
+ function lastCommits(dirs) {
70
+ if (dirs.length === 0) {
71
+ return new Map();
72
+ }
73
+
74
+ let log;
75
+
76
+ try {
77
+ log = execFileSync('git', ['log', '--format=%cI', '--name-only', '--', SPECS_DIR], {
78
+ cwd: ROOT,
79
+ encoding: 'utf8',
80
+ maxBuffer: 64 * 1024 * 1024,
81
+ });
82
+ } catch {
83
+ return new Map();
84
+ }
85
+
86
+ const dates = new Map();
87
+ let current = null;
88
+
89
+ for (const line of log.split('\n')) {
90
+ if (line === '') {
91
+ continue;
92
+ }
93
+
94
+ const dir = dirs.find((candidate) => line.startsWith(`${candidate}/`));
95
+
96
+ if (dir === undefined) {
97
+ current = line.includes('/') ? current : line;
98
+ continue;
99
+ }
100
+
101
+ if (current !== null && !dates.has(dir)) {
102
+ dates.set(dir, current);
103
+ }
104
+ }
105
+
106
+ return dates;
107
+ }
108
+
109
+ /** Договорённости, лежащие без правок дольше срока: путь и возраст в сутках, старые впереди. */
110
+ export function staleProposed(dirs, now = Date.now()) {
111
+ const dates = lastCommits(dirs);
112
+
113
+ return dirs
114
+ .map((dir) => {
115
+ const committed = dates.get(dir);
116
+ const ageDays = committed === undefined ? 0 : (now - new Date(committed).getTime()) / DAY_MS;
117
+
118
+ return { dir, ageDays };
119
+ })
120
+ .filter((record) => record.ageDays > STALE_PROPOSED_DAYS)
121
+ .sort((one, other) => other.ageDays - one.ageDays);
122
+ }
@@ -11,6 +11,9 @@
11
11
  #
12
12
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: помощник не назвал профиль — пропуск.
13
13
 
14
+ # Своё имя в наблюдениях: отбой пишет общий хвост отказа, а не сам гард.
15
+ RT_GUARD_NAME=browser-guard-device-id
16
+
14
17
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
15
18
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
16
19
 
@@ -14,6 +14,9 @@
14
14
  #
15
15
  # FAIL-OPEN when browser-device-id.sh prints nothing.
16
16
 
17
+ # Своё имя в наблюдениях: отбой пишет общий хвост отказа, а не сам гард.
18
+ RT_GUARD_NAME=browser-guard-no-asking
19
+
17
20
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
18
21
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
19
22
 
@@ -10,6 +10,9 @@
10
10
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: помощник не назвал профиль — пропуск. Гард, который не может назвать
11
11
  # нужный профиль, ничего не предлагает взамен, и слепой отказ только заводил бы работу в тупик.
12
12
 
13
+ # Своё имя в наблюдениях: отбой пишет общий хвост отказа, а не сам гард.
14
+ RT_GUARD_NAME=browser-guard-no-listing
15
+
13
16
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
14
17
 
15
18
  cat >/dev/null 2>&1
@@ -19,6 +19,9 @@
19
19
  #
20
20
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: помощник не назвал профиль — пропуск.
21
21
 
22
+ # Своё имя в наблюдениях: отбой пишет общий хвост отказа, а не сам гард.
23
+ RT_GUARD_NAME=browser-guard-no-other-drivers
24
+
22
25
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
23
26
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
24
27
 
@@ -20,6 +20,9 @@
20
20
  #
21
21
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: помощник не назвал профиль — пропуск.
22
22
 
23
+ # Своё имя в наблюдениях: отбой пишет общий хвост отказа, а не сам гард.
24
+ RT_GUARD_NAME=browser-guard-require-select
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
 
@@ -31,6 +31,9 @@
31
31
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: при любой ошибке, нехватке `jq`, отсутствии записи хода или пустом
32
32
  # тексте ход РАЗРЕШАЕТСЯ (exit 0). Сломанный гард не имеет права заклинить разговор.
33
33
 
34
+ # Своё имя в наблюдениях: отбой пишет общий хвост отказа, а не сам гард.
35
+ RT_GUARD_NAME=claim-guard
36
+
34
37
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
35
38
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
36
39
 
@@ -19,6 +19,9 @@
19
19
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: нет `jq`, нет записи хода, роль молчит или отвечает не по форме — ход
20
20
  # разрешается. Сломанная совесть не имеет права заклинить разговор.
21
21
 
22
+ # Своё имя в наблюдениях: отбой пишет общий хвост отказа, а не сам гард.
23
+ RT_GUARD_NAME=conscience-guard
24
+
22
25
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
23
26
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
24
27
 
@@ -16,6 +16,13 @@
16
16
  #
17
17
  # ЧЕГО ХВОСТ НЕ ДЕЛАЕТ. Он не судит, годится ли обход в этом случае: это решает владелец. И он не
18
18
  # заменяет причины отказа — она стоит перед ним и называет промах поимённо.
19
+ #
20
+ # ЗАПИСЬ ОТБОЯ ИДЁТ ОТСЮДА ЖЕ. Хвост — единственное место, через которое проходит каждый отказ:
21
+ # правило дерева требует называть два законных хода при любом отказе, и гард, который отказывает
22
+ # мимо хвоста, нарушает его раньше, чем теряет счёт. Написанная в самом гарде, запись стояла в
23
+ # пяти гардах из тридцати, и сводка отвечала по этой пятой части. Гард объявляет себя строкой
24
+ # `RT_GUARD_NAME=<имя>` у себя вверху; не объявивший не пишет ничего — так своё отбитие считает
25
+ # сам гейт правил.
19
26
 
20
27
  # Хвост отказа строкой. Первый параметр — законная форма обхода, если она у отказа есть; пусто —
21
28
  # значит законной формы нет вовсе, и говорится об этом прямо.
@@ -23,6 +30,11 @@
23
30
  # reason="BLOCKED: <причина>. $(rt_deny_tail 'строка `Docs-skip: <причина>` в теле коммита')"
24
31
  # reason="BLOCKED: <причина>. $(rt_deny_tail)"
25
32
  rt_deny_tail() {
33
+ # shellcheck disable=SC1090
34
+ [ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/guard-note.sh" ] \
35
+ && . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/guard-note.sh" 2>/dev/null
36
+ command -v rt_guard_note >/dev/null 2>&1 && rt_guard_note
37
+
26
38
  if [ -n "$1" ]; then
27
39
  printf 'Ходов отсюда два: починить названное и повторить вызов — либо принести владельцу цену обхода и ждать его слова. Законная форма обхода: %s. Молча мимо гарда правка не кладётся: он отбивает один вызов, а обойдённый снимает требование со всего дерева и молчит об этом.' "$1"
28
40
  return 0
@@ -14,6 +14,9 @@
14
14
  # Где именно подняты приложения, знает профиль проекта: .claude/rt-kit/project.sh, переменная
15
15
  # RT_STANDS. Нет профиля — текст отказа остаётся общим, сам гард работает.
16
16
 
17
+ # Своё имя в наблюдениях: отбой пишет общий хвост отказа, а не сам гард.
18
+ RT_GUARD_NAME=dev-server-guard
19
+
17
20
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
18
21
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
19
22
 
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bash
2
2
  # rt-hook: PreToolUse Edit|Write|MultiEdit|NotebookEdit|Bash|mcp__webstorm__create_new_file|mcp__webstorm__execute_terminal_command|mcp__webstorm__execute_tool
3
- # Требует: hooks/profile-check.sh, hooks/deny-tail.sh
3
+ # Требует: hooks/profile-check.sh, hooks/deny-tail.sh, hooks/guard-note.sh
4
4
  # Гард пары «правка и её документ». PreToolUse.
5
5
  #
6
6
  # Расхождение кода с текстом беззвучно. Ни линтер, ни сборка, ни тесты не читают правила,
@@ -26,6 +26,9 @@
26
26
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: не репозиторий, нет разборщика, битый ввод, пустой список файлов —
27
27
  # пропуск.
28
28
 
29
+ # Своё имя в наблюдениях: отбой пишет общий хвост отказа, а не сам гард.
30
+ RT_GUARD_NAME=docs-guard
31
+
29
32
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
30
33
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
31
34
 
@@ -37,16 +40,6 @@ command -v jq >/dev/null 2>&1 || exit 0
37
40
  tool="$(rt_hook_tool)"
38
41
 
39
42
  decide() {
40
- # Наблюдение пишется только на отказе: подсказку гард раздаёт и там, где всё в порядке, и
41
- # счёт, в котором они смешаны, не значит ничего.
42
- if [ "$1" = "deny" ]; then
43
- # shellcheck disable=SC1090
44
- [ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/observe.sh" ] \
45
- && . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/observe.sh" 2>/dev/null
46
- command -v rt_note >/dev/null 2>&1 \
47
- && rt_note guard-deny res=docs-guard "sid=$(printf '%s' "$input" | jq -r '.session_id // "nosession"' 2>/dev/null)"
48
- fi
49
-
50
43
  reason="$2"
51
44
  if [ "$1" = "deny" ]; then
52
45
  # shellcheck disable=SC1090
@@ -33,6 +33,9 @@
33
33
  # FAIL-OPEN: нет jq, нет записи хода, чужой инструмент → пропуск. Сломанный гард не должен
34
34
  # мешать работать.
35
35
 
36
+ # Своё имя в наблюдениях: отбой пишет общий хвост отказа, а не сам гард.
37
+ RT_GUARD_NAME=exam-guard
38
+
36
39
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
37
40
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
38
41
 
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bash
2
2
  # rt-hook: PreToolUse Bash|mcp__webstorm__execute_terminal_command|mcp__webstorm__execute_tool
3
- # Требует: hooks/git-guard-delivery-folder.sh, hooks/git-guard-delivery-conflict.sh, hooks/profile-check.sh, hooks/deny-tail.sh
3
+ # Требует: hooks/git-guard-delivery-folder.sh, hooks/git-guard-delivery-conflict.sh, hooks/profile-check.sh, hooks/deny-tail.sh, hooks/guard-note.sh
4
4
  # Гард поставки. PreToolUse на заведении ветки, пуше и открытии заявки на слияние.
5
5
  #
6
6
  # Закон о поставке требует трёх вещей, которых обычно не проверяет ничто: правка начинается с
@@ -39,6 +39,9 @@
39
39
  #
40
40
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: не репозиторий, нет разборщика, битый ввод, нет профиля — пропуск.
41
41
 
42
+ # Своё имя в наблюдениях: отбой пишет общий хвост отказа, а не сам гард.
43
+ RT_GUARD_NAME=git-guard-delivery
44
+
42
45
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
43
46
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
44
47
 
@@ -131,9 +134,6 @@ folder_skip_re='(^|#)[[:space:]]*Task-folder-skip:[[:space:]]*[^[:space:]<"'"'"'
131
134
  deny() {
132
135
  # Отказ гарда — наблюдение: гард, отбивающий чаще прочих, говорит, какое место поставки раз
133
136
  # за разом делают не так. Текст отказа туда не идёт: в нём номера задач и имена веток дерева.
134
- # shellcheck disable=SC1090
135
- [ -f "$rt_hooks_dir/observe.sh" ] && . "$rt_hooks_dir/observe.sh" 2>/dev/null
136
- command -v rt_note >/dev/null 2>&1 && rt_note guard-deny res=git-guard-delivery "sid=$sid"
137
137
 
138
138
  # Хвост отказа: два законных хода и форма обхода вторым параметром.
139
139
  # shellcheck disable=SC1090
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bash
2
2
  # rt-hook: PreToolUse Bash|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
  # Гард главной ветки. PreToolUse на вызове коммита.
5
5
  #
6
6
  # Коммит в главную ветку минует ветку, PR и разбор, а поставка построена на них целиком —
@@ -13,6 +13,9 @@
13
13
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: не репозиторий, нет гита, открепившийся HEAD, битый ввод — пропуск.
14
14
  # Сломанный гард не должен мешать работать.
15
15
 
16
+ # Своё имя в наблюдениях: отбой пишет общий хвост отказа, а не сам гард.
17
+ RT_GUARD_NAME=git-guard-main
18
+
16
19
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
17
20
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
18
21
 
@@ -72,14 +75,6 @@ fi
72
75
 
73
76
  reason="Отбито: коммит прямо в «${default}». Работа едет через ветку и PR — правило git-workflow. Заведи ветку отдельным вызовом и коммить в неё: подготовленные изменения при этом сохранятся. Если коммит в ${default} действительно нужен — спроси владельца, сам не обходи."
74
77
 
75
- # Отказ — наблюдение. Имя главной ветки в него не идёт: у деревьев оно своё, а счёт отказов
76
- # одинаков везде.
77
- rt_hooks_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
78
- # shellcheck disable=SC1090
79
- [ -f "$rt_hooks_dir/observe.sh" ] && . "$rt_hooks_dir/observe.sh" 2>/dev/null
80
- command -v rt_note >/dev/null 2>&1 \
81
- && rt_note guard-deny res=git-guard-main "sid=$(printf '%s' "$input" | jq -r '.session_id // "nosession"' 2>/dev/null)"
82
-
83
78
  # Общий хвост отказа: два законных хода и законная форма обхода, если она у отказа есть.
84
79
  # Файл может быть не разложен — тогда хвоста нет, а причина отказа остаётся прежней.
85
80
  # shellcheck disable=SC1090
@@ -25,6 +25,9 @@
25
25
  #
26
26
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: не репозиторий, битый ввод, нет профиля — пропуск.
27
27
 
28
+ # Своё имя в наблюдениях: отбой пишет общий хвост отказа, а не сам гард.
29
+ RT_GUARD_NAME=git-guard-push-tests
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
 
@@ -33,6 +33,9 @@
33
33
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: при любой ошибке, отсутствии записи хода и повторном заходе ход
34
34
  # РАЗРЕШАЕТСЯ (exit 0). Сломанный гард не имеет права заклинить разговор.
35
35
 
36
+ # Своё имя в наблюдениях: отбой пишет общий хвост отказа, а не сам гард.
37
+ RT_GUARD_NAME=grill-gate
38
+
36
39
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
37
40
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
38
41
 
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env bash
2
+ # Запись отбоя гарда. НЕ гард: объявления `rt-hook:` у него нет, к событиям агента он не
3
+ # подключается. Его источает общий хвост отказа — единственное место, через которое проходит
4
+ # каждый отказ.
5
+ #
6
+ # Зачем он есть. Запись отбоя стояла в самом гарде пятью строками: подключение записи, проверка
7
+ # наличия функции, вызов с именем и признаком сессии. Написать их в каждом гарде не вышло — из
8
+ # тридцати их написали в пяти, и сводка наблюдений отвечала по этой пятой части: гард, который
9
+ # отбивать умеет, а записывать нет, не значится в ней ни отбивавшим, ни молчащим, и вопрос «стоял
10
+ # и не работал» по нему не задать вовсе.
11
+ #
12
+ # КАК ГАРД ЗАЯВЛЯЕТ О СЕБЕ. Одной строкой `RT_GUARD_NAME=<имя>` у себя вверху. Заявка добровольна
13
+ # и точна: хук, который своё отбитие считает сам — как гейт правил, — её не ставит, и второй раз
14
+ # то же событие в счёт не идёт.
15
+ #
16
+ # ЧТО ЗАПИСЫВАЕТСЯ. Имя гарда и признак сессии. Чистит значения и решает, писать ли вообще, сама
17
+ # запись наблюдений; здесь только сборка вызова.
18
+ #
19
+ # ОТКАЗ В ПОЛЬЗУ РАБОТЫ. Записи может не быть вовсе — файла нет, дерево выключило наблюдения,
20
+ # каталог недоступен. Гард, упавший на записи отбоя, останавливал бы работу ради статистики.
21
+
22
+ # Отбой гарда в наблюдения. Имя берётся из `RT_GUARD_NAME`, признак сессии — из ввода хука;
23
+ # без имени не пишется ничего.
24
+ rt_guard_note() {
25
+ local rt_gn_dir rt_gn_sid rt_gn_name
26
+ # Умолчание, а не голое чтение: гард бывает написан с обрывом на необъявленной переменной, и
27
+ # запись, читающая её голой, роняет такой гард целиком — на каждом вызове, а не на отказе.
28
+ rt_gn_name="${RT_GUARD_NAME:-}"
29
+ [ -n "$rt_gn_name" ] || return 0
30
+
31
+ rt_gn_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
32
+ # shellcheck disable=SC1090
33
+ [ -f "$rt_gn_dir/observe.sh" ] && . "$rt_gn_dir/observe.sh" 2>/dev/null
34
+ command -v rt_note >/dev/null 2>&1 || return 0
35
+
36
+ rt_gn_sid="$(printf '%s' "${RT_HOOK_INPUT:-}" | jq -r '.session_id // empty' 2>/dev/null)"
37
+
38
+ if [ -n "$rt_gn_sid" ]; then
39
+ rt_note guard-deny "res=$rt_gn_name" "sid=$rt_gn_sid"
40
+ else
41
+ rt_note guard-deny "res=$rt_gn_name"
42
+ fi
43
+
44
+ return 0
45
+ }
@@ -14,6 +14,9 @@
14
14
  #
15
15
  # FAIL-OPEN: нет `jq`, нет записи хода, передачи в реплике нет → пропуск.
16
16
 
17
+ # Своё имя в наблюдениях: отбой пишет общий хвост отказа, а не сам гард.
18
+ RT_GUARD_NAME=handoff-entry-guard
19
+
17
20
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
18
21
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
19
22