@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.
- package/assets/checks/board-runs.github.mjs +87 -0
- package/assets/checks/board.github.mjs +0 -40
- package/assets/checks/check-board.github.mjs +39 -8
- package/assets/checks/check-file-size.mjs +19 -4
- package/assets/checks/check-schema-drift.mjs +28 -5
- package/assets/checks/check-state-next.mjs +10 -2
- package/assets/checks/rt-kit-checks.config.mjs +28 -2
- package/assets/commands/feedback.md +8 -0
- package/assets/defaults/project.sh +59 -6
- package/assets/defaults/turn-map.md +8 -6
- package/assets/hooks/browser-guard-device-id.sh +3 -1
- package/assets/hooks/browser-guard-no-asking.sh +3 -1
- package/assets/hooks/browser-guard-no-other-drivers.sh +6 -4
- package/assets/hooks/browser-guard-require-select.sh +4 -2
- package/assets/hooks/claim-guard.sh +3 -1
- package/assets/hooks/conscience-guard.sh +3 -1
- package/assets/hooks/dev-server-guard.sh +6 -4
- package/assets/hooks/dispatch.sh +69 -0
- package/assets/hooks/docs-guard.sh +6 -4
- package/assets/hooks/exam-guard.sh +5 -3
- package/assets/hooks/git-guard-delivery-folder.sh +99 -0
- package/assets/hooks/git-guard-delivery-signature.sh +10 -4
- package/assets/hooks/git-guard-delivery.sh +58 -74
- package/assets/hooks/git-guard-main.sh +6 -4
- package/assets/hooks/git-guard-push-tests.sh +8 -6
- package/assets/hooks/grill-gate.sh +4 -2
- package/assets/hooks/handoff-entry-guard.sh +4 -2
- package/assets/hooks/handoff-write.sh +27 -6
- package/assets/hooks/hook-input.sh +77 -0
- package/assets/hooks/lint-after-edit.sh +5 -3
- package/assets/hooks/override-write-guard.sh +107 -0
- package/assets/hooks/postmortem-guard.sh +3 -1
- package/assets/hooks/proposal-guard.sh +3 -1
- package/assets/hooks/prose-style-guard.sh +5 -3
- package/assets/hooks/qa-dataid-guard.sh +4 -2
- package/assets/hooks/rerun-guard.sh +6 -4
- package/assets/hooks/reuse-first-guard.sh +5 -3
- package/assets/hooks/rule-article.sh +99 -0
- package/assets/hooks/rule-source-guard.sh +126 -0
- package/assets/hooks/skill-gate-rearm.sh +3 -1
- package/assets/hooks/skill-gate.sh +23 -2
- package/assets/hooks/skill-loaded.sh +3 -1
- package/assets/hooks/sql-guard-request.sh +2 -1
- package/assets/hooks/sql-guard.sh +4 -2
- package/assets/hooks/task-flow-guard.sh +24 -4
- package/assets/hooks/turn-exit-guard.sh +59 -17
- package/assets/hooks/waiting-turn-guard.sh +3 -1
- package/assets/hooks/window-fill-guard.sh +6 -4
- package/assets/laws/work-conduct.md +5 -9
- package/assets/patterns/dependencies-upgrade.md +1 -1
- package/assets/patterns/doc-style-write.md +3 -3
- package/assets/patterns/git-workflow-commit.azure.md +2 -202
- package/assets/patterns/git-workflow-commit.github.md +2 -258
- package/assets/patterns/git-workflow-commit.gitlab.md +1 -217
- package/assets/patterns/git-workflow-docker.md +3 -3
- package/assets/patterns/git-workflow-merge.md +3 -2
- package/assets/patterns/git-workflow-migration.md +3 -3
- package/assets/patterns/git-workflow-pr.azure.md +224 -0
- package/assets/patterns/git-workflow-pr.github.md +280 -0
- package/assets/patterns/git-workflow-pr.gitlab.md +240 -0
- package/assets/patterns/git-workflow-restart.md +3 -3
- package/assets/patterns/git-workflow-secrets.md +3 -3
- package/assets/patterns/task-flow-archive.md +195 -0
- package/assets/patterns/task-flow-close.md +72 -240
- package/assets/patterns/task-flow-handoff.md +4 -4
- package/assets/patterns/task-flow-resume.md +5 -3
- package/assets/pitfalls/agent-kit.md +80 -0
- package/assets/pitfalls/doc-style.md +80 -0
- package/assets/pitfalls/git-workflow.azure.md +50 -0
- package/assets/pitfalls/git-workflow.github.md +78 -0
- package/assets/pitfalls/git-workflow.gitlab.md +49 -0
- package/assets/pitfalls/spec-driven.md +36 -0
- package/assets/pitfalls/styling-bem.md +45 -0
- package/assets/pitfalls/task-flow.md +62 -0
- package/assets/pitfalls/testing.md +70 -0
- package/assets/rules/deploy-flow.azure.md +114 -0
- package/assets/rules/deploy-flow.github.md +122 -0
- package/assets/rules/deploy-flow.gitlab.md +116 -0
- package/assets/rules/doc-style.md +25 -76
- package/assets/rules/git-workflow.azure.md +6 -92
- package/assets/rules/git-workflow.github.md +20 -128
- package/assets/rules/git-workflow.gitlab.md +6 -93
- package/assets/rules/spec-driven.md +39 -30
- package/assets/rules/styling-bem.md +20 -39
- package/assets/rules/task-flow.md +56 -222
- package/assets/rules/testing.md +3 -64
- package/assets/rules/turn-conduct.md +210 -0
- package/assets/rules/typescript-conventions.md +15 -0
- package/assets/skills/agent-kit.md +67 -89
- package/assets/templates/pitfalls.md +10 -0
- package/assets/templates/proposal.md +16 -1
- package/assets/templates/rule.md +5 -3
- package/bin/agent-kit.d.ts.map +1 -1
- package/bin/agent-kit.js +1 -42
- package/bin/agent-kit.js.map +1 -1
- package/lib/assets.d.ts.map +1 -1
- package/lib/assets.js +6 -1
- package/lib/assets.js.map +1 -1
- package/lib/cascade.d.ts.map +1 -1
- package/lib/cascade.js +19 -1
- package/lib/cascade.js.map +1 -1
- package/lib/commands.d.ts.map +1 -1
- package/lib/commands.js +1 -0
- package/lib/commands.js.map +1 -1
- package/lib/config.d.ts +16 -1
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +8 -0
- package/lib/config.js.map +1 -1
- package/lib/hooks-map.d.ts +13 -0
- package/lib/hooks-map.d.ts.map +1 -1
- package/lib/hooks-map.js +33 -1
- package/lib/hooks-map.js.map +1 -1
- package/lib/proposals.d.ts +5 -1
- package/lib/proposals.d.ts.map +1 -1
- package/lib/proposals.js +74 -5
- package/lib/proposals.js.map +1 -1
- package/lib/ship.d.ts +1 -2
- package/lib/ship.d.ts.map +1 -1
- package/lib/ship.js +0 -54
- package/lib/ship.js.map +1 -1
- package/lib/shipment.d.ts.map +1 -1
- package/lib/shipment.fixture.d.ts +39 -0
- package/lib/shipment.fixture.d.ts.map +1 -0
- package/lib/shipment.fixture.js +99 -0
- package/lib/shipment.fixture.js.map +1 -0
- package/lib/shipment.js +59 -7
- package/lib/shipment.js.map +1 -1
- package/package.json +1 -1
- package/rt-tools-agent-kit-0.13.0.tgz +0 -0
- package/assets/commands/agent-kit-digest.md +0 -89
- package/assets/commands/rules-review.md +0 -98
- package/assets/patterns/cargo-triage-mark.md +0 -119
- package/assets/rules/cargo-triage.md +0 -126
- package/lib/cargo-state.d.ts +0 -62
- package/lib/cargo-state.d.ts.map +0 -1
- package/lib/cargo-state.js +0 -118
- package/lib/cargo-state.js.map +0 -1
- 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}/» —
|
|
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 >
|
|
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]) =>
|
|
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} файлов,
|
|
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
|
-
|
|
174
|
+
// Ни «сошлось», ни «разошлось» — сама команда сравнения не отработала. Пропуск
|
|
175
|
+
// здесь неотличим от сошедшихся миграций, и именно им проверка молчала о пустом
|
|
176
|
+
// имени схемы: с ним она звала сравнение без обязательного довода, а отказ
|
|
177
|
+
// читался зелёным гейтом.
|
|
178
|
+
console.error('check-schema-drift: сравнение не отработало\n');
|
|
179
|
+
console.error(`${diff.stdout ?? ''}${diff.stderr ?? ''}`);
|
|
166
180
|
|
|
167
|
-
return
|
|
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
|
-
|
|
184
|
-
|
|
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
|
-
|
|
171
|
-
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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-
|
|
23
|
-
| `влито` | разбор работы правилами и сверка очереди | `task-flow-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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="$(
|
|
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="$(
|
|
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
|
|
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
|
-
|
|
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="$(
|
|
32
|
+
tool="$(rt_hook_tool)"
|
|
31
33
|
# У перечисления, переключения и самого выбора свои гарды.
|
|
32
34
|
case "$tool" in
|
|
33
35
|
*list_connected_browsers|*switch_browser|*select_browser) exit 0 ;;
|