@rt-tools/agent-kit 0.11.0 → 0.13.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 (138) hide show
  1. package/assets/checks/board-runs.github.mjs +87 -0
  2. package/assets/checks/board.github.mjs +0 -40
  3. package/assets/checks/check-board.github.mjs +39 -8
  4. package/assets/checks/check-file-size.mjs +19 -4
  5. package/assets/checks/check-schema-drift.mjs +28 -5
  6. package/assets/checks/check-state-next.mjs +10 -2
  7. package/assets/checks/rt-kit-checks.config.mjs +28 -2
  8. package/assets/commands/feedback.md +8 -0
  9. package/assets/defaults/project.sh +59 -6
  10. package/assets/defaults/turn-map.md +8 -6
  11. package/assets/hooks/browser-guard-device-id.sh +3 -1
  12. package/assets/hooks/browser-guard-no-asking.sh +3 -1
  13. package/assets/hooks/browser-guard-no-other-drivers.sh +6 -4
  14. package/assets/hooks/browser-guard-require-select.sh +4 -2
  15. package/assets/hooks/claim-guard.sh +3 -1
  16. package/assets/hooks/conscience-guard.sh +3 -1
  17. package/assets/hooks/dev-server-guard.sh +6 -4
  18. package/assets/hooks/dispatch.sh +69 -0
  19. package/assets/hooks/docs-guard.sh +6 -4
  20. package/assets/hooks/exam-guard.sh +5 -3
  21. package/assets/hooks/git-guard-delivery-folder.sh +99 -0
  22. package/assets/hooks/git-guard-delivery-signature.sh +10 -4
  23. package/assets/hooks/git-guard-delivery.sh +58 -74
  24. package/assets/hooks/git-guard-main.sh +6 -4
  25. package/assets/hooks/git-guard-push-tests.sh +8 -6
  26. package/assets/hooks/grill-gate.sh +4 -2
  27. package/assets/hooks/handoff-entry-guard.sh +4 -2
  28. package/assets/hooks/handoff-write.sh +27 -6
  29. package/assets/hooks/hook-input.sh +77 -0
  30. package/assets/hooks/lint-after-edit.sh +5 -3
  31. package/assets/hooks/override-write-guard.sh +107 -0
  32. package/assets/hooks/postmortem-guard.sh +3 -1
  33. package/assets/hooks/proposal-guard.sh +3 -1
  34. package/assets/hooks/prose-style-guard.sh +5 -3
  35. package/assets/hooks/qa-dataid-guard.sh +4 -2
  36. package/assets/hooks/rerun-guard.sh +6 -4
  37. package/assets/hooks/reuse-first-guard.sh +5 -3
  38. package/assets/hooks/rule-article.sh +99 -0
  39. package/assets/hooks/rule-source-guard.sh +126 -0
  40. package/assets/hooks/skill-gate-rearm.sh +3 -1
  41. package/assets/hooks/skill-gate.sh +23 -2
  42. package/assets/hooks/skill-loaded.sh +3 -1
  43. package/assets/hooks/sql-guard-request.sh +2 -1
  44. package/assets/hooks/sql-guard.sh +4 -2
  45. package/assets/hooks/task-flow-guard.sh +24 -4
  46. package/assets/hooks/turn-exit-guard.sh +59 -17
  47. package/assets/hooks/waiting-turn-guard.sh +3 -1
  48. package/assets/hooks/window-fill-guard.sh +6 -4
  49. package/assets/laws/work-conduct.md +5 -9
  50. package/assets/patterns/dependencies-upgrade.md +1 -1
  51. package/assets/patterns/doc-style-write.md +3 -3
  52. package/assets/patterns/git-workflow-commit.azure.md +2 -202
  53. package/assets/patterns/git-workflow-commit.github.md +2 -258
  54. package/assets/patterns/git-workflow-commit.gitlab.md +1 -217
  55. package/assets/patterns/git-workflow-docker.md +3 -3
  56. package/assets/patterns/git-workflow-merge.md +3 -2
  57. package/assets/patterns/git-workflow-migration.md +3 -3
  58. package/assets/patterns/git-workflow-pr.azure.md +224 -0
  59. package/assets/patterns/git-workflow-pr.github.md +280 -0
  60. package/assets/patterns/git-workflow-pr.gitlab.md +240 -0
  61. package/assets/patterns/git-workflow-restart.md +3 -3
  62. package/assets/patterns/git-workflow-secrets.md +3 -3
  63. package/assets/patterns/task-flow-archive.md +195 -0
  64. package/assets/patterns/task-flow-close.md +72 -240
  65. package/assets/patterns/task-flow-handoff.md +4 -4
  66. package/assets/patterns/task-flow-resume.md +5 -3
  67. package/assets/pitfalls/agent-kit.md +80 -0
  68. package/assets/pitfalls/doc-style.md +80 -0
  69. package/assets/pitfalls/git-workflow.azure.md +50 -0
  70. package/assets/pitfalls/git-workflow.github.md +78 -0
  71. package/assets/pitfalls/git-workflow.gitlab.md +49 -0
  72. package/assets/pitfalls/spec-driven.md +36 -0
  73. package/assets/pitfalls/styling-bem.md +45 -0
  74. package/assets/pitfalls/task-flow.md +62 -0
  75. package/assets/pitfalls/testing.md +70 -0
  76. package/assets/rules/deploy-flow.azure.md +114 -0
  77. package/assets/rules/deploy-flow.github.md +122 -0
  78. package/assets/rules/deploy-flow.gitlab.md +116 -0
  79. package/assets/rules/doc-style.md +25 -76
  80. package/assets/rules/git-workflow.azure.md +6 -92
  81. package/assets/rules/git-workflow.github.md +20 -128
  82. package/assets/rules/git-workflow.gitlab.md +6 -93
  83. package/assets/rules/spec-driven.md +39 -30
  84. package/assets/rules/styling-bem.md +20 -39
  85. package/assets/rules/task-flow.md +56 -222
  86. package/assets/rules/testing.md +3 -64
  87. package/assets/rules/turn-conduct.md +210 -0
  88. package/assets/rules/typescript-conventions.md +15 -0
  89. package/assets/skills/agent-kit.md +67 -89
  90. package/assets/templates/pitfalls.md +10 -0
  91. package/assets/templates/proposal.md +16 -1
  92. package/assets/templates/rule.md +5 -3
  93. package/bin/agent-kit.d.ts.map +1 -1
  94. package/bin/agent-kit.js +1 -42
  95. package/bin/agent-kit.js.map +1 -1
  96. package/lib/assets.d.ts.map +1 -1
  97. package/lib/assets.js +6 -1
  98. package/lib/assets.js.map +1 -1
  99. package/lib/cascade.d.ts.map +1 -1
  100. package/lib/cascade.js +19 -1
  101. package/lib/cascade.js.map +1 -1
  102. package/lib/commands.d.ts.map +1 -1
  103. package/lib/commands.js +1 -0
  104. package/lib/commands.js.map +1 -1
  105. package/lib/config.d.ts +16 -1
  106. package/lib/config.d.ts.map +1 -1
  107. package/lib/config.js +8 -0
  108. package/lib/config.js.map +1 -1
  109. package/lib/hooks-map.d.ts +13 -0
  110. package/lib/hooks-map.d.ts.map +1 -1
  111. package/lib/hooks-map.js +33 -1
  112. package/lib/hooks-map.js.map +1 -1
  113. package/lib/proposals.d.ts +5 -1
  114. package/lib/proposals.d.ts.map +1 -1
  115. package/lib/proposals.js +74 -5
  116. package/lib/proposals.js.map +1 -1
  117. package/lib/ship.d.ts +1 -2
  118. package/lib/ship.d.ts.map +1 -1
  119. package/lib/ship.js +0 -54
  120. package/lib/ship.js.map +1 -1
  121. package/lib/shipment.d.ts.map +1 -1
  122. package/lib/shipment.fixture.d.ts +39 -0
  123. package/lib/shipment.fixture.d.ts.map +1 -0
  124. package/lib/shipment.fixture.js +99 -0
  125. package/lib/shipment.fixture.js.map +1 -0
  126. package/lib/shipment.js +59 -7
  127. package/lib/shipment.js.map +1 -1
  128. package/package.json +1 -1
  129. package/rt-tools-agent-kit-0.13.0.tgz +0 -0
  130. package/assets/commands/agent-kit-digest.md +0 -89
  131. package/assets/commands/rules-review.md +0 -98
  132. package/assets/patterns/cargo-triage-mark.md +0 -119
  133. package/assets/rules/cargo-triage.md +0 -126
  134. package/lib/cargo-state.d.ts +0 -62
  135. package/lib/cargo-state.d.ts.map +0 -1
  136. package/lib/cargo-state.js +0 -118
  137. package/lib/cargo-state.js.map +0 -1
  138. package/rt-tools-agent-kit-0.11.0.tgz +0 -0
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Состояние прогонов и выкатки у хостинга: что встало на вершине, чем кончилось и на сколько
3
+ * прод отстал от главной ветки.
4
+ *
5
+ * Отдельным файлом, а не внутри работы с очередью: у очереди свой предмет — задачи, колонки и
6
+ * заявки, — а здесь спрашивают конвейер. Вместе они переросли предел длины файла, и делить их
7
+ * по предмету дешевле, чем по числу строк: правку прогонов и правку очереди делают разные
8
+ * работы.
9
+ *
10
+ * Нет сети или нет токена — вызовы бросают `OfflineError`, как и остальная работа с хостингом:
11
+ * невозможность спросить расхождением не считается.
12
+ */
13
+ import { OWNER, REPO, gh } from './board.mjs';
14
+
15
+ /**
16
+ * Сколько прогонов завелось на этой вершине.
17
+ *
18
+ * Спрашивается вершина, а не ветка: прогон промежуточного коммита о состоянии вершины не
19
+ * говорит ничего, а список прогонов ветки отдаёт их вперемешку.
20
+ */
21
+ export function runsOnHead(sha, options) {
22
+ const answer = gh(['api', `repos/${OWNER}/${REPO}/actions/runs?head_sha=${sha}&per_page=1`, '--jq', '.total_count'], options);
23
+ return Number(String(answer).trim());
24
+ }
25
+
26
+ /**
27
+ * Коммит последней успешной выкатки и то, на сколько от него ушла главная ветка.
28
+ *
29
+ * Спрашивается сама выкатка, а не последний прогон главной ветки. Там, где выкатку запускают
30
+ * рукой, слияние прода не двигает вовсе, и прогон главной о нём не говорит ничего: прод
31
+ * отставал на 476 коммитов, а сверка молчала. Судится только завершённая успехом выкатка —
32
+ * идущая ещё может кончиться чем угодно.
33
+ *
34
+ * Возвращает `null`, если выкаток не было ни одной: это не расхождение, а нечего сравнивать.
35
+ */
36
+ export function deployLag(workflow, mainBranch, options) {
37
+ const runs = gh(
38
+ [
39
+ 'api',
40
+ `repos/${OWNER}/${REPO}/actions/workflows/${encodeURIComponent(workflow)}/runs` +
41
+ '?status=success&per_page=1',
42
+ '--jq',
43
+ '[.workflow_runs[] | {sha: .head_sha, at: .created_at}] | first // empty',
44
+ ],
45
+ options
46
+ );
47
+ const last = String(runs).trim();
48
+ if (!last) {
49
+ return null;
50
+ }
51
+
52
+ const run = JSON.parse(last);
53
+ const behind = gh(
54
+ ['api', `repos/${OWNER}/${REPO}/compare/${run.sha}...${encodeURIComponent(mainBranch)}`, '--jq', '.ahead_by'],
55
+ options
56
+ );
57
+ return { sha: run.sha, at: run.at, behind: Number(String(behind).trim()) };
58
+ }
59
+
60
+ /**
61
+ * Чем кончились прогоны на этой вершине: `success`, если все завершились успехом, `running`,
62
+ * если хоть один ещё идёт, `failure` — если хоть один упал. Прогонов нет вовсе — `none`.
63
+ *
64
+ * Цвет спрашивается отдельно от факта: факт отвечает на вопрос «событие дошло», цвет — на
65
+ * вопрос «работу можно отдавать». Второй вопрос задаётся там, где готовое стоит черновиком.
66
+ */
67
+ export function verdictOnHead(sha, options) {
68
+ const answer = gh(
69
+ [
70
+ 'api',
71
+ `repos/${OWNER}/${REPO}/actions/runs?head_sha=${sha}&per_page=20`,
72
+ '--jq',
73
+ '[.workflow_runs[] | {status, conclusion}] | if length == 0 then "none"' +
74
+ ' elif any(.status != "completed") then "running"' +
75
+ ' elif any(.conclusion != "success") then "failure"' +
76
+ ' else "success" end',
77
+ ],
78
+ options
79
+ );
80
+ return String(answer).trim();
81
+ }
82
+
83
+ /** Когда вершина легла в ветку — по времени коммита у хостинга, а не по местным часам ветки. */
84
+ export function headCommittedAt(sha, options) {
85
+ const answer = gh(['api', `repos/${OWNER}/${REPO}/commits/${sha}`, '--jq', '.commit.committer.date'], options);
86
+ return Date.parse(String(answer).trim());
87
+ }
@@ -256,46 +256,6 @@ export function fetchOpenPulls(options) {
256
256
  );
257
257
  }
258
258
 
259
- /**
260
- * Сколько прогонов завелось на этой вершине.
261
- *
262
- * Спрашивается вершина, а не ветка: прогон промежуточного коммита о состоянии вершины не
263
- * говорит ничего, а список прогонов ветки отдаёт их вперемешку.
264
- */
265
- export function runsOnHead(sha, options) {
266
- const answer = gh(['api', `repos/${OWNER}/${REPO}/actions/runs?head_sha=${sha}&per_page=1`, '--jq', '.total_count'], options);
267
- return Number(String(answer).trim());
268
- }
269
-
270
- /**
271
- * Чем кончились прогоны на этой вершине: `success`, если все завершились успехом, `running`,
272
- * если хоть один ещё идёт, `failure` — если хоть один упал. Прогонов нет вовсе — `none`.
273
- *
274
- * Цвет спрашивается отдельно от факта: факт отвечает на вопрос «событие дошло», цвет — на
275
- * вопрос «работу можно отдавать». Второй вопрос задаётся там, где готовое стоит черновиком.
276
- */
277
- export function verdictOnHead(sha, options) {
278
- const answer = gh(
279
- [
280
- 'api',
281
- `repos/${OWNER}/${REPO}/actions/runs?head_sha=${sha}&per_page=20`,
282
- '--jq',
283
- '[.workflow_runs[] | {status, conclusion}] | if length == 0 then "none"' +
284
- ' elif any(.status != "completed") then "running"' +
285
- ' elif any(.conclusion != "success") then "failure"' +
286
- ' else "success" end',
287
- ],
288
- options
289
- );
290
- return String(answer).trim();
291
- }
292
-
293
- /** Когда вершина легла в ветку — по времени коммита у хостинга, а не по местным часам ветки. */
294
- export function headCommittedAt(sha, options) {
295
- const answer = gh(['api', `repos/${OWNER}/${REPO}/commits/${sha}`, '--jq', '.commit.committer.date'], options);
296
- return Date.parse(String(answer).trim());
297
- }
298
-
299
259
  /** `[<КЛЮЧ>-<номер>]` в начале заголовка — единственная форма номера в названиях */
300
260
  export const TITLE_NUMBER = new RegExp(`^\\[${TASK_KEY}-(\\d+)\\]\\s+\\S`);
301
261
  /** `<КЛЮЧ>-<номер>-<slug>` — имя ветки, отведённой под задачу */
@@ -43,13 +43,11 @@ import {
43
43
  fetchIssues,
44
44
  fetchOpenPulls,
45
45
  gh,
46
- headCommittedAt,
47
46
  numberFromTaskDir,
48
47
  numberFromTitle,
49
- runsOnHead,
50
48
  taskDirs,
51
- verdictOnHead,
52
49
  } from './board.mjs';
50
+ import { deployLag, headCommittedAt, runsOnHead, verdictOnHead } from './board-runs.mjs';
53
51
  import { CONFIG, ROOT } from './rt-kit-checks.config.mjs';
54
52
 
55
53
  const IN_REVIEW = STATUS_OPTIONS[IN_REVIEW_STATUS].name;
@@ -67,6 +65,12 @@ const RUN_GRACE_MINUTES = 10;
67
65
  */
68
66
  const PIPELINE = CONFIG.pushGate?.pipelineFile ?? '';
69
67
  const HAS_PIPELINE = PIPELINE !== '' && existsSync(join(ROOT, PIPELINE));
68
+ /**
69
+ * Рабочий поток выкатки и ветка, с которой прод сравнивают. Не назвав потока, дерево сверки
70
+ * прода не получает — и сверка говорит об этом вслух: молчание читалось бы как «прод сошёлся».
71
+ */
72
+ const DEPLOY_WORKFLOW = CONFIG.deploy?.workflow ?? '';
73
+ const MAIN_BRANCH = CONFIG.deploy?.mainBranch ?? 'main';
70
74
 
71
75
  const problems = [];
72
76
  const report = (message) => problems.push(message);
@@ -252,10 +256,11 @@ try {
252
256
  claimed.set(titleNumber, pull.number);
253
257
  }
254
258
 
255
- // Папка задачи, лежащая в ветке открытого PR, — единственное расхождение, которое
256
- // сверка обязана назвать ДО слияния: гард судит её на слиянии, а слияние нажимает
257
- // человек в браузере, где хуков нет вовсе. Сказанная после, эта строка уже не чинится
258
- // тем же PR работа перешла дальше, и на разбор заводится вторая задача.
259
+ // Папка задачи, лежащая в ветке открытого PR, — расхождение с первой минуты заявки:
260
+ // уборка стоит до её открытия, и открытие с лежащей папкой отбивает гард поставки.
261
+ // Дошедшая сюда папка означает обход либо заявку, открытую мимо гарда. Сказанная
262
+ // после слияния, эта строка уже не чинится тем же PR: работа перешла дальше, и на
263
+ // разбор заводится вторая задача.
259
264
  if (HAS_PIPELINE && pull.headRefOid) {
260
265
  checkHeadRun(pull, options);
261
266
  }
@@ -266,7 +271,7 @@ try {
266
271
  const folder = folderInBranch(pull.headRefName, options);
267
272
  if (folder !== null) {
268
273
  report(
269
- `PR #${pull.number}: ветка везёт папку задачи «${folder}/» — разбери её этим же PR или поставь в тело строку «Task-folder-skip: <причина>»`
274
+ `PR #${pull.number}: ветка везёт папку задачи «${folder}/» — заявка открывается после уборки. Разбери её этим же PR или поставь в тело строку «Task-folder-skip: <причина>»`
270
275
  );
271
276
  }
272
277
  }
@@ -312,7 +317,33 @@ try {
312
317
  }
313
318
  }
314
319
 
320
+ // Прод сверяется с главной веткой по последней успешной выкатке. Задача уходит из очереди
321
+ // слиянием, но слияние — ещё не прод: там, где выкатку запускают рукой, между ними может лечь
322
+ // сколько угодно коммитов, и заметить это неоткуда.
323
+ if (!offline && DEPLOY_WORKFLOW) {
324
+ try {
325
+ const lag = deployLag(DEPLOY_WORKFLOW, MAIN_BRANCH, { token: botToken() ?? undefined });
326
+ if (lag === null) {
327
+ report(`выкаток по «${DEPLOY_WORKFLOW}» не было ни одной — сравнить прод не с чем`);
328
+ } else if (lag.behind > 0) {
329
+ report(
330
+ `прод отстал от «${MAIN_BRANCH}» на ${lag.behind} коммитов: последняя выкатка — ${lag.sha.slice(0, 8)} от ${String(lag.at).slice(0, 10)}`
331
+ );
332
+ }
333
+ } catch (error) {
334
+ if (error instanceof OfflineError) {
335
+ console.log(`check-board: прод не сверялся — ${error.message}`);
336
+ } else {
337
+ throw error;
338
+ }
339
+ }
340
+ }
341
+
315
342
  // Непроверенное называется вслух: молчание о прогонах читалось бы как «прогоны на месте».
343
+ if (!offline && !DEPLOY_WORKFLOW) {
344
+ console.log('check-board: прод с главной веткой не сверялся — рабочий поток выкатки в настройке дерева не назван');
345
+ }
346
+
316
347
  if (!offline && !HAS_PIPELINE) {
317
348
  console.log('check-board: прогоны на вершинах не спрашивались — файла конвейера в дереве нет');
318
349
  }
@@ -35,8 +35,18 @@ import { join } from 'node:path';
35
35
  import { allowlistOf, baselineOf, CONFIG, ROOT, parseAllowlist } from './rt-kit-checks.config.mjs';
36
36
 
37
37
  const ALLOWLIST = allowlistOf('file-size');
38
- /** Предел один на все роды файлов: своё число каждому роду спор о числе на каждой правке. */
38
+ /** Пределов два: код и текст слоя правил. Какой из них применён, каждая строка отказа называет. */
39
39
  const LIMIT = CONFIG.fileSizeLimit;
40
+ const PROSE_LIMIT = CONFIG.proseSizeLimit ?? CONFIG.fileSizeLimit;
41
+ /** Корни текста слоя правил; дерево, их не назвавшее, судится одним пределом. */
42
+ const PROSE_ROOTS = CONFIG.proseRoots ?? [];
43
+
44
+ /** Предел для файла и имя предела для отказа: по корню, а не по расширению — код лежит и в `.md`. */
45
+ function limitOf(path) {
46
+ return PROSE_ROOTS.some((root) => root && path.startsWith(root))
47
+ ? { limit: PROSE_LIMIT, title: 'предел текста' }
48
+ : { limit: LIMIT, title: 'предел кода' };
49
+ }
40
50
 
41
51
  /** Роды файлов, которых не читает линтер. Код остаётся за ним. */
42
52
  const JUDGED = ['.md', '.scss', '.html', '.js', '.mjs', '.sh'];
@@ -78,7 +88,7 @@ const tracked = trackedFiles().filter(judged);
78
88
 
79
89
  for (const path of tracked) {
80
90
  const lines = lineCount(path);
81
- if (lines > LIMIT) {
91
+ if (lines > limitOf(path).limit) {
82
92
  tooLong.set(path, lines);
83
93
  }
84
94
  }
@@ -95,7 +105,10 @@ const gone = [...known.keys()].filter((path) => !existsSync(join(ROOT, path)));
95
105
  const shrunk = [...known.keys()].filter((path) => !tooLong.has(path) && existsSync(join(ROOT, path)));
96
106
 
97
107
  const problems = [
98
- ...fresh.map(([path, lines]) => `${path}: ${lines} строк, предел ${LIMIT} — делить, а не дописывать строку в ${ALLOWLIST}`),
108
+ ...fresh.map(([path, lines]) => {
109
+ const { limit, title } = limitOf(path);
110
+ return `${path}: ${lines} строк, ${title} ${limit} — делить, а не дописывать строку в ${ALLOWLIST}`;
111
+ }),
99
112
  ...gone.map((path) => `${path}: строка в ${ALLOWLIST} устарела — файла в дереве нет`),
100
113
  ...shrunk.map((path) => `${path}: значится в ${ALLOWLIST}, но уже короче предела — строку убрать`),
101
114
  ];
@@ -107,7 +120,9 @@ if (problems.length > 0) {
107
120
  process.exit(1);
108
121
  }
109
122
 
123
+ const limits = PROSE_ROOTS.length > 0 ? `предел кода ${LIMIT}, предел текста ${PROSE_LIMIT}` : `предел ${LIMIT}`;
124
+
110
125
  console.log(
111
- `check-file-size: проверено ${tracked.length} файлов, длиннее ${LIMIT} строк ${tooLong.size}, ` +
126
+ `check-file-size: проверено ${tracked.length} файлов, ${limits}, длиннее предела ${tooLong.size}, ` +
112
127
  `из них принято ${accepted.size}, долг ${debt.size} — новых нет`
113
128
  );
@@ -111,6 +111,15 @@ async function withServiceClient(serviceUrl, run) {
111
111
  }
112
112
 
113
113
  async function main() {
114
+ // Незаданное имя спрашивается отдельно от несуществующего файла. Склеенное с корнем, пустое
115
+ // имя даёт сам корень — а он есть всегда, и проверка шла дальше, будто схема на месте.
116
+ // Дерево без хранилища так и говорит: имени нет, сверять нечего.
117
+ if (!CONFIG.schemaFile) {
118
+ console.log('check-schema-drift: имя файла схемы не задано — сверять нечего');
119
+
120
+ return 0;
121
+ }
122
+
114
123
  if (!existsSync(join(ROOT, CONFIG.schemaFile))) {
115
124
  console.log('check-schema-drift: схемы нет — сверять нечего');
116
125
 
@@ -162,9 +171,14 @@ async function main() {
162
171
  return 1;
163
172
  }
164
173
  if (diff.status !== 0) {
165
- console.log('check-schema-drift: сверка не отработалапропущено');
174
+ // Ни «сошлось», ни «разошлось» сама команда сравнения не отработала. Пропуск
175
+ // здесь неотличим от сошедшихся миграций, и именно им проверка молчала о пустом
176
+ // имени схемы: с ним она звала сравнение без обязательного довода, а отказ
177
+ // читался зелёным гейтом.
178
+ console.error('check-schema-drift: сравнение не отработало\n');
179
+ console.error(`${diff.stdout ?? ''}${diff.stderr ?? ''}`);
166
180
 
167
- return 0;
181
+ return 1;
168
182
  }
169
183
 
170
184
  console.log('check-schema-drift: миграции и схема сошлись');
@@ -179,8 +193,17 @@ async function main() {
179
193
  main().then(
180
194
  (code) => process.exit(code),
181
195
  (error) => {
182
- // Сбой самой проверки пуш не держит: сломанная обвязка не должна мешать работать
183
- console.log(`check-schema-drift: проверка не отработала (${error?.message ?? error})пропущено`);
184
- process.exit(0);
196
+ // Сломанная обвязка не пропуск, а отказ. «Проверять негде» проверка говорит сама и
197
+ // раньше: нет схемы, нет адреса, адрес боевой, сервер не отвечает всё это законные
198
+ // выходы нулём, и каждый назван своей строкой. Сюда доходит то, чего она не предвидела,
199
+ // и молчаливый ноль здесь означает «гейт зелен, потому что сверять не получилось».
200
+ // Отличить его от «сверено и сошлось» нечем: за таким нулём проверка простояла
201
+ // выключенной, пока её не позвали руками.
202
+ console.error(`check-schema-drift: проверка не отработала — ${error?.message ?? error}\n`);
203
+ console.error(
204
+ 'Это отказ самой проверки, а не расхождение схемы. Почини обвязку: недостающий пакет ставится в корень,\n' +
205
+ 'пустое имя схемы или каталога миграций задаётся в настройке проверок дерева.'
206
+ );
207
+ process.exit(1);
185
208
  }
186
209
  );
@@ -31,6 +31,13 @@ import { join } from 'node:path';
31
31
 
32
32
  const ROOT = process.cwd();
33
33
  const RULE = join(ROOT, '.claude/skills/task-flow/SKILL.md');
34
+ /**
35
+ * Правило хода захода: граница состояния живёт там, а не в правиле ведения работы.
36
+ * Разделились они, когда правило ведения работы вышло за предел длины: ход работы
37
+ * остался в одном, выходы хода уехали в другое. Дерево, где разложено только первое,
38
+ * судится по нему одному — второго файла у него нет.
39
+ */
40
+ const TURN_RULE = join(ROOT, '.claude/skills/turn-conduct/SKILL.md');
34
41
  const SKILLS = join(ROOT, '.claude/skills');
35
42
  const MAP = join(ROOT, '.claude/rt-kit/defaults/turn-map.md');
36
43
  const LAW = join(ROOT, 'docs/constitution/work-conduct.md');
@@ -167,8 +174,9 @@ if (counted === 0) {
167
174
  problems.push('ни одного раздела состояния не нашлось: паттерны не разложены или заголовки в них другие');
168
175
  }
169
176
 
170
- if (!ruleText.includes(BOUNDARY)) {
171
- problems.push(`правило ведения работы о границе состояния молчит: строки «${BOUNDARY}» в нём нет`);
177
+ const turnText = existsSync(TURN_RULE) ? readFileSync(TURN_RULE, 'utf8') : '';
178
+ if (!ruleText.includes(BOUNDARY) && !turnText.includes(BOUNDARY)) {
179
+ problems.push(`правило хода захода о границе состояния молчит: строки «${BOUNDARY}» в нём нет`);
172
180
  }
173
181
 
174
182
  for (const [file, what] of [
@@ -67,10 +67,24 @@ const DEFAULTS = {
67
67
  */
68
68
  generatedDirs: [],
69
69
  /**
70
- * Предел длины файла одно число на все роды: своё число каждому роду означает спор о
71
- * числе на каждой правке, а не о длине файла.
70
+ * Предел длины файла для кода и обвязки. Родов у него по-прежнему не два десятка, а два:
71
+ * код и текст слоя правил. Спорить о числе на каждой правке нечему оба числа стоят
72
+ * здесь и меняются работой, а не по ходу.
72
73
  */
73
74
  fileSizeLimit: 500,
75
+ /**
76
+ * Предел длины текста слоя правил — закона, правила, паттерна, холодной части и их
77
+ * источников. Он ниже предела кода вдвое, и это не строгость ради строгости: текст, не
78
+ * влезающий на экран целиком, дописывают в конец, не перечитав начала, — так в одном файле
79
+ * и оказываются два ответа на один вопрос. У кода от этого спасает линтер, у текста —
80
+ * только это число.
81
+ */
82
+ proseSizeLimit: 300,
83
+ /**
84
+ * Корни, под которыми лежит текст слоя правил, и его источники. Файл отсюда судится
85
+ * пределом текста, всё остальное — пределом кода. Пусто — предел один на всё дерево.
86
+ */
87
+ proseRoots: [],
74
88
  /**
75
89
  * Внешние пакеты, чьи перечисления считаются наравне с либами: своё перечисление под уже
76
90
  * объявленный там набор — такая же копия, как и между двумя либами. Каждая запись — имя
@@ -129,6 +143,18 @@ const DEFAULTS = {
129
143
  stepPattern: '^\\s*-\\s*name:\\s*(.+?)\\s*$',
130
144
  steps: {},
131
145
  },
146
+ /**
147
+ * Выкатка. `workflow` — файл рабочего потока, которым прод выкатывают; пусто — сверка
148
+ * прода молчит вслух. `mainBranch` — ветка, с которой прод сравнивают.
149
+ *
150
+ * Спрашивается именно последняя успешная выкатка, а не последний прогон главной ветки:
151
+ * там, где выкатку запускают рукой, прогон главной о проде не говорит ничего, и прод,
152
+ * отставший на сотни коммитов, не виден ничему.
153
+ */
154
+ deploy: {
155
+ workflow: '',
156
+ mainBranch: 'main',
157
+ },
132
158
  /** Очередь работ: владелец, репозиторий, борда и учётная запись машинной работы. */
133
159
  board: {
134
160
  owner: '',
@@ -70,11 +70,19 @@ cp .claude/rt-kit/templates/proposal.md \
70
70
 
71
71
  - **место:** раздел «<заголовок>», в конец
72
72
  - **повод:** что в этой работе пошло не так без этого правила
73
+ - **ближайшее:** «<точная строка ресурса, к которой это ближе всего>» — <чего она не покрывает>
73
74
 
74
75
  > Готовый текст правки — ровно то, что вставить, в стиле соседних правил: по-русски,
75
76
  > утверждением, без воды.
76
77
  ```
77
78
 
79
+ **Строка «ближайшее» обязательна, и она единственная, которую проверяет машина.** Цитата ищется
80
+ в ресурсе, и ненайденная отбивает блок: либо ресурс не читали, либо утверждение с тех пор
81
+ переписано. Ближайшего нет вовсе — так и пишется: «нет». Написана она затем, чтобы ресурс был
82
+ прочитан: разбор кончается предложением дописать статью в тот же ресурс, который промах уже
83
+ описывал, и вторая статья о том же дороже, чем её отсутствие. Отбитый блок остаётся лежать с
84
+ отметкой и причиной — видно, что разбор был, и видно, почему он не стал правкой.
85
+
78
86
  Повод пишется от случая, а не от желания: «здесь было неудобно» правилом не становится. Слово
79
87
  пользователя пересказывается его смыслом, а не твоими выводами о том, как надо было бы.
80
88
 
@@ -277,11 +277,50 @@ rt_shell_paths_default() {
277
277
  }')"
278
278
  fi
279
279
 
280
- printf '%s' "$text" \
281
- | tr "(),;=" ' ' \
282
- | tr '[:space:]' '\n' \
283
- | grep -E '^[A-Za-z0-9_@.-]*/[A-Za-z0-9_@./-]+$' \
284
- | sed 's|^\./||' \
280
+ # Пути берутся только у тех кусков команды, которые пишут. Прежде брались у всей строки
281
+ # целиком, и команда чтения, сцепленная с записью, отдавала свои пути как цели записи:
282
+ # `python3 <<PY … PY` рядом с `grep -n … projects/…` отбивался за правку кода, которой в нём
283
+ # не было. Отбитий, пришедшихся не на правку файла, набиралось большинство, и цену платил
284
+ # тот, кто просто читал соседний файл в той же строке.
285
+ #
286
+ # Кусок — строка верхнего уровня, а внутри неё `;`, `&&` и `||`. Тело heredoc от своей
287
+ # команды не отрывается: оно едет вместе с ней одним куском, потому что путь записи
288
+ # интерпретатора стоит именно там.
289
+ printf '%s' "$text" | awk '
290
+ function trim(s) { sub(/^[ \t]+/, "", s); sub(/[ \t]+$/, "", s); return s }
291
+ function flush( n, i, part) {
292
+ if (chunk == "") { return }
293
+ if (tag != "") { part = chunk; gsub(/\n/, " ", part); print part; chunk = ""; return }
294
+ n = split(chunk, parts, /;|&&|\|\|/)
295
+ for (i = 1; i <= n; i++) { print parts[i] }
296
+ chunk = ""
297
+ }
298
+ tag != "" {
299
+ chunk = chunk "\n" $0
300
+ if (trim($0) == tag) { flush(); tag = "" }
301
+ next
302
+ }
303
+ {
304
+ chunk = $0
305
+ if (match($0, /<<-?[ \t]*[A-Za-z_][A-Za-z0-9_]*/)) {
306
+ t = substr($0, RSTART, RLENGTH)
307
+ sub(/^<<-?[ \t]*/, "", t)
308
+ tag = t
309
+ next
310
+ }
311
+ flush()
312
+ }
313
+ END { if (chunk != "") { part = chunk; gsub(/\n/, " ", part); print part } }
314
+ ' \
315
+ | while IFS= read -r piece; do
316
+ [ -z "$piece" ] && continue
317
+ rt_shell_writes "$piece" || continue
318
+ printf '%s' "$piece" \
319
+ | tr "(),;=" ' ' \
320
+ | tr '[:space:]' '\n' \
321
+ | grep -E '^[A-Za-z0-9_@.-]*/[A-Za-z0-9_@./-]+$' \
322
+ | sed 's|^\./||'
323
+ done \
285
324
  | sort -u
286
325
  }
287
326
 
@@ -317,9 +356,17 @@ RT_TASK_TITLE_RE="${RT_TASK_TITLE_RE:-^\[[A-Za-z]+-[0-9]+\][[:space:]]+[^[:space
317
356
  RT_TASK_NEW_CMD="${RT_TASK_NEW_CMD:-npm run task:new}"
318
357
  RT_BOARD_CHECK_CMD="${RT_BOARD_CHECK_CMD:-npm run check:board}"
319
358
 
320
- # Учётная запись, которую ставят исполнителем. Умолчание молчит: у каждого дерева она своя.
359
+ # Учётная запись, которую ставят исполнителем и которой открывают заявку. Умолчание молчит: у
360
+ # каждого дерева она своя, а у иного её нет вовсе.
321
361
  RT_TASK_BOT="${RT_TASK_BOT:-}"
322
362
 
363
+ # Чем вызову подставляют токен машинной записи и как эта подстановка выглядит целиком. Клиент
364
+ # хостинга держит две записи сразу — залогиненную и ту, чей токен стоит в окружении вызова, — и
365
+ # из текста команды видно только явную подстановку. Оба умолчания молчат: дерево без отдельной
366
+ # машинной записи автора заявки не судит вовсе.
367
+ RT_PULL_TOKEN_VAR="${RT_PULL_TOKEN_VAR:-}"
368
+ RT_PULL_TOKEN_HINT="${RT_PULL_TOKEN_HINT:-}"
369
+
323
370
  # Команда перевода задачи между колонками очереди работ и имя первой колонки — той, из которой
324
371
  # задача уходит, когда её берут в работу. Имя без умолчания: колонки дерево называет своими
325
372
  # словами, а выдуманное не совпало бы ни с чем и молча выключило бы проверку колонки.
@@ -412,8 +459,14 @@ rt_lint_for() { rt_lint_for_default "$@"; }
412
459
  rt_task_branch_ok() { rt_task_branch_ok_default "$@"; }
413
460
  rt_reinvented_in() { rt_reinvented_in_default "$@"; }
414
461
  rt_is_app_code() { rt_is_app_code_default "$@"; }
462
+ # Каталог источников пакета правил в этом дереве, от корня. Пусто — дерево пакета не везёт, и
463
+ # адрес правки у него один: надстройка. Дерево, которое пакет и разрабатывает, называет каталог
464
+ # сам — иначе гард места правки посылал бы его в надстройку вместо источника.
465
+ rt_kit_sources_dir_default() { printf ''; }
466
+
415
467
  rt_shell_writes() { rt_shell_writes_default "$@"; }
416
468
  rt_shell_paths() { rt_shell_paths_default "$@"; }
469
+ rt_kit_sources_dir() { rt_kit_sources_dir_default "$@"; }
417
470
  rt_qa_decorative() { rt_qa_decorative_default "$@"; }
418
471
  rt_task_state() { rt_task_state_default "$@"; }
419
472
  rt_pull_state() { rt_pull_state_default "$@"; }
@@ -1,7 +1,8 @@
1
1
  # Карта хода
2
2
 
3
3
  Это не правило, а его короткая выжимка: правило объясняет, карта называет. Полный текст —
4
- скил `task-flow`; он же называет паттерн, который ведёт каждое состояние.
4
+ скил `task-flow`; он же называет паттерн, который ведёт каждое состояние. Чем ход
5
+ кончается — скил `turn-conduct` под тем же законом.
5
6
 
6
7
  Состояние работы объявлено строкой в разделе «Где стоим» хода работы. Пока обязательное
7
8
  действие не сделано, работа стоит в том же состоянии.
@@ -19,8 +20,8 @@
19
20
  | `этапы-кончились` | прогнать набор и открыть PR черновиком | `task-flow-close` |
20
21
  | `работа-отдана` | взять следующую задачу | `task-flow-resume` |
21
22
  | `разбор-кончился` | влить договорённость, привести тексты, разобрать папку | `task-flow-close` |
22
- | `папка-разобрана` | снять черновик и попросить влить | `task-flow-close` |
23
- | `влито` | разбор работы правилами и сверка очереди | `task-flow-close` |
23
+ | `папка-разобрана` | снять черновик и попросить влить | `task-flow-archive` |
24
+ | `влито` | разбор работы правилами и сверка очереди | `task-flow-archive` |
24
25
 
25
26
  Ни у одного состояния обязательное действие не звучит как «ждать». Прогон, разбор владельцем и
26
27
  слияние идут без исполнителя и от взгляда быстрее не становятся.
@@ -41,6 +42,7 @@
41
42
  остановки там — страховка на случай, когда сжатие не пришло.
42
43
 
43
44
  Всё остальное — продолжение хода. Ходом не кончаются: коммит, записанный замысел, закрытый
44
- разбор просьбы, прочитанная договорённость, зелёная проверка, сводка о чужом шаге и объявление
45
- намерения. Переход из состояния в состояние — тем более: обязательное действие сделано, и
46
- следующее делается тем же ходом. Названо может быть только сделанное.
45
+ разбор просьбы, прочитанная договорённость, зелёная проверка, сводка о чужом шаге, объявление
46
+ намерения и названная, но не запущенная команда. Переход из состояния в состояние — тем более:
47
+ обязательное действие сделано, и следующее делается тем же ходом. Названо может быть только
48
+ сделанное.
@@ -12,8 +12,10 @@
12
12
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: помощник не назвал профиль — пропуск.
13
13
 
14
14
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
15
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
15
16
 
16
- input="$(cat 2>/dev/null)"
17
+ rt_hook_read
18
+ input="$RT_HOOK_INPUT"
17
19
 
18
20
  device_id="$("${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/browser-device-id.sh" 2>/dev/null)"
19
21
  [ -z "$device_id" ] && exit 0
@@ -15,8 +15,10 @@
15
15
  # FAIL-OPEN when browser-device-id.sh prints nothing.
16
16
 
17
17
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
18
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
18
19
 
19
- input="$(cat 2>/dev/null)"
20
+ rt_hook_read
21
+ input="$RT_HOOK_INPUT"
20
22
 
21
23
  device_id="$("${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/browser-device-id.sh" 2>/dev/null)"
22
24
  [ -z "$device_id" ] && exit 0
@@ -14,13 +14,15 @@
14
14
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: помощник не назвал профиль — пропуск.
15
15
 
16
16
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
17
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
17
18
 
18
- input="$(cat 2>/dev/null)"
19
+ rt_hook_read
20
+ input="$RT_HOOK_INPUT"
19
21
 
20
22
  device_id="$("${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/browser-device-id.sh" 2>/dev/null)"
21
23
  [ -z "$device_id" ] && exit 0
22
24
 
23
- tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
25
+ tool="$(rt_hook_tool)"
24
26
 
25
27
  # shellcheck disable=SC1090
26
28
  [ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" ] \
@@ -45,7 +47,7 @@ case "$tool" in
45
47
  *) exit 0 ;;
46
48
  esac
47
49
 
48
- cmd="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null)"
50
+ cmd="$(rt_hook_cmd)"
49
51
  [ -z "$cmd" ] && exit 0
50
52
 
51
53
  if [ "$tool" = "mcp__webstorm__execute_tool" ] && command -v perl >/dev/null 2>&1; then
@@ -78,7 +80,7 @@ esac
78
80
  # помечают движок в прогоне сквозных спек, — такой образец отбил бы сам прогон в день, когда
79
81
  # появился. Поэтому якорь на границе команды и требование похожего на исполняемый файл слова,
80
82
  # а не значения флага.
81
- printf '%s' "$cmd" | grep -qE '(^|[;&|(]|[[:space:]]&&|[[:space:]]\|\|)[[:space:]]*(/[^[:space:]]*/)?(google-chrome|chromium)([[:space:]]|$)' \
83
+ printf '%s' "$cmd" | grep -qE "${RT_CMD_BOUND}(/[^[:space:]]*/)?(google-chrome|chromium)([[:space:]]|\$)" \
82
84
  && deny "Прямой запуск бинарника браузера обходит закреплённый профиль."
83
85
 
84
86
  printf '%s' "$cmd" | grep -qF 'Google Chrome.app/Contents/MacOS' \
@@ -21,13 +21,15 @@
21
21
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: помощник не назвал профиль — пропуск.
22
22
 
23
23
  . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/utf8.sh" 2>/dev/null || true
24
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hook-input.sh" 2>/dev/null || true
24
25
 
25
- input="$(cat 2>/dev/null)"
26
+ rt_hook_read
27
+ input="$RT_HOOK_INPUT"
26
28
 
27
29
  device_id="$("${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/browser-device-id.sh" 2>/dev/null)"
28
30
  [ -z "$device_id" ] && exit 0
29
31
 
30
- tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
32
+ tool="$(rt_hook_tool)"
31
33
  # У перечисления, переключения и самого выбора свои гарды.
32
34
  case "$tool" in
33
35
  *list_connected_browsers|*switch_browser|*select_browser) exit 0 ;;