@rt-tools/agent-kit 0.9.0 → 0.10.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/README.md +21 -7
- package/assets/agents/conscience.md +58 -0
- package/assets/agents/prose-editor.md +44 -0
- package/assets/agents/strict-teacher.md +59 -0
- package/assets/checks/board.github.mjs +56 -1
- package/assets/checks/check-board.github.mjs +24 -0
- package/assets/checks/check-doc-paths.mjs +4 -15
- package/assets/checks/check-dupes.mjs +5 -6
- package/assets/checks/check-file-size.mjs +6 -20
- package/assets/checks/check-prose-style.mjs +137 -0
- package/assets/checks/check-reuse.mjs +5 -5
- package/assets/checks/check-styles.mjs +5 -5
- package/assets/checks/lib-common.mjs +3 -3
- package/assets/checks/rt-kit-checks.config.mjs +85 -1
- package/assets/defaults/project.sh +50 -0
- package/assets/docs/GLOSSARY.md +17 -15
- package/assets/hooks/browser-guard-device-id.sh +9 -1
- package/assets/hooks/browser-guard-no-asking.sh +9 -1
- package/assets/hooks/browser-guard-no-listing.sh +9 -1
- package/assets/hooks/browser-guard-no-other-drivers.sh +7 -1
- package/assets/hooks/browser-guard-require-select.sh +11 -2
- package/assets/hooks/claim-guard.sh +113 -0
- package/assets/hooks/conscience-guard.sh +98 -0
- package/assets/hooks/deny-tail.sh +32 -0
- package/assets/hooks/dev-server-guard.sh +8 -2
- package/assets/hooks/docs-guard.sh +14 -2
- package/assets/hooks/exam-guard.sh +121 -0
- package/assets/hooks/git-guard-delivery-signature.sh +75 -0
- package/assets/hooks/git-guard-delivery.sh +154 -68
- package/assets/hooks/git-guard-main.sh +12 -0
- package/assets/hooks/git-guard-push-tests.sh +49 -2
- package/assets/hooks/grill-gate.sh +12 -0
- package/assets/hooks/handoff-entry-guard.sh +71 -0
- package/assets/hooks/postmortem-guard.sh +12 -0
- package/assets/hooks/proposal-guard.sh +12 -0
- package/assets/hooks/prose-style-guard.sh +73 -0
- package/assets/hooks/qa-dataid-guard.sh +12 -1
- package/assets/hooks/rerun-guard.sh +86 -0
- package/assets/hooks/reuse-first-guard.sh +12 -1
- package/assets/hooks/roles.sh +32 -0
- package/assets/hooks/skill-gate.sh +12 -0
- package/assets/hooks/sql-guard-write.sh +2 -1
- package/assets/hooks/sql-guard.sh +12 -1
- package/assets/hooks/task-flow-guard.sh +71 -7
- package/assets/hooks/turn-exit-guard.sh +189 -0
- package/assets/hooks/waiting-turn-guard.sh +12 -0
- package/assets/hooks/window-fill-guard.sh +12 -1
- package/assets/patterns/task-flow-close.md +8 -8
- package/assets/patterns/task-flow-handoff.md +1 -1
- package/assets/patterns/task-flow-resume.md +30 -4
- package/assets/patterns/task-flow-start.md +15 -6
- package/assets/rules/git-workflow.azure.md +36 -9
- package/assets/rules/git-workflow.github.md +74 -9
- package/assets/rules/git-workflow.gitlab.md +40 -12
- package/assets/rules/task-flow.md +123 -71
- package/assets/rules/testing.md +15 -1
- package/assets/samples/tasks/_template/plan.md +4 -1
- package/assets/samples/tasks/_template/progress.md +1 -0
- package/assets/skills/agent-kit.md +33 -0
- package/assets/templates/project.sh +16 -0
- package/bin/agent-kit.d.ts.map +1 -1
- package/bin/agent-kit.js +4 -2
- package/bin/agent-kit.js.map +1 -1
- package/lib/config.d.ts +8 -0
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +1 -0
- package/lib/config.js.map +1 -1
- package/lib/enroll.d.ts +9 -1
- package/lib/enroll.d.ts.map +1 -1
- package/lib/enroll.js +65 -18
- package/lib/enroll.js.map +1 -1
- package/package.json +1 -1
- package/rt-tools-agent-kit-0.10.0.tgz +0 -0
- package/rt-tools-agent-kit-0.9.0.tgz +0 -0
|
@@ -33,7 +33,7 @@ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
|
33
33
|
import { createRequire } from 'node:module';
|
|
34
34
|
import { basename, dirname, join } from 'node:path';
|
|
35
35
|
|
|
36
|
-
import { allowlistOf, CONFIG, ROOT } from './rt-kit-checks.config.mjs';
|
|
36
|
+
import { allowlistOf, baselineOf, CONFIG, ROOT, parseAllowlist } from './rt-kit-checks.config.mjs';
|
|
37
37
|
|
|
38
38
|
const ALLOWLIST = allowlistOf('styles');
|
|
39
39
|
const SOURCE_ROOTS = CONFIG.sourceRoots;
|
|
@@ -51,9 +51,9 @@ const USE_RE = /@(?:use|forward)\s+['"]([^'"]+)['"]/g;
|
|
|
51
51
|
/** Строка списка известного: имя класса и перечень файлов при нём */
|
|
52
52
|
const KEY_RE = /^elem (\S+) @ (.*)$/;
|
|
53
53
|
|
|
54
|
-
const allowlist =
|
|
55
|
-
const known =
|
|
56
|
-
const debt = new Set(allowlist.debt
|
|
54
|
+
const allowlist = parseAllowlist('styles');
|
|
55
|
+
const known = allowlist.keys;
|
|
56
|
+
const debt = new Set(allowlist.debt.keys());
|
|
57
57
|
|
|
58
58
|
/** Строки списка по имени класса: перечень файлов в ключ входит, но сверяется отдельно */
|
|
59
59
|
const knownByName = new Map();
|
|
@@ -223,7 +223,7 @@ for (const [name, files] of [...usedIn].sort(([first], [second]) => first.locale
|
|
|
223
223
|
}
|
|
224
224
|
|
|
225
225
|
if (process.argv.includes('--baseline')) {
|
|
226
|
-
console.log(
|
|
226
|
+
console.log(baselineOf(findings.map((finding) => finding.key).sort(), allowlist));
|
|
227
227
|
process.exit(0);
|
|
228
228
|
}
|
|
229
229
|
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
10
10
|
import { join } from 'node:path';
|
|
11
11
|
|
|
12
|
-
import { CONFIG, ROOT,
|
|
12
|
+
import { CONFIG, ROOT, parseAllowlist } from './rt-kit-checks.config.mjs';
|
|
13
13
|
|
|
14
14
|
const FAMILIES = CONFIG.families;
|
|
15
15
|
/**
|
|
@@ -60,8 +60,8 @@ const isLib = (path) => existsSync(join(ROOT, path, 'project.json'));
|
|
|
60
60
|
* нет вовсе, и прямое чтение роняло проверку отказом «нет такого файла» — то есть первая же
|
|
61
61
|
* установка получала поломку вместо отчёта о том, что долгов нет.
|
|
62
62
|
*/
|
|
63
|
-
const allowlist =
|
|
64
|
-
const pathsOf = (key) =>
|
|
63
|
+
const allowlist = parseAllowlist('lib-layers', ['notDomains', 'legacyDomains', 'legacyLibs', 'singleLayerDomains', 'accepted', 'debt']);
|
|
64
|
+
const pathsOf = (key) => [...allowlist[key].keys()];
|
|
65
65
|
|
|
66
66
|
/** Паттерн `<корень>/x/*` покрывает и сам каталог `<корень>/x`: исключение снимается целиком */
|
|
67
67
|
const matches = (patterns, path) =>
|
|
@@ -187,8 +187,92 @@ export const allowlistOf = (name) => join(CONFIG.allowlistDir, `${name}-allowlis
|
|
|
187
187
|
*/
|
|
188
188
|
export const readAllowlist = (name) => {
|
|
189
189
|
const path = join(ROOT, allowlistOf(name));
|
|
190
|
+
if (!existsSync(path)) {
|
|
191
|
+
return {};
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
195
|
+
} catch (error) {
|
|
196
|
+
// Нечитаемая настройка — это не пустой список, и молчать о ней нельзя: проверка,
|
|
197
|
+
// прочитавшая пустоту вместо перечня, назовёт долгом всё дерево разом.
|
|
198
|
+
console.error(`${allowlistOf(name)}: список известного не прочитан — не разбирается как JSON: ${error.message}`);
|
|
199
|
+
process.exit(1);
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Разбор списка принятого: запись отвечает за себя сама.
|
|
205
|
+
*
|
|
206
|
+
* Причина, написанная прозой на весь список, за отдельную строку не отвечает: список пустеет и
|
|
207
|
+
* наполняется, а причина остаётся прежней — строка, внесённая позже, выглядит покрытой ею. Так
|
|
208
|
+
* и вышло у четырёх списков дерева: шапка говорила о разобранном долге, а под ней лежало
|
|
209
|
+
* принятое, которого в тот день ещё не было.
|
|
210
|
+
*
|
|
211
|
+
* Поэтому форма одна на все списки: сторона — объект, где ключ говорит, что принято, а
|
|
212
|
+
* значение несёт причину и номер задачи, которой запись внесена. Стороны называет зовущий:
|
|
213
|
+
* у большинства списков это `accepted` и `debt`, у иных свои имена, а разбор у всех один. Номер — это дорога
|
|
214
|
+
* к разговору, в котором заглушить разрешили: без него запись объясняет сама себя, а спросить
|
|
215
|
+
* о ней некого.
|
|
216
|
+
*
|
|
217
|
+
* Отказ называет файл и саму запись: список читают не целиком, а по строке, и «где-то здесь
|
|
218
|
+
* неверная запись» стоит того же, что и молчание.
|
|
219
|
+
*/
|
|
220
|
+
export const parseAllowlist = (name, sides = ['accepted', 'debt']) => {
|
|
221
|
+
const file = allowlistOf(name);
|
|
222
|
+
const raw = readAllowlist(name);
|
|
223
|
+
const key = CONFIG.board.taskKey;
|
|
224
|
+
const taskForm = key ? new RegExp(`^${key}-\\d+$`) : /^[A-Za-z]+-\d+$/;
|
|
225
|
+
const refuse = (message) => {
|
|
226
|
+
console.error(`${file}: ${message}`);
|
|
227
|
+
process.exit(1);
|
|
228
|
+
};
|
|
229
|
+
const parseSide = (side) => {
|
|
230
|
+
const entries = raw[side];
|
|
231
|
+
if (entries === undefined) {
|
|
232
|
+
return new Map();
|
|
233
|
+
}
|
|
234
|
+
if (Array.isArray(entries) || typeof entries !== 'object' || entries === null) {
|
|
235
|
+
refuse(`«${side}» записан не объектом — у записи нет места ни для причины, ни для номера задачи`);
|
|
236
|
+
}
|
|
237
|
+
const parsed = new Map();
|
|
238
|
+
for (const [entry, value] of Object.entries(entries)) {
|
|
239
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
240
|
+
refuse(`«${entry}» записан без причины — рядом с записью стоят «reason» и «task»`);
|
|
241
|
+
}
|
|
242
|
+
if (typeof value.reason !== 'string' || value.reason.trim() === '') {
|
|
243
|
+
refuse(`у «${entry}» пустая причина — заглушённое без причины через месяц не отличить от забытого`);
|
|
244
|
+
}
|
|
245
|
+
if (typeof value.task !== 'string' || !taskForm.test(value.task)) {
|
|
246
|
+
refuse(`у «${entry}» нет номера задачи вида «${key || 'КЛЮЧ'}-<номер>» — спросить о записи будет некого`);
|
|
247
|
+
}
|
|
248
|
+
parsed.set(entry, { reason: value.reason, task: value.task });
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return parsed;
|
|
252
|
+
};
|
|
253
|
+
const parsed = Object.fromEntries(sides.map((side) => [side, parseSide(side)]));
|
|
254
|
+
|
|
255
|
+
return { ...parsed, keys: new Set(sides.flatMap((side) => [...parsed[side].keys()])) };
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Заготовка списка принятого для режима пересъёмки: прежние записи сохраняются целиком, а
|
|
260
|
+
* новые приходят с пустой причиной и пустым номером задачи.
|
|
261
|
+
*
|
|
262
|
+
* Пустые поля здесь намеренны. Пересъёмка — это помощник, а не разрешение: заглушить проверку
|
|
263
|
+
* можно только словом владельца, и записать его должен человек. Разбор такую запись отбивает,
|
|
264
|
+
* поэтому список, снятый пересъёмкой и не заполненный, дальше гейта не проходит.
|
|
265
|
+
*/
|
|
266
|
+
export const baselineOf = (keys, parsed, side = 'debt') => {
|
|
267
|
+
const entryOf = (key) => parsed.debt?.get(key) ?? parsed.accepted?.get(key) ?? { reason: '', task: '' };
|
|
268
|
+
const fresh = keys.filter((key) => !parsed.keys.has(key));
|
|
269
|
+
if (fresh.length > 0) {
|
|
270
|
+
console.error(`новых записей ${fresh.length} — у каждой заполняются «reason» и «task», иначе разбор списка отбивает прогон`);
|
|
271
|
+
}
|
|
272
|
+
const filled = Object.fromEntries(keys.map((key) => [key, entryOf(key)]));
|
|
273
|
+
const rest = Object.fromEntries([...(parsed.accepted ?? new Map())].filter(([key]) => !keys.includes(key)));
|
|
190
274
|
|
|
191
|
-
return
|
|
275
|
+
return JSON.stringify(side === 'accepted' ? { accepted: filled } : { accepted: rest, debt: filled }, null, 4);
|
|
192
276
|
};
|
|
193
277
|
|
|
194
278
|
/** Есть ли в дереве то, без чего проверке нечего делать. Нет — она выходит с нулём и говорит это. */
|
|
@@ -50,6 +50,27 @@ RT_HOOKS_TESTS="${RT_HOOKS_TESTS:-.claude/hooks/tests/run.sh}"
|
|
|
50
50
|
# её делают, а всплывает на чужой работе — раскладка отказывает по правленому файлу целиком и не
|
|
51
51
|
# кладёт ни одного другого. Признак — сама настройка раскладки: дерево без неё пакета не ставит,
|
|
52
52
|
# и звать в нём нечего.
|
|
53
|
+
# Тронула ли ветка только тексты. Нулевой код — только тексты, иначе — код.
|
|
54
|
+
#
|
|
55
|
+
# Признак нужен тому дереву, которое дописало в набор гейта тяжёлый шаг: стенд, снимки витрин,
|
|
56
|
+
# сборку образов. Без него такой шаг идёт подряд, чем бы ветка ни была, и пуш коммита, менявшего
|
|
57
|
+
# одну строку в таблице markdown, занимает минуты — за которые владелец успевает прочитать его
|
|
58
|
+
# как зависший и отбить. Правило поставки требует того же от конвейера: ветка, не тронувшая ни
|
|
59
|
+
# строки кода, не поднимает стенда, не снимает кадров и не собирает образов.
|
|
60
|
+
#
|
|
61
|
+
# Текстовой считается ветка, у которой каждый задетый файл — либо `.md`, либо лежит под каталогом
|
|
62
|
+
# текстов. Всё остальное, включая обвязку и настройки, считается кодом: признак обязан ошибаться
|
|
63
|
+
# в сторону лишнего прогона, а не пропущенного. Пустая база означает, что сравнивать не с чем, —
|
|
64
|
+
# тогда признак молчит и гоняется всё.
|
|
65
|
+
rt_push_docs_only_default() {
|
|
66
|
+
[ -z "$1" ] && return 1
|
|
67
|
+
changed="$(git diff --name-only "$1"...HEAD 2>/dev/null)"
|
|
68
|
+
[ -z "$changed" ] && return 1
|
|
69
|
+
printf '%s\n' "$changed" | grep -qvE "(^${RT_DOCS_DIR:-docs}/|\.md\$)" && return 1
|
|
70
|
+
|
|
71
|
+
return 0
|
|
72
|
+
}
|
|
73
|
+
|
|
53
74
|
rt_push_checks_default() {
|
|
54
75
|
runner="$(rt_runner)"
|
|
55
76
|
root="${CLAUDE_PROJECT_DIR:-.}"
|
|
@@ -101,6 +122,9 @@ rt_lint_for_default() {
|
|
|
101
122
|
}
|
|
102
123
|
|
|
103
124
|
# Каталог папок задач. Пусто — ведения работы папкой в дереве нет, и гард замысла молчит.
|
|
125
|
+
# Каталог текстов проекта: по нему признак состава правки отличает текстовую ветку от кодовой.
|
|
126
|
+
RT_DOCS_DIR="${RT_DOCS_DIR:-docs}"
|
|
127
|
+
|
|
104
128
|
RT_TASKS_DIR="${RT_TASKS_DIR:-docs/tasks}"
|
|
105
129
|
|
|
106
130
|
# Каталог записей о законченных работах. Туда переносят то, что объясняет решения закрытой
|
|
@@ -144,6 +168,11 @@ RT_SPECS_DIR="${RT_SPECS_DIR:-docs/specs}"
|
|
|
144
168
|
# папкой задачи и с архивом. Если общего предка нет, сравнивать не с чем — проверка молчит.
|
|
145
169
|
RT_MAIN_BRANCH="${RT_MAIN_BRANCH:-main}"
|
|
146
170
|
|
|
171
|
+
# Имя клиента хостинга в строке запуска. По нему гард перезапуска узнаёт вызов: у каждого вида
|
|
172
|
+
# хостинга свой клиент, и угаданное имя не совпадает ни с чем. Умолчание записано под самый
|
|
173
|
+
# распространённый вид; дерево с другим хостингом называет своё.
|
|
174
|
+
RT_HOST_CLI="${RT_HOST_CLI:-gh}"
|
|
175
|
+
|
|
147
176
|
# Тело PR по его номеру. Обход требования пишут в PR, а в команде слияния его нет — там только
|
|
148
177
|
# номер. Пустой ответ значит «спросить не у кого»: тогда обход ищется только в тексте команды.
|
|
149
178
|
rt_report_body_default() {
|
|
@@ -239,6 +268,12 @@ RT_BOARD_CHECK_CMD="${RT_BOARD_CHECK_CMD:-npm run check:board}"
|
|
|
239
268
|
# Учётная запись, которую ставят исполнителем. Умолчание молчит: у каждого дерева она своя.
|
|
240
269
|
RT_TASK_BOT="${RT_TASK_BOT:-}"
|
|
241
270
|
|
|
271
|
+
# Команда перевода задачи между колонками очереди работ и имя первой колонки — той, из которой
|
|
272
|
+
# задача уходит, когда её берут в работу. Имя без умолчания: колонки дерево называет своими
|
|
273
|
+
# словами, а выдуманное не совпало бы ни с чем и молча выключило бы проверку колонки.
|
|
274
|
+
RT_TASK_MOVE_CMD="${RT_TASK_MOVE_CMD:-npm run task:move}"
|
|
275
|
+
RT_BOARD_BACKLOG="${RT_BOARD_BACKLOG:-}"
|
|
276
|
+
|
|
242
277
|
# Почта, которой подписан коммит машинной учётной записи. Целым значением, а не образцом:
|
|
243
278
|
# служебный адрес хостинга состоит из числа, логина и домена, а сопоставляется по числу — логин
|
|
244
279
|
# рядом с ним не сверяет никто. Образец «число, плюс, логин» прошёл бы с чужим числом, то есть
|
|
@@ -268,6 +303,18 @@ rt_task_state_default() {
|
|
|
268
303
|
printf '%s' "$state"
|
|
269
304
|
}
|
|
270
305
|
|
|
306
|
+
# Состояние заявки одним объектом: exists, draft, author, reviewers, reviewed. Спрашивает того
|
|
307
|
+
# же помощника очереди работ, что и состояние задачи, — чтобы гард и сверка одинаково понимали
|
|
308
|
+
# «у заявки есть разбор». Нет узла, нет помощника, нет сети — молчание, и ярус пропускается.
|
|
309
|
+
rt_pull_state_default() {
|
|
310
|
+
command -v node >/dev/null 2>&1 || return 1
|
|
311
|
+
[ -f "${RT_BOARD_HELPER:-tools/board.mjs}" ] || return 1
|
|
312
|
+
state="$(node "${RT_BOARD_HELPER:-tools/board.mjs}" pr "$1" 2>/dev/null)" || return 1
|
|
313
|
+
[ -z "$state" ] && return 1
|
|
314
|
+
printf '%s' "$state" | jq -e 'has("offline") | not' >/dev/null 2>&1 || return 1
|
|
315
|
+
printf '%s' "$state"
|
|
316
|
+
}
|
|
317
|
+
|
|
271
318
|
# Что в этом дереве считается переизобретением. По строке «образец<таб>чем заменить».
|
|
272
319
|
# Образцы узкие намеренно: гард сверяет только НОВЫЙ текст, и широкий образец отбивал бы
|
|
273
320
|
# правку, которая ничего нового не заводит.
|
|
@@ -306,6 +353,8 @@ rt_qa_decorative_default() {
|
|
|
306
353
|
# Умолчания под общими именами. Надстройка проекта объявит любое из них заново — и позовёт
|
|
307
354
|
# отсюда то же имя с суффиксом `_default` для всего, чего не назвала сама.
|
|
308
355
|
rt_push_checks() { rt_push_checks_default "$@"; }
|
|
356
|
+
rt_push_docs_only() { rt_push_docs_only_default "$@"; }
|
|
357
|
+
|
|
309
358
|
rt_docs_pair_for() { rt_docs_pair_for_default "$@"; }
|
|
310
359
|
rt_lint_for() { rt_lint_for_default "$@"; }
|
|
311
360
|
rt_task_branch_ok() { rt_task_branch_ok_default "$@"; }
|
|
@@ -315,5 +364,6 @@ rt_shell_writes() { rt_shell_writes_default "$@"; }
|
|
|
315
364
|
rt_shell_paths() { rt_shell_paths_default "$@"; }
|
|
316
365
|
rt_qa_decorative() { rt_qa_decorative_default "$@"; }
|
|
317
366
|
rt_task_state() { rt_task_state_default "$@"; }
|
|
367
|
+
rt_pull_state() { rt_pull_state_default "$@"; }
|
|
318
368
|
rt_report_body() { rt_report_body_default "$@"; }
|
|
319
369
|
rt_handoff_allowed_cmd() { rt_handoff_allowed_cmd_default "$@"; }
|
package/assets/docs/GLOSSARY.md
CHANGED
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
| Раскладка | Перенос ресурса из пакета в дерево по его роду и настройке слоя |
|
|
31
31
|
| Разложенный файл | Файл в дереве с шапкой пакета. Правится не на месте, а надстройкой: правка на месте теряется на следующей раскладке |
|
|
32
32
|
| Надстройка | Файл дерева, который сливается с разложенным по заголовкам разделов |
|
|
33
|
+
| Выключенная роль | Роль, вызов которой дерево перестало считать обязательным: гард при ней молчит. Названа списком в настройке дерева, из раскладки не убирается и зовётся руками |
|
|
33
34
|
| Наблюдение | Строка о событии слоя правил: правило загружено, гейт отбил, гард отказал. Пишет гард, живёт в дереве, наружу уезжает счётчиками. Журналом не называется |
|
|
34
35
|
| Сводка | Что наблюдения говорят за отрезок дней: чем пользовались, чем ни разу, обо что спотыкались |
|
|
35
36
|
| Предложение | Готовая формулировка правки правил с адресом: пакет, компаньон или дерево. Приносит разбор закрытой задачи или слово посреди работы, отправляет человек командой |
|
|
@@ -37,21 +38,22 @@
|
|
|
37
38
|
|
|
38
39
|
## Работа
|
|
39
40
|
|
|
40
|
-
| Термин
|
|
41
|
-
|
|
|
42
|
-
| Задача
|
|
43
|
-
| PR
|
|
44
|
-
| Очередь работ
|
|
45
|
-
| Папка задачи
|
|
46
|
-
| Разбор
|
|
47
|
-
| Замысел
|
|
48
|
-
| Ход работы
|
|
49
|
-
|
|
|
50
|
-
|
|
|
51
|
-
|
|
|
52
|
-
|
|
|
53
|
-
|
|
|
54
|
-
|
|
|
41
|
+
| Термин | Что это |
|
|
42
|
+
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
43
|
+
| Задача | Единица работы в очереди работ. Заводится до ветки, и номер её стоит в имени ветки и в заголовке PR |
|
|
44
|
+
| PR | Заявка на слияние: то же название, что у задачи, переведённое в сделанное. Отчётом, пул-реквестом и мёрдж-реквестом не называется — ни в файлах, ни в разговоре |
|
|
45
|
+
| Очередь работ | Доска, на которой видно состояние каждой задачи. Ветки она не видит |
|
|
46
|
+
| Папка задачи | Одна работа от разбора до слияния: разбор просьбы, замысел, ход работы. Умирает со слиянием — разбирается, и объясняющее решение уезжает в архив |
|
|
47
|
+
| Разбор | Расспрос владельца до первой правки. Записывается его словами и задним числом не переписывается |
|
|
48
|
+
| Замысел | Файл папки задачи: след задачи и этапы с признаками готовности. После написания не правится — с ним сверяют результат при приёмке |
|
|
49
|
+
| Ход работы | Файл папки задачи: «Где стоим», решения по ходу с причинами, записи заходов. Единственное место, где отмечается сделанное. Журналом не называется |
|
|
50
|
+
| Состояние работы | Единица, которой работа ведётся: у каждого состояния названы вход, обязательное действие и выход. Объявляется машиночитаемой строкой в разделе «Где стоим» хода работы — гард судит объявленный переход, а не наличие файлов |
|
|
51
|
+
| След задачи | Раздел замысла: какие спеки, законы, правила и части кода работа задевает |
|
|
52
|
+
| Заход | Одна сессия работы над задачей. Работа живёт дольше одного захода, и между ними её состояние держит только ход работы |
|
|
53
|
+
| Заполнение окна | Доля места захода, которую он уже занял: вход, запись в кэш, прочитанное из кэша и вывод последнего ответа, делённые на размер окна. Не «расход» и не «бюджет»: речь о месте, а не о деньгах |
|
|
54
|
+
| Передача | Текст, которым заход закрывается: рабочее дерево, ветка, задача, где лежит ход работы, что сделано, следующий шаг, особенности захода. Кладётся вне дерева и не коммитится |
|
|
55
|
+
| Эпик | Серия задач одной темы, выполняемых в назначенном порядке. Живёт в двух местах сразу: карточка в очереди работ с меткой эпика и замысел рядом с ней — что за возможность разрабатывается, какие задачи входят и в каком порядке. Шире одной ветки. Линией работ не называется |
|
|
56
|
+
| Архив | Записи о состоявшемся: что объясняет закрытое решение. После выкатки не правится |
|
|
55
57
|
|
|
56
58
|
## Проверки
|
|
57
59
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# rt-hook: PreToolUse mcp__claude-in-chrome__select_browser
|
|
3
|
+
# Требует: hooks/deny-tail.sh
|
|
3
4
|
# Гард выбора браузера. PreToolUse на выборе браузера расширением.
|
|
4
5
|
#
|
|
5
6
|
# Отклоняет любой профиль, кроме закреплённого: чужой стоит лишнего круга и приводит в браузер,
|
|
@@ -24,5 +25,12 @@ if [ "$requested" = "$device_id" ]; then
|
|
|
24
25
|
exit 0
|
|
25
26
|
fi
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
# Общий хвост отказа: два законных хода и законная форма обхода, если она у отказа есть. Файл
|
|
29
|
+
# может быть не разложен — тогда хвоста нет, а причина отказа остаётся прежней.
|
|
30
|
+
# shellcheck disable=SC1090
|
|
31
|
+
[ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" ] \
|
|
32
|
+
&& . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" 2>/dev/null
|
|
33
|
+
command -v rt_deny_tail >/dev/null 2>&1 || rt_deny_tail() { :; }
|
|
34
|
+
|
|
35
|
+
echo "Профиль «${requested}» не тот, что закреплён за проектом. Бери ${device_id} — единственный профиль, где сделан вход. $(rt_deny_tail)" >&2
|
|
28
36
|
exit 2
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# rt-hook: PreToolUse AskUserQuestion
|
|
3
|
+
# Требует: hooks/deny-tail.sh
|
|
3
4
|
# PreToolUse guard for AskUserQuestion.
|
|
4
5
|
#
|
|
5
6
|
# The browser choice is already made and pinned in browser-device-id.sh — asking again is noise, and the
|
|
@@ -23,5 +24,12 @@ questions="$(printf '%s' "$input" | jq -r '[.tool_input.questions[]? | .question
|
|
|
23
24
|
|
|
24
25
|
printf '%s' "$questions" | grep -qiE 'deviceid|device id|(which|what|pick|choose|select)[^.]{0,25}\bbrowser\b|\bbrowser\b[^.]{0,25}(profile|to use)|как(ой|ую)?[^.]{0,15}браузер|брауз[а-я]*[^.]{0,20}(использовать|выбрать|выбор|нужен|запустить)' || exit 0
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
# Общий хвост отказа: два законных хода и законная форма обхода, если она у отказа есть. Файл
|
|
28
|
+
# может быть не разложен — тогда хвоста нет, а причина отказа остаётся прежней.
|
|
29
|
+
# shellcheck disable=SC1090
|
|
30
|
+
[ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" ] \
|
|
31
|
+
&& . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" 2>/dev/null
|
|
32
|
+
command -v rt_deny_tail >/dev/null 2>&1 || rt_deny_tail() { :; }
|
|
33
|
+
|
|
34
|
+
echo "Do not ask which browser to use — the profile is pinned. Call select_browser with deviceId ${device_id} ('Main' profile). $(rt_deny_tail)" >&2
|
|
27
35
|
exit 2
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# rt-hook: PreToolUse mcp__claude-in-chrome__(list_connected_browsers|switch_browser)
|
|
3
|
+
# Требует: hooks/deny-tail.sh
|
|
3
4
|
# Гард перечисления и переключения браузеров. PreToolUse.
|
|
4
5
|
#
|
|
5
6
|
# Сессии проекта живут в одном закреплённом профиле. Перечисление и переключение отдают общие
|
|
@@ -14,5 +15,12 @@ cat >/dev/null 2>&1
|
|
|
14
15
|
device_id="$("${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/browser-device-id.sh" 2>/dev/null)"
|
|
15
16
|
[ -z "$device_id" ] && exit 0
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
# Общий хвост отказа: два законных хода и законная форма обхода, если она у отказа есть. Файл
|
|
19
|
+
# может быть не разложен — тогда хвоста нет, а причина отказа остаётся прежней.
|
|
20
|
+
# shellcheck disable=SC1090
|
|
21
|
+
[ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" ] \
|
|
22
|
+
&& . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" 2>/dev/null
|
|
23
|
+
command -v rt_deny_tail >/dev/null 2>&1 || rt_deny_tail() { :; }
|
|
24
|
+
|
|
25
|
+
echo "Не перечисляй и не переключай браузеры. Вызови выбор браузера с профилем ${device_id} — единственным, где сделан вход. $(rt_deny_tail)" >&2
|
|
18
26
|
exit 2
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# rt-hook: PreToolUse mcp__playwright__.*|mcp__chrome-devtools__.*|Bash
|
|
3
|
+
# Требует: hooks/deny-tail.sh
|
|
3
4
|
# Гард обходных путей к браузеру. PreToolUse.
|
|
4
5
|
#
|
|
5
6
|
# Закрепление профиля чего-то стоит только тогда, когда дверь одна. Здесь перечислены двери,
|
|
@@ -19,8 +20,13 @@ device_id="$("${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/browser-device-id.sh" 2>/de
|
|
|
19
20
|
|
|
20
21
|
tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
|
|
21
22
|
|
|
23
|
+
# shellcheck disable=SC1090
|
|
24
|
+
[ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" ] \
|
|
25
|
+
&& . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" 2>/dev/null
|
|
26
|
+
command -v rt_deny_tail >/dev/null 2>&1 || rt_deny_tail() { :; }
|
|
27
|
+
|
|
22
28
|
deny() {
|
|
23
|
-
echo "$1 Води браузер закреплённым расширением: выбери профиль ${device_id} и работай его инструментами." >&2
|
|
29
|
+
echo "$1 Води браузер закреплённым расширением: выбери профиль ${device_id} и работай его инструментами. $(rt_deny_tail)" >&2
|
|
24
30
|
exit 2
|
|
25
31
|
}
|
|
26
32
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# rt-hook: PreToolUse mcp__claude-in-chrome__.*
|
|
3
|
+
# Требует: hooks/deny-tail.sh
|
|
3
4
|
# Гард свежести выбора браузера. PreToolUse на всех остальных вызовах расширения.
|
|
4
5
|
#
|
|
5
6
|
# ЗАЧЕМ ОН ЕСТЬ — отказ, из которого он вырос: расширение действует на тот браузер, который
|
|
@@ -35,8 +36,16 @@ ttl=300
|
|
|
35
36
|
sid="$(printf '%s' "$input" | jq -r '.session_id // "nosession"' 2>/dev/null)"
|
|
36
37
|
marker="${TMPDIR:-/tmp}/claude-browser-guard/${sid}"
|
|
37
38
|
|
|
39
|
+
# Общий хвост отказа: два законных хода и законная форма обхода, если она у отказа есть. Файл
|
|
40
|
+
# может быть не разложен — тогда хвоста нет, а причина отказа остаётся прежней.
|
|
41
|
+
# shellcheck disable=SC1090
|
|
42
|
+
[ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" ] \
|
|
43
|
+
&& . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" 2>/dev/null
|
|
44
|
+
command -v rt_deny_tail >/dev/null 2>&1 || rt_deny_tail() { :; }
|
|
45
|
+
|
|
46
|
+
|
|
38
47
|
if [ ! -f "$marker" ]; then
|
|
39
|
-
echo "В этой сессии браузер не выбран. Вызови выбор браузера с профилем ${device_id} до любого другого вызова." >&2
|
|
48
|
+
echo "В этой сессии браузер не выбран. Вызови выбор браузера с профилем ${device_id} до любого другого вызова. $(rt_deny_tail)" >&2
|
|
40
49
|
exit 2
|
|
41
50
|
fi
|
|
42
51
|
|
|
@@ -46,7 +55,7 @@ age=$(( now - stamped ))
|
|
|
46
55
|
|
|
47
56
|
if [ "$age" -gt "$ttl" ]; then
|
|
48
57
|
rm -f "$marker" 2>/dev/null
|
|
49
|
-
echo "Последний выбор браузера был ${age} с назад (предел ${ttl} с) — на таких перерывах активный браузер расширения уплывает, и это может быть уже не закреплённый профиль. Вызови выбор с профилем ${device_id} заново и повтори." >&2
|
|
58
|
+
echo "Последний выбор браузера был ${age} с назад (предел ${ttl} с) — на таких перерывах активный браузер расширения уплывает, и это может быть уже не закреплённый профиль. Вызови выбор с профилем ${device_id} заново и повтори. $(rt_deny_tail)" >&2
|
|
50
59
|
exit 2
|
|
51
60
|
fi
|
|
52
61
|
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# rt-hook: Stop
|
|
3
|
+
# Гард утверждения: сказанное владельцу о состоянии дерева несёт команду, показавшую это. Stop.
|
|
4
|
+
#
|
|
5
|
+
# Зачем именно так. Правило текстов само говорит, что ответ владельцу не читает ни одна
|
|
6
|
+
# проверка: за файл отвечает гейт, за ответ — автор, и цену ошибки в ответе платит владелец.
|
|
7
|
+
# Восемь разборов подряд про одно и то же — готовым названо лежащее на диске, снятыми названы
|
|
8
|
+
# только локальные ветки, проверенным назван набор уже конвейерного, состоянием дерева названа
|
|
9
|
+
# отставшая локальная ссылка. Каждый раз в правило дописывалась статья, и каждый раз промах
|
|
10
|
+
# повторялся: текст читают в начале захода, а утверждение говорят в конце.
|
|
11
|
+
#
|
|
12
|
+
# Что судится: текст, сказанный владельцу за этот ход. Вывод инструмента и записанное в файл
|
|
13
|
+
# сюда не входят — их читают гейт и сверки.
|
|
14
|
+
#
|
|
15
|
+
# Как судится: у каждого слова-утверждения назван свой род команды. Утверждение о зелёном
|
|
16
|
+
# наборе подтверждает прогон набора, о запушенном — вызов пуша, о снятом — вызов удаления.
|
|
17
|
+
# Команда ищется в этом же ходе: состояние дерева меняется, и вывод прошлого хода о нынешнем
|
|
18
|
+
# уже не говорит.
|
|
19
|
+
#
|
|
20
|
+
# Чего гард не судит. Неверный вывод: об образце, судимом по одному его файлу, и о пути, которым
|
|
21
|
+
# человек не пойдёт, машине судить нечем — там нет ни слова-утверждения, ни команды, с которой
|
|
22
|
+
# сверять. Эти случаи держит правило ведения работы, и это известная граница гарда, а не
|
|
23
|
+
# обещание.
|
|
24
|
+
#
|
|
25
|
+
# ОТКАЗ В ПОЛЬЗУ РАБОТЫ: при любой ошибке, нехватке `jq`, отсутствии записи хода или пустом
|
|
26
|
+
# тексте ход РАЗРЕШАЕТСЯ (exit 0). Сломанный гард не имеет права заклинить разговор.
|
|
27
|
+
|
|
28
|
+
input="$(cat 2>/dev/null)"
|
|
29
|
+
[ -z "$input" ] && exit 0
|
|
30
|
+
command -v jq >/dev/null 2>&1 || exit 0
|
|
31
|
+
|
|
32
|
+
# Повторный заход по тому же ходу не судится: гард сказал своё один раз и отпускает.
|
|
33
|
+
active="$(printf '%s' "$input" | jq -r '.stop_hook_active // false' 2>/dev/null)"
|
|
34
|
+
[ "$active" = "true" ] && exit 0
|
|
35
|
+
|
|
36
|
+
transcript="$(printf '%s' "$input" | jq -r '.transcript_path // empty' 2>/dev/null)"
|
|
37
|
+
[ -z "$transcript" ] && exit 0
|
|
38
|
+
[ -f "$transcript" ] || exit 0
|
|
39
|
+
|
|
40
|
+
# Ход — всё, что записано после последней настоящей реплики владельца: ответ инструмента
|
|
41
|
+
# приходит той же ролью и репликой не считается.
|
|
42
|
+
turn="$(tail -n 400 "$transcript" 2>/dev/null | jq -s -r '
|
|
43
|
+
def is_input:
|
|
44
|
+
.type == "user"
|
|
45
|
+
and (((.message.content // []) | if type == "array"
|
|
46
|
+
then ([.[] | select(.type == "tool_result")] | length)
|
|
47
|
+
else 0 end) == 0);
|
|
48
|
+
|
|
49
|
+
(map(is_input) | rindex(true)) as $i
|
|
50
|
+
| (if $i == null then [] else .[$i:] end) as $turn
|
|
51
|
+
| [$turn[] | select(.type == "assistant") | (.message.content // [])[]
|
|
52
|
+
| select(.type == "text") | (.text // "")] as $said
|
|
53
|
+
| [$turn[] | select(.type == "assistant") | (.message.content // [])[]
|
|
54
|
+
| select(.type == "tool_use") | (.input.command // "")] as $ran
|
|
55
|
+
| { said: ($said | join("\n")), ran: ($ran | join("\n")) }
|
|
56
|
+
' 2>/dev/null)"
|
|
57
|
+
|
|
58
|
+
[ -z "$turn" ] && exit 0
|
|
59
|
+
|
|
60
|
+
said="$(printf '%s' "$turn" | jq -r '.said // ""' 2>/dev/null)"
|
|
61
|
+
ran="$(printf '%s' "$turn" | jq -r '.ran // ""' 2>/dev/null)"
|
|
62
|
+
[ -z "$said" ] && exit 0
|
|
63
|
+
|
|
64
|
+
# Карта утверждений: что сказано о дереве | чем это показывают | что назвать в отказе.
|
|
65
|
+
#
|
|
66
|
+
# Слово берётся в той форме, в какой его говорят владельцу. Будущее время сюда не идёт:
|
|
67
|
+
# «проверю» и «запущу» утверждениями не являются — они обещание, и врать им нечем.
|
|
68
|
+
claims=(
|
|
69
|
+
'проверено|прогнал[а]?|тесты (зелёные|прошли)|линт(ер)? (зелёный|прошёл|чистый)|сборка (зелёная|прошла)|набор зелёный|проверки зелёные|спеки зелёные|всё зелен(о|ое)§nx (test|lint|build|run|affected|run-many)|npm (run|test)|pnpm (run|exec|test)|jest|vitest|playwright|check:|run\.sh§команду набора — прогон тестов, линта или сборки'
|
|
70
|
+
'запушен[аоы]?|запушил[а]?|пуш прошёл|ветка уехала§git push§`git push`'
|
|
71
|
+
'(PR|правка|ветка|работа)[^.]{0,20}(влит|слит|смержен)|влит[оа] в|слит[оа] в§git merge|gh pr merge§`git merge` или `gh pr merge`'
|
|
72
|
+
'(ветки|ветка|файлы|файл|папка|каталог)[^.]{0,40}(снят|удал|почищ|вычищ)|снят[оыа] с§git branch|git push .*--delete|git rm|gh api|rm §команду удаления — `git branch -d`, `git push --delete` или `git rm`'
|
|
73
|
+
'прогон (зелёный|прошёл|кончился)|конвейер зелёный|проверки на PR зелёные§gh run§`gh run list` или `gh run view`'
|
|
74
|
+
'(работа|правка|задача) готова|можно вливать|PR открыт|черновик снят§gh pr §`gh pr create`, `gh pr view` или `gh pr ready`'
|
|
75
|
+
'задача заведена|задача (в|переведена в) колонк|колонка переведена§gh issue|gh api|task:new|task:move|board\.mjs§команду очереди работ — заведение задачи или перевод колонки'
|
|
76
|
+
'(в дереве|в репозитории|здесь|такого файла|такой команды)[^.]{0,30}(нет|не бывает)|не заводили|нигде не встречается§grep|rg |ls |find |git ls-files|git grep|git log|cat §команду поиска — `grep`, `git ls-files` или обход каталога'
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
for row in "${claims[@]}"; do
|
|
80
|
+
words="${row%%§*}"
|
|
81
|
+
rest="${row#*§}"
|
|
82
|
+
proof="${rest%%§*}"
|
|
83
|
+
name="${rest#*§}"
|
|
84
|
+
|
|
85
|
+
found="$(printf '%s' "$said" | grep -oiE "$words" 2>/dev/null | head -1)"
|
|
86
|
+
[ -z "$found" ] && continue
|
|
87
|
+
printf '%s' "$ran" | grep -qiE "$proof" 2>/dev/null && continue
|
|
88
|
+
|
|
89
|
+
reason="BLOCKED by claim-guard: владельцу сказано «${found}» — это утверждение о состоянии дерева, а команды, которая его показывает, за этот ход не было.
|
|
90
|
+
|
|
91
|
+
Утверждение о дереве стоит ровно столько, сколько команда, его показавшая: сказанное без команды владелец читает как проверенный факт и узнаёт о расхождении последним.
|
|
92
|
+
|
|
93
|
+
Запусти ${name} этим же ходом и назови её вывод — либо убери утверждение из ответа.
|
|
94
|
+
|
|
95
|
+
Гард судит один ход: следующий заход не отбивается."
|
|
96
|
+
|
|
97
|
+
# Общий хвост отказа: два законных хода. Файл может быть не разложен — тогда хвоста нет,
|
|
98
|
+
# а причина отказа остаётся прежней.
|
|
99
|
+
# shellcheck disable=SC1090
|
|
100
|
+
[ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" ] \
|
|
101
|
+
&& . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deny-tail.sh" 2>/dev/null
|
|
102
|
+
command -v rt_deny_tail >/dev/null 2>&1 || rt_deny_tail() { :; }
|
|
103
|
+
deny_tail_text="$(rt_deny_tail "")"
|
|
104
|
+
[ -n "$deny_tail_text" ] && reason="${reason}
|
|
105
|
+
|
|
106
|
+
${deny_tail_text}"
|
|
107
|
+
|
|
108
|
+
jq -n --arg r "$reason" '{decision:"block",reason:$r}' 2>/dev/null \
|
|
109
|
+
|| printf '{"decision":"block","reason":"claim-guard: утверждение о дереве не подтверждено командой."}\n'
|
|
110
|
+
exit 0
|
|
111
|
+
done
|
|
112
|
+
|
|
113
|
+
exit 0
|