@rt-tools/agent-kit 0.16.1 → 0.18.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-epics.github.mjs +123 -0
- package/assets/checks/board-gh.github.mjs +121 -0
- package/assets/checks/board-paths.github.mjs +88 -0
- package/assets/checks/board-runs.github.mjs +8 -0
- package/assets/checks/board-titles.github.mjs +66 -0
- package/assets/checks/board.github.mjs +77 -73
- package/assets/checks/check-board.github.mjs +56 -7
- package/assets/checks/check-file-size.mjs +10 -2
- package/assets/checks/check-glossary.mjs +170 -0
- package/assets/checks/check-hook-scope.mjs +126 -0
- package/assets/checks/check-profile-drift.mjs +195 -0
- package/assets/checks/check-push-gate.mjs +59 -1
- package/assets/checks/check-schema-drift.mjs +12 -5
- package/assets/checks/check-specs.mjs +1 -1
- package/assets/checks/rt-kit-checks.config.mjs +13 -0
- package/assets/checks/spec-anchors.mjs +2 -2
- package/assets/checks/spec-contract.mjs +9 -0
- package/assets/defaults/gate-map.sh +13 -4
- package/assets/defaults/project.sh +31 -108
- package/assets/defaults/shell.sh +139 -0
- package/assets/docs/GLOSSARY.md +1 -1
- package/assets/hooks/browser-device-id.sh +20 -4
- package/assets/hooks/browser-guard-device-id.sh +5 -2
- package/assets/hooks/browser-guard-no-other-drivers.sh +41 -2
- package/assets/hooks/claim-guard.sh +22 -1
- package/assets/hooks/docs-guard.sh +1 -1
- package/assets/hooks/exam-guard.sh +85 -12
- package/assets/hooks/git-guard-delivery-conflict.sh +85 -0
- package/assets/hooks/git-guard-delivery-folder.sh +19 -0
- package/assets/hooks/git-guard-delivery-signature.sh +14 -5
- package/assets/hooks/git-guard-delivery.sh +54 -2
- package/assets/hooks/git-guard-push-tests.sh +21 -1
- package/assets/hooks/grill-gate-ask.sh +25 -0
- package/assets/hooks/grill-gate.sh +75 -11
- package/assets/hooks/lint-after-edit.sh +44 -16
- package/assets/hooks/proposal-guard.sh +11 -4
- package/assets/hooks/rule-source-guard.sh +8 -13
- package/assets/hooks/skill-gate-layers.sh +7 -0
- package/assets/hooks/skill-gate.sh +29 -0
- package/assets/hooks/task-context-load.sh +32 -0
- package/assets/hooks/task-flow-context.sh +194 -0
- package/assets/hooks/task-flow-draft-guard.sh +109 -0
- package/assets/hooks/task-flow-guard.sh +27 -159
- package/assets/hooks/turn-exit-guard.sh +53 -88
- package/assets/hooks/waiting-turn-guard.sh +65 -2
- package/assets/hooks/window-fill-guard.sh +5 -1
- package/assets/hooks/work-start-guard.sh +166 -0
- package/assets/hooks/write-targets.sh +24 -0
- package/assets/laws/delivery.md +19 -0
- package/assets/laws/frontend-application.md +4 -0
- package/assets/laws/verifiability.md +44 -0
- package/assets/laws/work-conduct.md +15 -0
- package/assets/patterns/browser-verification-measure.md +3 -1
- package/assets/patterns/browser-verification-stand.md +17 -4
- package/assets/patterns/doc-style-write.md +62 -2
- package/assets/patterns/git-workflow-commit.github.md +33 -5
- package/assets/patterns/git-workflow-docker.md +22 -0
- package/assets/patterns/git-workflow-freshness.md +87 -0
- package/assets/patterns/git-workflow-merge.md +41 -0
- package/assets/patterns/git-workflow-migration.md +11 -0
- package/assets/patterns/git-workflow-pr.github.md +12 -1
- package/assets/patterns/git-workflow-restart.md +18 -0
- package/assets/patterns/git-workflow-secrets.md +14 -0
- package/assets/patterns/git-workflow-stack.md +93 -1
- package/assets/patterns/lib-layers-move.md +4 -0
- package/assets/patterns/spec-driven-domain.md +47 -2
- package/assets/patterns/spec-driven-rule.md +16 -4
- package/assets/patterns/spec-driven-sweep.md +57 -0
- package/assets/patterns/status-report-table.github.md +1 -1
- package/assets/patterns/task-flow-archive.md +58 -16
- package/assets/patterns/task-flow-close.md +106 -23
- package/assets/patterns/task-flow-handoff.md +14 -2
- package/assets/patterns/task-flow-resume.md +35 -8
- package/assets/patterns/task-flow-start.md +60 -22
- package/assets/patterns/testing-e2e.md +23 -0
- package/assets/patterns/turn-entry-map.md +1 -1
- package/assets/pitfalls/agent-kit.md +71 -3
- package/assets/pitfalls/doc-style.md +15 -0
- package/assets/pitfalls/git-workflow.github.md +88 -0
- package/assets/pitfalls/spec-driven.md +22 -0
- package/assets/pitfalls/task-flow.md +113 -0
- package/assets/pitfalls/testing.md +6 -0
- package/assets/pitfalls/turn-conduct.md +49 -0
- package/assets/rules/browser-verification.md +58 -0
- package/assets/rules/deploy-flow.azure.md +8 -0
- package/assets/rules/deploy-flow.github.md +27 -0
- package/assets/rules/deploy-flow.gitlab.md +8 -0
- package/assets/rules/doc-style.md +20 -0
- package/assets/rules/git-workflow.azure.md +22 -2
- package/assets/rules/git-workflow.github.md +127 -118
- package/assets/rules/git-workflow.gitlab.md +17 -4
- package/assets/rules/lists.md +5 -0
- package/assets/rules/observability.needs-app.md +4 -0
- package/assets/rules/reuse-first.md +14 -0
- package/assets/rules/shared-code.md +5 -0
- package/assets/rules/spec-driven.md +33 -15
- package/assets/rules/task-flow.md +132 -126
- package/assets/rules/testing.md +41 -2
- package/assets/rules/turn-conduct.md +73 -57
- package/assets/rules/turn-entry.md +6 -0
- package/assets/samples/tasks/_template/grill.md +5 -0
- package/assets/samples/tasks/_template/plan.md +3 -0
- package/assets/skills/agent-kit.md +108 -76
- package/assets/templates/postmortem.md +5 -1
- package/bin/agent-kit.d.ts.map +1 -1
- package/bin/agent-kit.js +30 -6
- package/bin/agent-kit.js.map +1 -1
- package/lib/catalog.d.ts.map +1 -1
- package/lib/catalog.js +2 -1
- package/lib/catalog.js.map +1 -1
- package/lib/commands.d.ts.map +1 -1
- package/lib/commands.js +96 -7
- package/lib/commands.js.map +1 -1
- package/lib/hooks-map.d.ts +26 -0
- package/lib/hooks-map.d.ts.map +1 -1
- package/lib/hooks-map.js +58 -2
- package/lib/hooks-map.js.map +1 -1
- package/lib/sections.d.ts +6 -0
- package/lib/sections.d.ts.map +1 -1
- package/lib/sections.js +19 -0
- package/lib/sections.js.map +1 -1
- package/lib/shipment.d.ts +2 -0
- package/lib/shipment.d.ts.map +1 -1
- package/lib/shipment.fixture.d.ts +5 -0
- package/lib/shipment.fixture.d.ts.map +1 -1
- package/lib/shipment.fixture.js +7 -0
- package/lib/shipment.fixture.js.map +1 -1
- package/lib/shipment.js +13 -1
- package/lib/shipment.js.map +1 -1
- package/lib/sync.d.ts +35 -3
- package/lib/sync.d.ts.map +1 -1
- package/lib/sync.js +59 -8
- package/lib/sync.js.map +1 -1
- package/package.json +1 -1
- package/rt-tools-agent-kit-0.18.0.tgz +0 -0
- package/rt-tools-agent-kit-0.16.1.tgz +0 -0
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
* `rt_push_checks` профиля; она сверяется с его выводом, потому что объявление
|
|
19
19
|
* без исполнения — та же дыра. Исключение — причина, по которой шага в гейте
|
|
20
20
|
* нет; пустая причина исключением не считается: она единственное, чем
|
|
21
|
-
* постоянная дыра отличается от забытой строки.
|
|
21
|
+
* постоянная дыра отличается от забытой строки. Причина, называющая задачу,
|
|
22
|
+
* судится ещё и на живость этой задачи: отсрочка со сроком и отсрочка без срока
|
|
23
|
+
* выглядят одинаково, пока номер никто не спросил.
|
|
22
24
|
*
|
|
23
25
|
* Дерево без файла конвейера сверки не получает: проверка, падающая там, где
|
|
24
26
|
* конвейера нет, отбивала бы работу вместо промаха.
|
|
@@ -34,6 +36,8 @@ import { CONFIG, ROOT } from './rt-kit-checks.config.mjs';
|
|
|
34
36
|
const GATE = CONFIG.pushGate ?? {};
|
|
35
37
|
const PIPELINE = GATE.pipelineFile ?? '';
|
|
36
38
|
const DECLARED = GATE.steps ?? {};
|
|
39
|
+
/** Ключ задач дерева: по нему номер в причине отличается от версии, порта и года. */
|
|
40
|
+
const TASK_KEY = CONFIG.board?.taskKey ?? '';
|
|
37
41
|
|
|
38
42
|
/**
|
|
39
43
|
* Профиль дерева ищется той же цепочкой, что и у гарда пуша: умолчание пакета, поверх него
|
|
@@ -85,9 +89,39 @@ rt_push_checks ""`;
|
|
|
85
89
|
}
|
|
86
90
|
}
|
|
87
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Номера задач, названные причиной. Ключ дерева обязателен: без него «663» неотличимо от порта,
|
|
94
|
+
* года и номера редакции, и проверка спрашивала бы очередь работ обо всём подряд.
|
|
95
|
+
*/
|
|
96
|
+
function taskNumbers(reason) {
|
|
97
|
+
if (!TASK_KEY) {
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
return [...reason.matchAll(new RegExp(`\\b${TASK_KEY}-(\\d+)\\b`, 'g'))].map((match) => Number(match[1]));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Очередь работ ищется разрешением модуля, а не собирается здесь: у каждого хостинга она своя, и
|
|
105
|
+
* дерево, у которого её нет, судится как прежде. Ярус тот же, что у гарда поставки: есть чем
|
|
106
|
+
* спросить — спрашивает, нет — молчит.
|
|
107
|
+
*/
|
|
108
|
+
async function boardModule() {
|
|
109
|
+
if (!TASK_KEY) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
const module = await import('./board.mjs');
|
|
114
|
+
return typeof module.taskState === 'function' ? module : null;
|
|
115
|
+
} catch {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
88
120
|
const steps = pipelineSteps();
|
|
89
121
|
const checks = gateChecks();
|
|
90
122
|
const problems = [];
|
|
123
|
+
/** Задачи, названные причинами исключений: спрашиваются пачкой после разбора всех шагов. */
|
|
124
|
+
const deferrals = [];
|
|
91
125
|
|
|
92
126
|
for (const step of steps) {
|
|
93
127
|
const declaration = DECLARED[step];
|
|
@@ -103,6 +137,10 @@ for (const step of steps) {
|
|
|
103
137
|
const reason = String(declaration.skip ?? '').trim();
|
|
104
138
|
if (!reason) {
|
|
105
139
|
problems.push(`шаг конвейера «${step}» объявлен исключением без причины — пустая причина не считается`);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
for (const number of taskNumbers(reason)) {
|
|
143
|
+
deferrals.push({ step, number });
|
|
106
144
|
}
|
|
107
145
|
continue;
|
|
108
146
|
}
|
|
@@ -126,6 +164,26 @@ for (const step of Object.keys(DECLARED)) {
|
|
|
126
164
|
}
|
|
127
165
|
}
|
|
128
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Мёртвый номер в причине делает исключение бессрочным, не сказав об этом ни строкой. Первый же
|
|
169
|
+
* отказ сети кончает опрос целиком: спрашивать остальные незачем, а падать проверке, которую
|
|
170
|
+
* гоняют в самолёте, — тем более.
|
|
171
|
+
*/
|
|
172
|
+
const board = deferrals.length > 0 ? await boardModule() : null;
|
|
173
|
+
if (board) {
|
|
174
|
+
for (const { step, number } of deferrals) {
|
|
175
|
+
let state;
|
|
176
|
+
try {
|
|
177
|
+
state = board.taskState(number);
|
|
178
|
+
} catch {
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
if (state && state.exists === false) {
|
|
182
|
+
problems.push(`шаг конвейера «${step}» отложен до задачи ${TASK_KEY}-${number}, а такой задачи в очереди работ нет — отсрочка бессрочная`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
129
187
|
if (problems.length > 0) {
|
|
130
188
|
console.error(`check-push-gate: расхождений ${problems.length}\n`);
|
|
131
189
|
problems.forEach((problem) => console.error(` ${problem}`));
|
|
@@ -76,6 +76,13 @@ function shadowAddresses(url) {
|
|
|
76
76
|
return { shadowName, shadowUrl: shadow.toString(), serviceUrl: service.toString() };
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
// Код, которым проверка объявляет, что смотреть было не на что. Прежде каждый такой выход был
|
|
80
|
+
// нулём: строка о пропуске уходила в вывод, а в сводке гейта пуша ноль стоял рядом с
|
|
81
|
+
// пройденными проверками и ничем от них не отличался — набор читался как проверенный целиком.
|
|
82
|
+
// Число знает и гард пуша: он называет пропущенное вслух, не отбивая пуш, потому что проверка,
|
|
83
|
+
// которой нечего смотреть, поломкой не является.
|
|
84
|
+
const SKIP = Number(process.env.RT_SKIP_CODE ?? 7);
|
|
85
|
+
|
|
79
86
|
function prisma(args, url) {
|
|
80
87
|
return spawnSync('npx', ['prisma', ...args], {
|
|
81
88
|
cwd: ROOT,
|
|
@@ -98,7 +105,7 @@ async function withServiceClient(serviceUrl, run) {
|
|
|
98
105
|
if (SERVER_DOWN_CODES.includes(error?.code)) {
|
|
99
106
|
console.log('check-schema-drift: база недоступна — сверять негде');
|
|
100
107
|
|
|
101
|
-
return
|
|
108
|
+
return SKIP;
|
|
102
109
|
}
|
|
103
110
|
throw error;
|
|
104
111
|
}
|
|
@@ -117,26 +124,26 @@ async function main() {
|
|
|
117
124
|
if (!CONFIG.schemaFile) {
|
|
118
125
|
console.log('check-schema-drift: имя файла схемы не задано — сверять нечего');
|
|
119
126
|
|
|
120
|
-
return
|
|
127
|
+
return SKIP;
|
|
121
128
|
}
|
|
122
129
|
|
|
123
130
|
if (!existsSync(join(ROOT, CONFIG.schemaFile))) {
|
|
124
131
|
console.log('check-schema-drift: схемы нет — сверять нечего');
|
|
125
132
|
|
|
126
|
-
return
|
|
133
|
+
return SKIP;
|
|
127
134
|
}
|
|
128
135
|
|
|
129
136
|
const url = databaseUrl();
|
|
130
137
|
if (!url) {
|
|
131
138
|
console.log('check-schema-drift: адрес базы не задан — сверять негде');
|
|
132
139
|
|
|
133
|
-
return
|
|
140
|
+
return SKIP;
|
|
134
141
|
}
|
|
135
142
|
|
|
136
143
|
if (PRODUCTION_MARKS.some((mark) => url.includes(mark))) {
|
|
137
144
|
console.log('check-schema-drift: адрес боевой — проверка туда не ходит');
|
|
138
145
|
|
|
139
|
-
return
|
|
146
|
+
return SKIP;
|
|
140
147
|
}
|
|
141
148
|
|
|
142
149
|
const { shadowName, shadowUrl, serviceUrl } = shadowAddresses(url);
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* владельца, а четырём незаведённым механикам скидок привязки не нашлось бы
|
|
18
18
|
* вовсе. Ключ связи — сам текст правила, поэтому переформулировать его, забыв
|
|
19
19
|
* поправить привязку, нельзя. Правило без привязки — намерение, и писать его
|
|
20
|
-
* надо как `Q
|
|
20
|
+
* надо как открытый вопрос закона — `Q-<буква закона>-<номер>`.
|
|
21
21
|
*
|
|
22
22
|
* 2. КОНТРАКТ ПРОТИВ ДЕКОРАТОРОВ. Таблица процедур сверяется с тем, что
|
|
23
23
|
* объявлено в `*.procedure.ts` домена: `@RequiresPermission` / `@PublicProcedure`
|
|
@@ -185,6 +185,19 @@ const DEFAULTS = {
|
|
|
185
185
|
bot: '',
|
|
186
186
|
tokenPath: '',
|
|
187
187
|
reviewer: '',
|
|
188
|
+
/**
|
|
189
|
+
* Метка карточки эпика. По ней сверка находит эпики и сверяет их связь с задачами в обе
|
|
190
|
+
* стороны. Слово у каждого дерева своё, общего умолчания нет: не названа — связь не
|
|
191
|
+
* судится вовсе, потому что отличить карточку эпика от обычной задачи станет нечем.
|
|
192
|
+
*/
|
|
193
|
+
epicLabel: '',
|
|
194
|
+
/**
|
|
195
|
+
* Метки, которыми в очереди работ помечен груз — присланные деревьями разборы и
|
|
196
|
+
* предложения. Задачами они не судятся вовсе: заголовка с номером, исполнителя и места
|
|
197
|
+
* на борде у них нет и не будет. Слово у каждого дерева своё, общего умолчания нет:
|
|
198
|
+
* пусто — сверка судит как прежде.
|
|
199
|
+
*/
|
|
200
|
+
cargoLabels: [],
|
|
188
201
|
},
|
|
189
202
|
};
|
|
190
203
|
|
|
@@ -140,7 +140,7 @@ function checkRuleImplementation(specFile, text, mapFile, heading = '## Прав
|
|
|
140
140
|
report(
|
|
141
141
|
mapFile,
|
|
142
142
|
`правило без привязки: «${head.slice(0, 60)}…» — допиши строку с \`файл:символ\`, ` +
|
|
143
|
-
'вердиктом «Не исполняется» с причиной либо перенеси правило в «Открытые вопросы» как Q
|
|
143
|
+
'вердиктом «Не исполняется» с причиной либо перенеси правило в «Открытые вопросы» закона как Q-<буква>-<номер>'
|
|
144
144
|
);
|
|
145
145
|
continue;
|
|
146
146
|
}
|
|
@@ -263,7 +263,7 @@ function checkTracedAnchors() {
|
|
|
263
263
|
report(
|
|
264
264
|
mapFile,
|
|
265
265
|
`привязка ведёт в мёртвый код: \`${symbol}\` объявлен в \`${path}\` и больше нигде не встречается — ` +
|
|
266
|
-
'либо правило исполняется в другом месте, либо ему место в «Открытых вопросах» как Q
|
|
266
|
+
'либо правило исполняется в другом месте, либо ему место в «Открытых вопросах» закона как Q-<буква>-<номер>'
|
|
267
267
|
);
|
|
268
268
|
}
|
|
269
269
|
}
|
|
@@ -64,6 +64,15 @@ function contractRows(text) {
|
|
|
64
64
|
continue;
|
|
65
65
|
}
|
|
66
66
|
const permission = (cells[1].match(/`([^`]+)`/) || [])[1] || cells[1];
|
|
67
|
+
// Строка с кодом отказа во второй ячейке процедурой не считается. Раздел «Контракта»
|
|
68
|
+
// держит две разные таблицы: список процедур с правами и список кодов отказа с поводами,
|
|
69
|
+
// — и по форме строки они неотличимы, обе несут значение в кавычках первой ячейкой.
|
|
70
|
+
// Пока разбор брал любую, спек с объявленными процедурами получал свою таблицу кодов
|
|
71
|
+
// прочитанной как список процедур: каждая её строка становилась процедурой, которой в
|
|
72
|
+
// домене нет.
|
|
73
|
+
if (/^\d{3}$/.test(permission.trim())) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
67
76
|
rows.push({ line: index, name, permission: permission.trim(), short: name.split('.').pop() });
|
|
68
77
|
}
|
|
69
78
|
|
|
@@ -40,6 +40,12 @@ skill_for_default() {
|
|
|
40
40
|
case "$kind" in
|
|
41
41
|
edit)
|
|
42
42
|
case "$target" in
|
|
43
|
+
# Собранное дерево кодом не бывает, и правкой его никто не занимается: путь к
|
|
44
|
+
# артефакту приходит из команды, которая его ЗАПУСКАЕТ. Под артефактом лежат те
|
|
45
|
+
# же имена каталогов, что и под исходником, поэтому ветка стоит первой — любая
|
|
46
|
+
# ниже совпала бы с ним и потребовала правило, которое работе ничего не даёт.
|
|
47
|
+
*/node_modules/* | */dist/* | */build/* | */.nx/* | */coverage/*) return 0 ;;
|
|
48
|
+
|
|
43
49
|
# Надстройка над разложенным текстом — правка того же текста: путь другой, а
|
|
44
50
|
# предмет тот же. Ветка стоит первой, потому что путь надстройки кончается тем
|
|
45
51
|
# же именем файла, что и разложенная копия, и ветки ниже разобрали бы её по
|
|
@@ -93,10 +99,13 @@ skill_for_default() {
|
|
|
93
99
|
# Своё правило линтера кода пишется по тем же соглашениям, что и код под ним.
|
|
94
100
|
*/eslint-rules/*) printf '%s\n' 'typescript-conventions' ;;
|
|
95
101
|
|
|
96
|
-
# Схема
|
|
97
|
-
# времени ставит инструмент в момент заведения —
|
|
98
|
-
# только накатом с нуля, то есть уже после
|
|
99
|
-
|
|
102
|
+
# Схема хранилища, её миграции и настройка клиента: порядок каталогов
|
|
103
|
+
# лексикографический, а метку времени ставит инструмент в момент заведения —
|
|
104
|
+
# цепочка ломается молча и падает только накатом с нуля, то есть уже после
|
|
105
|
+
# слияния. Настройка клиента стоит здесь же: её читает тот же накат, а правилась
|
|
106
|
+
# она без правила под рукой. Правило живёт при поставке.
|
|
107
|
+
*/schema.prisma | */prisma/migrations/* | */prisma.config.ts)
|
|
108
|
+
printf '%s\n' 'git-workflow' ;;
|
|
100
109
|
# Конвейер и образ: проверки решают, что вообще гоняется до слияния, а образ —
|
|
101
110
|
# что приезжает на прод. И то и другое правилось без единого правила поставки.
|
|
102
111
|
*/.github/workflows/*.yml | */.gitlab-ci.yml | */azure-pipelines*.yml)
|
|
@@ -87,8 +87,8 @@ rt_push_checks_default() {
|
|
|
87
87
|
[ -x "$root/$RT_HOOKS_TESTS" ] && printf '%s\n' "bash $RT_HOOKS_TESTS"
|
|
88
88
|
|
|
89
89
|
for check in check-doc-paths check-specs check-file-size check-dupes check-styles \
|
|
90
|
-
check-lib-layers check-reuse check-schema-drift check-states check-state-next \
|
|
91
|
-
check-turn-map check-archive-age check-push-gate; do
|
|
90
|
+
check-glossary check-lib-layers check-reuse check-schema-drift check-states check-state-next \
|
|
91
|
+
check-turn-map check-archive-age check-profile-drift check-hook-scope check-push-gate; do
|
|
92
92
|
[ -f "$root/$RT_CHECKS_DIR/$check.mjs" ] && printf '%s\n' "node $RT_CHECKS_DIR/$check.mjs"
|
|
93
93
|
done
|
|
94
94
|
|
|
@@ -183,6 +183,9 @@ RT_LAWS_DIR="${RT_LAWS_DIR:-docs/constitution}"
|
|
|
183
183
|
RT_RULES_DIR="${RT_RULES_DIR:-.claude/skills}"
|
|
184
184
|
RT_SPECS_DIR="${RT_SPECS_DIR:-docs/specs}"
|
|
185
185
|
|
|
186
|
+
# Каталог замыслов, переживающих одну задачу: порядок задач эпика лежит там, а не в правилах.
|
|
187
|
+
RT_PLANS_DIR="${RT_PLANS_DIR:-docs/plans}"
|
|
188
|
+
|
|
186
189
|
# Главная ветка. Гарду она нужна, чтобы найти общего предка и понять, что ветка сделала с
|
|
187
190
|
# папкой задачи и с архивом. Если общего предка нет, сравнивать не с чем — проверка молчит.
|
|
188
191
|
RT_MAIN_BRANCH="${RT_MAIN_BRANCH:-main}"
|
|
@@ -222,112 +225,10 @@ rt_is_app_code_default() {
|
|
|
222
225
|
esac
|
|
223
226
|
}
|
|
224
227
|
|
|
225
|
-
#
|
|
226
|
-
#
|
|
227
|
-
#
|
|
228
|
-
|
|
229
|
-
# перенаправлением, `tee`, `sed -i`, интерпретатором с heredoc. Отбитая правка дважды за один
|
|
230
|
-
# заход легла именно так, и не увидел этого никто: в дереве она неотличима от положенной
|
|
231
|
-
# инструментом. Разбор — `2026-08-15-guard-denied-shell-wrote-anyway.md`.
|
|
232
|
-
#
|
|
233
|
-
# Список намеренно широк, и цена этого названа: команда чтения, в которой стоит имя
|
|
234
|
-
# интерпретатора, будет отбита наравне с командой правки. Узкий список стоил бы дороже —
|
|
235
|
-
# пропущенная форма записи возвращает обход целиком, а найти её можно только промахом.
|
|
236
|
-
#
|
|
237
|
-
# Перенаправление в пустое устройство и в поток ошибок снимается до разбора: файла оно не
|
|
238
|
-
# пишет, а выглядит как перенаправление в файл. Так глушат вывод команды чтения, и без этого
|
|
239
|
-
# `grep -rn x libs/ 2>/dev/null` судится наравне с записью — правило требуется на чтение, а
|
|
240
|
-
# отбитий, пришедшихся не на правку файла, набирается большинство. Настоящая запись рядом с
|
|
241
|
-
# заглушённым потоком остаётся видной: снимается перенаправление, а не команда целиком.
|
|
242
|
-
rt_shell_writes_default() {
|
|
243
|
-
printf '%s' "$1" \
|
|
244
|
-
| sed -E 's#(&|[0-9]*)>>?[[:space:]]*/dev/(null|stderr)##g; s#[0-9]*>&[0-9-]##g' \
|
|
245
|
-
| grep -Eq \
|
|
246
|
-
'>>?[[:space:]]*[^|&>[:space:]]|\btee\b|\bsed\b[^|]*-i|\bperl\b[^|]*-i|\bpython3?\b|\bnode\b|\bruby\b|\bdd\b[^|]*of=|\bcp\b|\bmv\b|\brm\b|\btouch\b|\btruncate\b|\binstall\b|\bpatch\b|\bgit[[:space:]]+(checkout|restore|apply|stash)\b'
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
# Пути, названные командой оболочки. Печатает по одному в строке; судит их зовущий.
|
|
250
|
-
#
|
|
251
|
-
# Разбирать оболочку по-настоящему нечем — здесь и не разбирают: из текста вынимается всё, что
|
|
252
|
-
# похоже на путь, и каждое отдаётся признаку. Лишнее он отсеет сам, а пропущенное вернуло бы
|
|
253
|
-
# обход. Кавычки снимаются заменой на пробел: путь внутри них тот же самый.
|
|
254
|
-
#
|
|
255
|
-
# Тело документа на месте путей не даёт: там лежит текст, который команда кладёт в файл, а
|
|
256
|
-
# чужой путь, названный в нём словами, требовал бы правила под запись, которой нет. Запись
|
|
257
|
-
# замысла так отбивалась трижды подряд, пока пути под каталогом кода не были названы иначе.
|
|
258
|
-
# Судится заголовок команды — именно в нём стоит тот путь, куда команда пишет.
|
|
259
|
-
#
|
|
260
|
-
# Исключение — интерпретатор: ему код приходит телом, и путь записи стоит именно там. Признак
|
|
261
|
-
# читается у той строки, которая тело открыла, а не у всей команды: тело принадлежит команде
|
|
262
|
-
# своего заголовка. Прежде он читался у всего текста разом, и слово из документа отключало
|
|
263
|
-
# вырезание целиком — строка «**Чем проверяется:** `bash projects/…`» в замысле делала запись
|
|
264
|
-
# `plan.md` правкой кода приложения. Гард отбивал тем самым запись того файла, отсутствием
|
|
265
|
-
# которого он же и отказывает.
|
|
266
|
-
rt_shell_paths_default() {
|
|
267
|
-
text="$(printf '%s' "$1" | tr "\"'\`" ' ')"
|
|
268
|
-
text="$(printf '%s' "$text" | awk '
|
|
269
|
-
function trim(s) { sub(/^[ \t]+/, "", s); sub(/[ \t]+$/, "", s); return s }
|
|
270
|
-
tag != "" {
|
|
271
|
-
if (keep) { print }
|
|
272
|
-
if (trim($0) == tag) { tag = ""; keep = 0 }
|
|
273
|
-
next
|
|
274
|
-
}
|
|
275
|
-
{
|
|
276
|
-
print
|
|
277
|
-
if (match($0, /<<-?[ \t]*[A-Za-z_][A-Za-z0-9_]*/)) {
|
|
278
|
-
t = substr($0, RSTART, RLENGTH)
|
|
279
|
-
sub(/^<<-?[ \t]*/, "", t)
|
|
280
|
-
tag = t
|
|
281
|
-
keep = ($0 ~ /(^|[|;&(]|[ \t])(python3?|node|ruby|perl|php|deno|bun|bash|sh|zsh)([ \t]|$)/)
|
|
282
|
-
}
|
|
283
|
-
}')"
|
|
284
|
-
|
|
285
|
-
# Пути берутся только у тех кусков команды, которые пишут. Прежде брались у всей строки
|
|
286
|
-
# целиком, и команда чтения, сцепленная с записью, отдавала свои пути как цели записи:
|
|
287
|
-
# `python3 <<PY … PY` рядом с `grep -n … projects/…` отбивался за правку кода, которой в нём
|
|
288
|
-
# не было. Отбитий, пришедшихся не на правку файла, набиралось большинство, и цену платил
|
|
289
|
-
# тот, кто просто читал соседний файл в той же строке.
|
|
290
|
-
#
|
|
291
|
-
# Кусок — строка верхнего уровня, а внутри неё `;`, `&&` и `||`. Тело heredoc от своей
|
|
292
|
-
# команды не отрывается: оно едет вместе с ней одним куском, потому что путь записи
|
|
293
|
-
# интерпретатора стоит именно там.
|
|
294
|
-
printf '%s' "$text" | awk '
|
|
295
|
-
function trim(s) { sub(/^[ \t]+/, "", s); sub(/[ \t]+$/, "", s); return s }
|
|
296
|
-
function flush( n, i, part) {
|
|
297
|
-
if (chunk == "") { return }
|
|
298
|
-
if (tag != "") { part = chunk; gsub(/\n/, " ", part); print part; chunk = ""; return }
|
|
299
|
-
n = split(chunk, parts, /;|&&|\|\|/)
|
|
300
|
-
for (i = 1; i <= n; i++) { print parts[i] }
|
|
301
|
-
chunk = ""
|
|
302
|
-
}
|
|
303
|
-
tag != "" {
|
|
304
|
-
chunk = chunk "\n" $0
|
|
305
|
-
if (trim($0) == tag) { flush(); tag = "" }
|
|
306
|
-
next
|
|
307
|
-
}
|
|
308
|
-
{
|
|
309
|
-
chunk = $0
|
|
310
|
-
if (match($0, /<<-?[ \t]*[A-Za-z_][A-Za-z0-9_]*/)) {
|
|
311
|
-
t = substr($0, RSTART, RLENGTH)
|
|
312
|
-
sub(/^<<-?[ \t]*/, "", t)
|
|
313
|
-
tag = t
|
|
314
|
-
next
|
|
315
|
-
}
|
|
316
|
-
flush()
|
|
317
|
-
}
|
|
318
|
-
END { if (chunk != "") { part = chunk; gsub(/\n/, " ", part); print part } }
|
|
319
|
-
' \
|
|
320
|
-
| while IFS= read -r piece; do
|
|
321
|
-
[ -z "$piece" ] && continue
|
|
322
|
-
rt_shell_writes "$piece" || continue
|
|
323
|
-
printf '%s' "$piece" \
|
|
324
|
-
| tr "(),;=" ' ' \
|
|
325
|
-
| tr '[:space:]' '\n' \
|
|
326
|
-
| grep -E '^[A-Za-z0-9_@.-]*/[A-Za-z0-9_@./-]+$' \
|
|
327
|
-
| sed 's|^\./||'
|
|
328
|
-
done \
|
|
329
|
-
| sort -u
|
|
330
|
-
}
|
|
228
|
+
# Разбор команды оболочки — пишет ли она и какие пути называет — живёт соседним файлом:
|
|
229
|
+
# вдвоём с ним профиль перерастал предел длины, а читают эти две функции порознь.
|
|
230
|
+
# shellcheck disable=SC1090
|
|
231
|
+
[ -f "$(dirname "${BASH_SOURCE[0]}")/shell.sh" ] && . "$(dirname "${BASH_SOURCE[0]}")/shell.sh"
|
|
331
232
|
|
|
332
233
|
# Имя ветки, с которой разрешено открывать заявку на слияние: в имени стоит номер задачи.
|
|
333
234
|
# Приставка — либо род правки, либо метка очереди работ: обе формы носят номер, а он и нужен.
|
|
@@ -372,6 +273,12 @@ RT_TASK_BOT="${RT_TASK_BOT:-}"
|
|
|
372
273
|
RT_PULL_TOKEN_VAR="${RT_PULL_TOKEN_VAR:-}"
|
|
373
274
|
RT_PULL_TOKEN_HINT="${RT_PULL_TOKEN_HINT:-}"
|
|
374
275
|
|
|
276
|
+
# Раздел, который тело заявки обязано нести с минуты открытия: решение о слиянии принимается на
|
|
277
|
+
# её странице, где переписки нет вовсе, и сказанного вслух там не остаётся. Умолчание молчит —
|
|
278
|
+
# заголовок пишется языком заявки, а чужих слов пакет не знает: не названный деревом, раздел не
|
|
279
|
+
# судится вовсе.
|
|
280
|
+
RT_PULL_BODY_SECTION="${RT_PULL_BODY_SECTION:-}"
|
|
281
|
+
|
|
375
282
|
# Команда перевода задачи между колонками очереди работ и имя первой колонки — той, из которой
|
|
376
283
|
# задача уходит, когда её берут в работу. Имя без умолчания: колонки дерево называет своими
|
|
377
284
|
# словами, а выдуманное не совпало бы ни с чем и молча выключило бы проверку колонки.
|
|
@@ -419,6 +326,21 @@ rt_pull_state_default() {
|
|
|
419
326
|
printf '%s' "$state"
|
|
420
327
|
}
|
|
421
328
|
|
|
329
|
+
# Свои открытые заявки, помеченные конфликтующими, — по строке на заявку: «#номер ветка».
|
|
330
|
+
# Спрашивает того же помощника очереди работ, что и два соседа выше. Пусто на выходе значит
|
|
331
|
+
# «конфликтующих нет», отказ кода возврата — «спросить некого», и ярус гарда пропускается:
|
|
332
|
+
# отбивать работу на молчании сети значило бы останавливать её всякий раз, когда её не с чем
|
|
333
|
+
# сверить.
|
|
334
|
+
rt_conflicting_pulls_default() {
|
|
335
|
+
command -v node >/dev/null 2>&1 || return 1
|
|
336
|
+
command -v jq >/dev/null 2>&1 || return 1
|
|
337
|
+
[ -f "${RT_BOARD_HELPER:-tools/board.mjs}" ] || return 1
|
|
338
|
+
state="$(node "${RT_BOARD_HELPER:-tools/board.mjs}" conflicts 2>/dev/null)" || return 1
|
|
339
|
+
[ -z "$state" ] && return 1
|
|
340
|
+
printf '%s' "$state" | jq -e 'has("conflicting")' >/dev/null 2>&1 || return 1
|
|
341
|
+
printf '%s' "$state" | jq -r '.conflicting[]? | "#\(.number) \(.branch)"'
|
|
342
|
+
}
|
|
343
|
+
|
|
422
344
|
# Что в этом дереве считается переизобретением. По строке «образец<таб>чем заменить».
|
|
423
345
|
# Образцы узкие намеренно: гард сверяет только НОВЫЙ текст, и широкий образец отбивал бы
|
|
424
346
|
# правку, которая ничего нового не заводит.
|
|
@@ -475,5 +397,6 @@ rt_kit_sources_dir() { rt_kit_sources_dir_default "$@"; }
|
|
|
475
397
|
rt_qa_decorative() { rt_qa_decorative_default "$@"; }
|
|
476
398
|
rt_task_state() { rt_task_state_default "$@"; }
|
|
477
399
|
rt_pull_state() { rt_pull_state_default "$@"; }
|
|
400
|
+
rt_conflicting_pulls() { rt_conflicting_pulls_default "$@"; }
|
|
478
401
|
rt_report_body() { rt_report_body_default "$@"; }
|
|
479
402
|
rt_handoff_allowed_cmd() { rt_handoff_allowed_cmd_default "$@"; }
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Разбор команды оболочки: пишет ли она и какие пути называет.
|
|
3
|
+
#
|
|
4
|
+
# Умолчание пакета, часть профиля дерева — грузится из `project.sh`, рядом с которым лежит.
|
|
5
|
+
# Отдельным файлом потому, что вдвоём они перерастают предел длины, а читают их порознь: одному
|
|
6
|
+
# нужен признак записи, другому — цель.
|
|
7
|
+
|
|
8
|
+
# Пишет ли команда оболочки файл. Успех — да, и тогда пути из неё судятся тем же признаком,
|
|
9
|
+
# что и путь из вызова инструмента правки.
|
|
10
|
+
#
|
|
11
|
+
# Гарды подписаны на инструменты правки файла, и этого мало: ту же правку кладут командой —
|
|
12
|
+
# перенаправлением, `tee`, `sed -i`, интерпретатором с heredoc. Отбитая правка дважды за один
|
|
13
|
+
# заход легла именно так, и не увидел этого никто: в дереве она неотличима от положенной
|
|
14
|
+
# инструментом. Разбор — `2026-08-15-guard-denied-shell-wrote-anyway.md`.
|
|
15
|
+
#
|
|
16
|
+
# Список намеренно широк, и цена этого названа: команда чтения, в которой стоит имя
|
|
17
|
+
# интерпретатора, будет отбита наравне с командой правки. Узкий список стоил бы дороже —
|
|
18
|
+
# пропущенная форма записи возвращает обход целиком, а найти её можно только промахом.
|
|
19
|
+
#
|
|
20
|
+
# Перенаправление в пустое устройство и в поток ошибок снимается до разбора: файла оно не
|
|
21
|
+
# пишет, а выглядит как перенаправление в файл. Так глушат вывод команды чтения, и без этого
|
|
22
|
+
# `grep -rn x libs/ 2>/dev/null` судится наравне с записью — правило требуется на чтение, а
|
|
23
|
+
# отбитий, пришедшихся не на правку файла, набирается большинство. Настоящая запись рядом с
|
|
24
|
+
# заглушённым потоком остаётся видной: снимается перенаправление, а не команда целиком.
|
|
25
|
+
#
|
|
26
|
+
# Стрелка снимается там же и по той же причине. `->` и `=>` в оболочке не значат ничего, а знак
|
|
27
|
+
# в них тот же: команда, печатавшая таблицу «путь -> правило», объявлялась пишущей и отдавала
|
|
28
|
+
# все свои пути под суд. Туда же закрывающая скобка комментария разметки.
|
|
29
|
+
#
|
|
30
|
+
# Цель перенаправления сужена до знаков, из которых собирают пути. Строка цитаты разметки —
|
|
31
|
+
# знак и слово через пробел — от записи в файл одним знаком неотличима, и различает их только
|
|
32
|
+
# цель: за настоящим знаком стоит путь, а не слово словами. Цена названа прямо: путь, набранный
|
|
33
|
+
# не латиницей, записью больше не считается — в дереве таких нет ни одного, а появятся, признак
|
|
34
|
+
# придётся расширить.
|
|
35
|
+
rt_shell_writes_default() {
|
|
36
|
+
printf '%s' "$1" \
|
|
37
|
+
| sed -E 's#(&|[0-9]*)>>?[[:space:]]*/dev/(null|stderr)##g; s#[0-9]*>&[0-9-]##g; s#[-=]+>##g' \
|
|
38
|
+
| grep -Eq \
|
|
39
|
+
'>>?[[:space:]]*[A-Za-z0-9_./~$"'"'"'-]|\btee\b|\bsed\b[^|]*-i|\bperl\b[^|]*-i|\bpython3?\b|\bnode\b|\bruby\b|\bdd\b[^|]*of=|\bcp\b|\bmv\b|\brm\b|\btouch\b|\btruncate\b|\binstall\b|\bpatch\b|\bgit[[:space:]]+(checkout|restore|apply|stash)\b'
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# Пути, названные командой оболочки. Печатает по одному в строке; судит их зовущий.
|
|
43
|
+
#
|
|
44
|
+
# Разбирать оболочку по-настоящему нечем — здесь и не разбирают: из текста вынимается всё, что
|
|
45
|
+
# похоже на путь, и каждое отдаётся признаку. Лишнее он отсеет сам, а пропущенное вернуло бы
|
|
46
|
+
# обход. Кавычки снимаются заменой на пробел: путь внутри них тот же самый.
|
|
47
|
+
#
|
|
48
|
+
# Тело документа на месте путей не даёт: там лежит текст, который команда кладёт в файл, а
|
|
49
|
+
# чужой путь, названный в нём словами, требовал бы правила под запись, которой нет. Запись
|
|
50
|
+
# замысла так отбивалась трижды подряд, пока пути под каталогом кода не были названы иначе.
|
|
51
|
+
# Судится заголовок команды — именно в нём стоит тот путь, куда команда пишет.
|
|
52
|
+
#
|
|
53
|
+
# Исключение — интерпретатор: ему код приходит телом, и путь записи стоит именно там. Признак
|
|
54
|
+
# читается у той строки, которая тело открыла, а не у всей команды: тело принадлежит команде
|
|
55
|
+
# своего заголовка. Прежде он читался у всего текста разом, и слово из документа отключало
|
|
56
|
+
# вырезание целиком — строка «**Чем проверяется:** `bash projects/…`» в замысле делала запись
|
|
57
|
+
# `plan.md` правкой кода приложения. Гард отбивал тем самым запись того файла, отсутствием
|
|
58
|
+
# которого он же и отказывает.
|
|
59
|
+
rt_shell_paths_default() {
|
|
60
|
+
text="$(printf '%s' "$1" | tr "\"'\`" ' ')"
|
|
61
|
+
text="$(printf '%s' "$text" | awk '
|
|
62
|
+
function trim(s) { sub(/^[ \t]+/, "", s); sub(/[ \t]+$/, "", s); return s }
|
|
63
|
+
tag != "" {
|
|
64
|
+
if (keep) { print }
|
|
65
|
+
if (trim($0) == tag) { tag = ""; keep = 0 }
|
|
66
|
+
next
|
|
67
|
+
}
|
|
68
|
+
{
|
|
69
|
+
print
|
|
70
|
+
if (match($0, /<<-?[ \t]*[A-Za-z_][A-Za-z0-9_]*/)) {
|
|
71
|
+
t = substr($0, RSTART, RLENGTH)
|
|
72
|
+
sub(/^<<-?[ \t]*/, "", t)
|
|
73
|
+
tag = t
|
|
74
|
+
keep = ($0 ~ /(^|[|;&(]|[ \t])(python3?|node|ruby|perl|php|deno|bun|bash|sh|zsh)([ \t]|$)/)
|
|
75
|
+
}
|
|
76
|
+
}')"
|
|
77
|
+
|
|
78
|
+
# Пути берутся только у тех кусков команды, которые пишут. Прежде брались у всей строки
|
|
79
|
+
# целиком, и команда чтения, сцепленная с записью, отдавала свои пути как цели записи:
|
|
80
|
+
# `python3 <<PY … PY` рядом с `grep -n … projects/…` отбивался за правку кода, которой в нём
|
|
81
|
+
# не было. Отбитий, пришедшихся не на правку файла, набиралось большинство, и цену платил
|
|
82
|
+
# тот, кто просто читал соседний файл в той же строке.
|
|
83
|
+
#
|
|
84
|
+
# Кусок — строка верхнего уровня, а внутри неё `;`, `&&` и `||`. Тело heredoc от своей
|
|
85
|
+
# команды не отрывается: оно едет вместе с ней одним куском, потому что путь записи
|
|
86
|
+
# интерпретатора стоит именно там.
|
|
87
|
+
printf '%s' "$text" | awk '
|
|
88
|
+
function trim(s) { sub(/^[ \t]+/, "", s); sub(/[ \t]+$/, "", s); return s }
|
|
89
|
+
function flush( n, i, part) {
|
|
90
|
+
if (chunk == "") { return }
|
|
91
|
+
if (tag != "") { part = chunk; gsub(/\n/, " ", part); print part; chunk = ""; return }
|
|
92
|
+
n = split(chunk, parts, /;|&&|\|\|/)
|
|
93
|
+
for (i = 1; i <= n; i++) { print parts[i] }
|
|
94
|
+
chunk = ""
|
|
95
|
+
}
|
|
96
|
+
tag != "" {
|
|
97
|
+
chunk = chunk "\n" $0
|
|
98
|
+
if (trim($0) == tag) { flush(); tag = "" }
|
|
99
|
+
next
|
|
100
|
+
}
|
|
101
|
+
{
|
|
102
|
+
chunk = $0
|
|
103
|
+
if (match($0, /<<-?[ \t]*[A-Za-z_][A-Za-z0-9_]*/)) {
|
|
104
|
+
t = substr($0, RSTART, RLENGTH)
|
|
105
|
+
sub(/^<<-?[ \t]*/, "", t)
|
|
106
|
+
tag = t
|
|
107
|
+
next
|
|
108
|
+
}
|
|
109
|
+
flush()
|
|
110
|
+
}
|
|
111
|
+
END { if (chunk != "") { part = chunk; gsub(/\n/, " ", part); print part } }
|
|
112
|
+
' \
|
|
113
|
+
| while IFS= read -r piece; do
|
|
114
|
+
[ -z "$piece" ] && continue
|
|
115
|
+
rt_shell_writes "$piece" || continue
|
|
116
|
+
# Кусок, который пишет одним лишь перенаправлением, отдаёт цель записи, а не всякое
|
|
117
|
+
# похожее на путь слово из своего текста. Имя файла, названное в аргументе или в
|
|
118
|
+
# содержимом, правкой этого файла не является: команда чтения, сцепленная с записью
|
|
119
|
+
# в свой файл, требовала правила по чужому имени и кончала ход — цена такого отказа
|
|
120
|
+
# равна цене настоящего срабатывания, потому что обойти его нечем.
|
|
121
|
+
#
|
|
122
|
+
# Прочие виды записи разбираются по-прежнему: у правки по месту, копирования,
|
|
123
|
+
# переноса и интерпретатора путь стоит в самой команде и не в одном месте.
|
|
124
|
+
if ! printf '%s' "$piece" | grep -Eq '\bsed\b[^|]*-i|\bperl\b[^|]*-i|\bpython3?\b|\bnode\b|\bruby\b|\bdd\b[^|]*of=|\bcp\b|\bmv\b|\brm\b|\btouch\b|\btruncate\b|\binstall\b|\bpatch\b|\bgit[[:space:]]+(checkout|restore|apply|stash)\b'; then
|
|
125
|
+
printf '%s' "$piece" \
|
|
126
|
+
| grep -oE '(>>?[[:space:]]*|\btee\b([[:space:]]+-a)?[[:space:]]+)[A-Za-z0-9_@.~/-]+' \
|
|
127
|
+
| sed -E 's/^(>>?|tee([[:space:]]+-a)?)[[:space:]]*//' \
|
|
128
|
+
| grep -E '^[A-Za-z0-9_@.-]*/[A-Za-z0-9_@./-]+$' \
|
|
129
|
+
| sed 's|^\./||'
|
|
130
|
+
continue
|
|
131
|
+
fi
|
|
132
|
+
printf '%s' "$piece" \
|
|
133
|
+
| tr "(),;=" ' ' \
|
|
134
|
+
| tr '[:space:]' '\n' \
|
|
135
|
+
| grep -E '^[A-Za-z0-9_@.-]*/[A-Za-z0-9_@./-]+$' \
|
|
136
|
+
| sed 's|^\./||'
|
|
137
|
+
done \
|
|
138
|
+
| sort -u
|
|
139
|
+
}
|
package/assets/docs/GLOSSARY.md
CHANGED
|
@@ -73,4 +73,4 @@
|
|
|
73
73
|
- **бэклог** — очередь работ
|
|
74
74
|
- **линия работ** — эпик
|
|
75
75
|
- **контекст-виндоу** — окно захода, а его доля — заполнение окна
|
|
76
|
-
- **скилл,
|
|
76
|
+
- **скилл, скиллы** — правило, паттерн или скил без закона — по тому, что это на самом деле
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
+
# Местное значение: .claude/rt-kit/browser-device-id — без него браузерные гарды пропускают любой профиль
|
|
2
3
|
# Общий помощник: печатает идентификатор закреплённого профиля браузера.
|
|
3
4
|
#
|
|
4
5
|
# Идентификатор локален для машины и в пакет не едет вовсе. Он берётся из переменной окружения,
|
|
5
|
-
# а если её нет — из файла рядом с конфигом раскладки. Ни там ни там ничего нет —
|
|
6
|
-
#
|
|
7
|
-
#
|
|
6
|
+
# а если её нет — из файла рядом с конфигом раскладки. Ни там ни там ничего нет — все браузерные
|
|
7
|
+
# гарды пропускают: гард, который не может назвать нужный профиль, ничего не предлагает взамен,
|
|
8
|
+
# и слепой отказ только заводил бы работу в тупик.
|
|
9
|
+
#
|
|
10
|
+
# Пропуск при этом не молчит. Ненастроенное дерево получает строку в поток ошибок — один раз на
|
|
11
|
+
# признак, чтобы она не тонула в каждом вызове: молчание тут неотличимо от «всё в порядке», и
|
|
12
|
+
# читается оно как разрешение водить браузер каким угодно профилем. Признак приходит первым
|
|
13
|
+
# доводом от того, кто зовёт; довода нет — метка дневная.
|
|
8
14
|
#
|
|
9
15
|
# Файл с идентификатором в репозиторий не коммитится: у каждой машины он свой.
|
|
10
16
|
|
|
@@ -16,7 +22,17 @@ if [ -n "${RT_BROWSER_DEVICE_ID:-}" ]; then
|
|
|
16
22
|
fi
|
|
17
23
|
|
|
18
24
|
file="${CLAUDE_PROJECT_DIR:-.}/.claude/rt-kit/browser-device-id"
|
|
19
|
-
|
|
25
|
+
|
|
26
|
+
if [ ! -f "$file" ]; then
|
|
27
|
+
key="${1:-$(date +%Y%m%d 2>/dev/null || printf 'nokey')}"
|
|
28
|
+
marker_dir="${TMPDIR:-/tmp}/claude-browser-guard"
|
|
29
|
+
marker="$marker_dir/silent-$key"
|
|
30
|
+
if [ ! -f "$marker" ]; then
|
|
31
|
+
mkdir -p "$marker_dir" 2>/dev/null && : >"$marker" 2>/dev/null
|
|
32
|
+
echo "Профиль браузера этому дереву не назван: нет ни RT_BROWSER_DEVICE_ID, ни .claude/rt-kit/browser-device-id. Браузерные гарды поэтому пропускают всё подряд — это не разрешение ехать, а повод остановиться и сказать владельцу." >&2
|
|
33
|
+
fi
|
|
34
|
+
exit 0
|
|
35
|
+
fi
|
|
20
36
|
|
|
21
37
|
tr -d '[:space:]' <"$file"
|
|
22
38
|
printf '\n'
|