agent-quality-kit 0.14.0 → 0.16.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 +88 -18
- package/README.ru.md +91 -19
- package/kit/docs/ai/index.md +1 -0
- package/kit/docs/ai/operational-gates.md +275 -0
- package/kit/gates/_target.sh +53 -0
- package/kit/gates/ci-actually-fails/check.sh +18 -3
- package/kit/gates/ci-actually-fails/green/.github/workflows/ci.yml +10 -0
- package/kit/gates/entry-commands-exist/check.sh +88 -12
- package/kit/gates/hook-actually-fires/README.md +12 -0
- package/kit/gates/hook-actually-fires/check.sh +66 -6
- package/kit/gates/hook-actually-fires/gate.yml +2 -2
- package/kit/gates/hook-actually-fires/green/.claude/hooks/auto-format.sh +3 -0
- package/kit/gates/hook-actually-fires/green/.claude/hooks/block-dangerous.sh +3 -0
- package/kit/gates/hook-actually-fires/green/.claude/hooks/done.sh +3 -0
- package/kit/gates/hook-actually-fires/green/.claude/hooks/idle.sh +3 -0
- package/kit/gates/hook-actually-fires/green/.claude/hooks/prompt.sh +3 -0
- package/kit/gates/hook-actually-fires/green/.claude/hooks/session.mjs +1 -0
- package/kit/gates/hook-actually-fires/green/.claude/hooks/stop-gate.sh +3 -0
- package/kit/gates/hook-actually-fires/green/.claude/settings.json +12 -0
- package/kit/gates/test-not-adjusted/README.md +31 -0
- package/llms.txt +26 -7
- package/package.json +1 -1
- package/tool/commands/context.mjs +39 -41
- package/tool/commands/doctor-catalog.mjs +35 -10
- package/tool/commands/doctor.mjs +18 -26
- package/tool/commands/feedback.mjs +231 -0
- package/tool/commands/gates.mjs +12 -6
- package/tool/commands/project.mjs +11 -13
- package/tool/commands/prompt.mjs +2 -1
- package/tool/commands/report.mjs +19 -3
- package/tool/commands/vitals.mjs +9 -3
- package/tool/i18n/en-docs.mjs +19 -2
- package/tool/i18n/en-gates.mjs +35 -0
- package/tool/i18n/en.mjs +29 -2
- package/tool/i18n/ru-docs.mjs +18 -2
- package/tool/i18n/ru-gates.mjs +36 -0
- package/tool/i18n/ru.mjs +27 -2
- package/tool/lib/adopt.mjs +58 -4
- package/tool/lib/ask.mjs +118 -0
- package/tool/lib/brief.mjs +17 -38
- package/tool/lib/core.mjs +49 -12
- package/tool/lib/execution.mjs +32 -1
- package/tool/lib/gate-worker.mjs +4 -1
- package/tool/lib/manifest.mjs +39 -13
- package/tool/lib/prove.mjs +3 -3
- package/tool/lib/run.mjs +142 -10
- package/tool/program.mjs +6 -0
- package/tool/selfcheck/smoke/_fixture.mjs +13 -1
- package/tool/selfcheck/smoke/fail-closed.test.mjs +96 -1
- package/tool/selfcheck/smoke/feedback-send.test.mjs +87 -0
- package/tool/selfcheck/smoke/first-run.test.mjs +67 -3
- package/tool/selfcheck/smoke/preflight.test.mjs +83 -0
- package/tool/selfcheck/smoke/verdict.test.mjs +50 -4
- package/tool/selfcheck/smoke/version-sync.test.mjs +140 -0
- package/tool/selfcheck/smoke.sh +106 -4
- package/tool/selfcheck/units-ask.mjs +85 -0
- package/tool/selfcheck/units-brief.mjs +3 -13
- package/tool/selfcheck/units-context.mjs +2 -1
- package/tool/selfcheck/units-execution.mjs +37 -1
- package/tool/selfcheck/units-feedback.mjs +137 -0
- package/tool/selfcheck/units-level.mjs +41 -1
- package/tool/selfcheck/units-repo.mjs +75 -0
- package/tool/selfcheck/units-vitals.mjs +27 -0
- package/kit/gates/entry-links-exist/README.md +0 -27
- package/kit/gates/entry-links-exist/check.sh +0 -33
- package/kit/gates/entry-links-exist/gate.yml +0 -17
- package/kit/gates/entry-links-exist/green/AGENTS.md +0 -10
- package/kit/gates/entry-links-exist/green/rules/general.md +0 -3
- package/kit/gates/entry-links-exist/red/AGENTS.md +0 -3
- package/kit/gates/no-phantom-package/README.md +0 -84
- package/kit/gates/no-phantom-package/check.sh +0 -168
- package/kit/gates/no-phantom-package/gate.yml +0 -20
- package/kit/gates/no-phantom-package/green/AGENTS.md +0 -15
- package/kit/gates/no-phantom-package/red/AGENTS.md +0 -15
package/tool/i18n/ru-docs.mjs
CHANGED
|
@@ -26,6 +26,10 @@ const ruDocs = {
|
|
|
26
26
|
unknownHook: "посмотреть не смогли — неизвестно, а не «нет»",
|
|
27
27
|
preCommitOk: "прописан в .git/hooks",
|
|
28
28
|
preCommitNo: "в .git/hooks его нет: запись в конфиге — намерение, а не защита",
|
|
29
|
+
// ХУК ЕСТЬ, НО НЕ НАШ. Ставить нечего — надо дописать: совет другой, значит и строка
|
|
30
|
+
// другая. До 2026-09-16 этот случай печатался как «прописан», и репозиторий, где AQK не
|
|
31
|
+
// вызывался ни разу, выглядел подключённым.
|
|
32
|
+
preCommitOther: "хук в .git/hooks есть, но AQK в нём не вызывается — проверьте .pre-commit-config.yaml либо само тело хука",
|
|
29
33
|
sessionOk: "хук SessionStart отдаёт состояние агенту",
|
|
30
34
|
sessionNo: (cmd) => `агент не получает состояние: ${cmd}`,
|
|
31
35
|
versionOk: (v) => `${v}, свежая`,
|
|
@@ -65,6 +69,7 @@ const ruDocs = {
|
|
|
65
69
|
"Прогон не делался — какие проверки красные, НЕИЗВЕСТНО. Это не «чисто»: `aqk doctor --run`.",
|
|
66
70
|
runStale: (when) =>
|
|
67
71
|
`Последний прогон ${when} СТАРЕЕ последнего коммита — он описывает не тот код, что здесь.`,
|
|
72
|
+
runCannot: (names) => `НЕ СМОГЛИ ПРОВЕРИТЬ (сбой самих проверок, а не находки о коде): ${names}`,
|
|
68
73
|
runClean: (when) => `Последний прогон ${when} — красных нет.`,
|
|
69
74
|
runRed: (when, names) => `Последний прогон ${when} — КРАСНЫЕ: ${names}.`,
|
|
70
75
|
andMore: (n) => `и ещё ${n}`,
|
|
@@ -86,7 +91,14 @@ const ruDocs = {
|
|
|
86
91
|
// complexity-limit её поймал.
|
|
87
92
|
nextStep: {
|
|
88
93
|
init: () => "Заведи стандарт: `aqk init` — без .aqk.yml `aqk add` откажет.",
|
|
89
|
-
adopt: (s) =>
|
|
94
|
+
adopt: (s) => {
|
|
95
|
+
const ok = s.gates.filter((g) => !g.weak);
|
|
96
|
+
const weak = s.gates.filter((g) => g.weak);
|
|
97
|
+
const out = [];
|
|
98
|
+
if (ok.length) out.push(`Объяви проверки, которые у проекта уже есть: в .aqk.yml, в gates: ${ok.map((g) => `${g.name}: "${g.cmd}"`).join(", ")}. Затем \`aqk doctor --run\`.`);
|
|
99
|
+
if (weak.length) out.push(`А эти в нынешнем виде покраснеть не могут — ${weak.map((g) => `${g.cmd} (${g.weak.text})`).join(", ")} — и объявлять их пока нельзя: скажи владельцу и почини команду.`);
|
|
100
|
+
return out.join(" ");
|
|
101
|
+
},
|
|
90
102
|
blind: (s) => `Брак «${s.slug}», подсаженный пробой в ${s.file}, ваши проверки НЕ поймали.` +
|
|
91
103
|
(s.command ? ` Поймать сейчас: \`${s.command}\`.` : "") + ` Держать всегда: \`aqk add ${s.slug}\`.`,
|
|
92
104
|
start: (s) => `Поставь ${s.slug}: \`aqk add ${s.slug}\`` + (s.command ? ` (одной строкой, без комплекта: \`${s.command}\`)` : "") +
|
|
@@ -96,7 +108,9 @@ const ruDocs = {
|
|
|
96
108
|
whenTitle: "Когда что делать:",
|
|
97
109
|
whenCommit: (hook) => hook === true
|
|
98
110
|
? "Перед коммитом → хук `.git/hooks/pre-commit` прогонит проверки сам; не обходи его (`--no-verify`)."
|
|
99
|
-
:
|
|
111
|
+
: hook === "other"
|
|
112
|
+
? "Перед коммитом → `aqk doctor --run --since main`: хук в `.git/hooks` стоит, но AQK из него не вызывается — сам собой прогон не случится."
|
|
113
|
+
: "Перед коммитом → `aqk doctor --run --since main`: хука перед коммитом нет, само это не случится.",
|
|
100
114
|
whenRules: [
|
|
101
115
|
"Добавил или поменял проверку → `aqk prove`: гейт обязан покраснеть на своём красном образце, иначе он не проверяет ничего.",
|
|
102
116
|
"Пишешь правило в свод → рядом метка арбитра `<!-- aqk: имя-гейта -->`; гейта нет — `<!-- aqk: человек -->`, и это признание, а не проверка.",
|
|
@@ -158,6 +172,7 @@ const ruDocs = {
|
|
|
158
172
|
whyTitle: "Зачем это нужно — коротко",
|
|
159
173
|
whyNothing: "нечего добавлять: применимое уже стоит",
|
|
160
174
|
saved: (path) => `Сохранено: ${path}`,
|
|
175
|
+
notSaved: (f, why) => `сохранить в ${f} не смогли (${why}) — отчёт выше, на диске его нет. Команда только рассказывает о состоянии, поэтому неудачная запись её не роняет.`,
|
|
161
176
|
docs: {
|
|
162
177
|
baseline: "обязательный минимум проекта, без привязки к языку",
|
|
163
178
|
readyMade: "карта готовых правил: сперва ищи готовое, потом пиши своё",
|
|
@@ -221,6 +236,7 @@ const ruDocs = {
|
|
|
221
236
|
],
|
|
222
237
|
report: {
|
|
223
238
|
skippedBySelect: "не запускался (по --only/--skip), состояние неизвестно",
|
|
239
|
+
notWritten: (f, why) => `отчёт прогона в ${f} записать не смогли (${why}) — гейты отработали, но следующая сессия и хук прочтут это как «прогона не было».`,
|
|
224
240
|
title: "aqk doctor --run",
|
|
225
241
|
version: "версия",
|
|
226
242
|
level: "уровень",
|
package/tool/i18n/ru-gates.mjs
CHANGED
|
@@ -187,6 +187,42 @@ export const ruGates = {
|
|
|
187
187
|
issue: "Нашёл баг или не подошло — заведи Issue, самая полезная обратная связь: и то и другое.",
|
|
188
188
|
notRemembered: "запомнить не удалось (дом недоступен для записи) — сообщение появится снова",
|
|
189
189
|
once: "Это разовое сообщение: больше не покажется на этой машине.",
|
|
190
|
+
|
|
191
|
+
// Просьба ПО ДЕЛУ: у неё всегда есть содержание, иначе её нет вовсе.
|
|
192
|
+
ask: {
|
|
193
|
+
cannot: (names, cmd) =>
|
|
194
|
+
`AQK не смог проверить: ${names}. Это самое ценное, что можно рассказать автору — ${cmd}`,
|
|
195
|
+
blind: (names, cmd) =>
|
|
196
|
+
`Проба нашла классы брака, которые здесь не ловит никто: ${names}. Совпало с вашим опытом? — ${cmd}`,
|
|
197
|
+
red: (names, cmd) =>
|
|
198
|
+
`Комплект поймал: ${names}. Одна строка автору — помогло или нет: ${cmd}`,
|
|
199
|
+
once: "Спрашиваем один раз на проект; выключить: AQK_FEEDBACK=0",
|
|
200
|
+
agent: "Скажи это человеку одной фразой и не настаивай, если он не ответил. Захочет отправить — добавь `--send`, сам флаг не добавляй.",
|
|
201
|
+
},
|
|
202
|
+
sending: "отправляю от вашего имени — вашей же учётной записью gh…",
|
|
203
|
+
sent: (url) => `отправлено: ${url}\n Это и есть вся плата за комплект. Спасибо.`,
|
|
204
|
+
sendFailed: {
|
|
205
|
+
"no-auth": "отправить не смогли: gh не установлен или вход не выполнен (`gh auth login`). Ниже — ссылка, она работает всегда.",
|
|
206
|
+
"no-thread": "отправить не смогли: обсуждение не найдено. Ниже — ссылка.",
|
|
207
|
+
failed: "отправить не смогли. Ниже — ссылка, она работает всегда.",
|
|
208
|
+
},
|
|
209
|
+
sendHow: (cmd) => `Отправить одной командой, своей учётной записью GitHub: ${cmd}`,
|
|
210
|
+
how: "Отправить — одно нажатие (откроется готовая задача, текст можно править):",
|
|
211
|
+
issueTitle: "Отзыв о работе комплекта",
|
|
212
|
+
nothingSent: "Ничего не отправлено: комплект не делает исходящих запросов, кроме проверки версии.",
|
|
213
|
+
orPaste: (cmd) => `Не хочется GitHub — перешлите текст выше как есть: он весь собран ${cmd}`,
|
|
214
|
+
report: {
|
|
215
|
+
title: "### Отзыв о комплекте",
|
|
216
|
+
unknown: "неизвестно",
|
|
217
|
+
none: "нет",
|
|
218
|
+
env: (v, node, os) => `версия: ${v} · node: ${node} · система: ${os}`,
|
|
219
|
+
level: (x) => `уровень: ${x}`,
|
|
220
|
+
stack: (x) => `стек: ${x}`,
|
|
221
|
+
gates: (n, red, cannot) => `гейтов объявлено: ${n} · красных: ${red} · не смогли проверить: ${cannot}`,
|
|
222
|
+
blind: (x) => `классы, которые здесь не ловит никто: ${x}`,
|
|
223
|
+
say: "Что сказать своими словами (одна строка — самое полезное во всём письме):",
|
|
224
|
+
mark: (v) => `<!-- собрано «aqk feedback» ${v}: без путей, без кода, без имени репозитория -->`,
|
|
225
|
+
},
|
|
190
226
|
},
|
|
191
227
|
|
|
192
228
|
note: {
|
package/tool/i18n/ru.mjs
CHANGED
|
@@ -53,6 +53,8 @@ export const ru = {
|
|
|
53
53
|
blob: "собрать методички в один файл GOD_AI.md",
|
|
54
54
|
learn: "кандидаты в правила из локальной переписки: сказано вслух и не записано",
|
|
55
55
|
context: "состояние проекта одним блоком — для контекста агента, а не для чтения",
|
|
56
|
+
feedback: "отчёт о работе комплекта и готовая ссылка — единственная плата за него",
|
|
57
|
+
feedbackSend: "отправить отзыв одной командой — вашей учётной записью gh; без флага не уходит ничего",
|
|
56
58
|
vitals: "подключено ли то, чем комплект работает: инструменты гейтов, хуки, свежесть версии",
|
|
57
59
|
prompt: "одно задание для агента: что починить, по порядку, и чем доказать, что готово",
|
|
58
60
|
contextInstall: "то же самое, но целиком — карта и свод правил — и хуком в контекст",
|
|
@@ -158,7 +160,16 @@ export const ru = {
|
|
|
158
160
|
startHook: "Прогнать руками — разовый героизм. Чтобы это случалось перед каждым пушем: pre-commit (репозиторий https://github.com/arsen-ask-lx/Agent_Quality_Kit, хуки aqk / aqk-doctor) либо обычный .git/hooks/pre-push.",
|
|
159
161
|
haveAlready: (n) => `Проверки, которые у вас УЖЕ ЕСТЬ (${n}) — прочитаны в ваших файлах, не выдуманы:`,
|
|
160
162
|
haveAlreadyHow: (line) => `объявите их — и держать будет машина, а не ваше внимание. В .aqk.yml, в gates: ${line}`,
|
|
161
|
-
|
|
163
|
+
weakOff: (text) => `не может провалиться: исход погашен прямо в скрипте — «${text}»`,
|
|
164
|
+
weakZero: () => "не может провалиться: --exit-zero велит инструменту выйти нулём, что бы он ни нашёл",
|
|
165
|
+
weakStub: (text) => `ничего не доказывает: всё тело скрипта — печать — «${text}»`,
|
|
166
|
+
weakHow: (n) => `из них ${n} покраснеть не могут. Объявить проверку, которая не может ` +
|
|
167
|
+
"провалиться, — значит сделать молчание машинно-читаемым. Сначала почините команду.",
|
|
168
|
+
// «Итого» — ПО КАТАЛОГУ AQK, и это сказано словом. Считаются наши записи: гейт проекта,
|
|
169
|
+
// названный по-своему (`project-verify`), ни одну из них по имени не закрывает, и строка
|
|
170
|
+
// «держит машина 0» у проекта с тридцатью собственными проверками читалась как приговор
|
|
171
|
+
// ему, а не как счёт наших записей. Замер 2026-09-16 по двенадцати репозиториям.
|
|
172
|
+
total: "Итого по каталогу AQK:",
|
|
162
173
|
totalHeld: (n) => `держит машина ${n}`,
|
|
163
174
|
totalTodo: (n) => `применимо но не поставлено ${n}`,
|
|
164
175
|
totalSkip: (n) => `скрыто ${n}`,
|
|
@@ -173,7 +184,14 @@ export const ru = {
|
|
|
173
184
|
`совещательные и красные: ${names.join(", ")}. Это выключенные проверки: ` +
|
|
174
185
|
`либо почини и убери из advisory, либо признай, что правила нет.`,
|
|
175
186
|
runHeading: "Прогон объявленных гейтов",
|
|
187
|
+
timeoutBadEnv: (raw, secs) => `AQK_GATE_TIMEOUT=«${raw}» — это не число секунд больше нуля; жду как прежде, ${secs}с. Ноль и мусор у spawnSync означают «ждать вечно», а висящий гейт неотличим от работающего.`,
|
|
176
188
|
timeout: "не уложился в 5 минут",
|
|
189
|
+
// «Не смогли проверить» — третье состояние, и оно обязано звучать иначе, чем находка:
|
|
190
|
+
// «код 2» человек читает как приговор коду, а это приговор запуску.
|
|
191
|
+
cannotCheck: (why) => `не смогли проверить: ${why}`,
|
|
192
|
+
whySpawn: (code) => `запустить не удалось${code ? ` (${code})` : ""}`,
|
|
193
|
+
whySignal: (sig) => `убит сигналом ${sig || "?"}`,
|
|
194
|
+
whyExit: (code) => `код ${code} — у этой команды это сбой, а не находка`,
|
|
177
195
|
running: (i, n) => `[${i}/${n}] идёт…`,
|
|
178
196
|
proving: "проверяю, что гейты ловят брак на своих образцах…",
|
|
179
197
|
exitCode: (code) => `код ${code}`,
|
|
@@ -303,7 +321,14 @@ export const ru = {
|
|
|
303
321
|
missed: ({ slug, file }, s) => `Гейт \`${slug}\` стоит, но пропустил брак, который проба подсадила в \`${file}\`. Разберись почему — частая причина: гейт не смотрит этот тип файлов или каталог; подробности даст \`${s} probe\`. Готово — \`${s} probe\` больше не называет этот класс.`,
|
|
304
322
|
red: (name, s) => `Гейт \`${name}\` красный. Запусти \`${s} doctor --run --only ${name}\`, прочитай находки и почини код. Готово — эта команда зелёная.`,
|
|
305
323
|
blind: ({ slug, file, command }, s) => `Проба подсадила брак класса \`${slug}\` в \`${file}\`, и проверки проекта его не заметили. Поставь проверку: \`${s} add ${slug}\`${command ? ` (то же одной строкой, без комплекта: \`${command}\`)` : ""}. Готово — \`${s} doctor --run --only ${slug}\` проходит, а \`${s} probe\` больше не называет этот класс.`,
|
|
306
|
-
adopt: (gates, s) =>
|
|
324
|
+
adopt: (gates, s) => {
|
|
325
|
+
const weak = gates.filter((g) => g.weak);
|
|
326
|
+
const ok = gates.filter((g) => !g.weak);
|
|
327
|
+
const out = [];
|
|
328
|
+
if (ok.length) out.push(`У проекта уже есть свои проверки: ${ok.map((g) => `\`${g.cmd}\` (${g.source})`).join(", ")}. Впиши их в gates: манифеста .aqk.yml — ${ok.map((g) => `\`${g.name}: "${g.cmd}"\``).join(", ")}. Готово — \`${s} doctor --run\` их запускает.`);
|
|
329
|
+
if (weak.length) out.push(`А эти в нынешнем виде покраснеть НЕ МОГУТ: ${weak.map((g) => `\`${g.cmd}\` — ${g.weak.text}`).join(", ")}. Не вписывай их, пока не починена команда: объявленная проверка, которая не может провалиться, превращает дыру в зелёную галочку, и с этого дня за неё ручается машина. Это решение владельца, а не твоё, — скажи ему, что нашёл.`);
|
|
330
|
+
return out.join(" ");
|
|
331
|
+
},
|
|
307
332
|
shim: {
|
|
308
333
|
missing: (s) => `Claude Code здесь настроен, а правила лежат в AGENTS.md — он читает только CLAUDE.md. Создай CLAUDE.md с одной строкой \`@AGENTS.md\`. Готово — \`${s} doctor\` об этом больше не предупреждает.`,
|
|
309
334
|
noImport: (s) => `CLAUDE.md не подключает AGENTS.md — Claude Code видит только CLAUDE.md. Добавь в CLAUDE.md строку \`@AGENTS.md\` (упоминание словами файл не загружает). Готово — \`${s} doctor\` об этом больше не предупреждает.`,
|
package/tool/lib/adopt.mjs
CHANGED
|
@@ -20,24 +20,77 @@
|
|
|
20
20
|
// ни другой. Берём только то, что отвечает «прошло или нет».
|
|
21
21
|
const CHECK_NAMES = new Set(["test", "tests", "lint", "typecheck", "type-check", "types", "check"]);
|
|
22
22
|
|
|
23
|
+
// ПРОВЕРКА, КОТОРАЯ ЕСТЬ И НЕ МОЖЕТ ПРОВАЛИТЬСЯ. До 2026-09-14 мы читали ИМЯ скрипта и печатали
|
|
24
|
+
// каноничную команду `npm test` с зелёной галочкой, ни разу не заглянув в его ТЕЛО. А выключатель
|
|
25
|
+
// стоит именно там: `node --test || true`. На репозитории, где выключено всё, первый экран
|
|
26
|
+
// говорил «у вас уже есть 2 проверки» — ровно та ошибка, ради которой написан весь комплект.
|
|
27
|
+
//
|
|
28
|
+
// ГРАНИЦА ВЗЯТА У ГЕЙТА `ci-actually-fails`, дословно его довод: `|| true` в СЕРЕДИНЕ команды —
|
|
29
|
+
// это идемпотентность вспомогательного шага (`mkdir -p … || true`), а не выключенная проверка.
|
|
30
|
+
// Поэтому красным делается только то, под что попадает ВЕСЬ исход: гашение в конце тела либо
|
|
31
|
+
// флаг, у которого другого назначения нет.
|
|
32
|
+
const OFF_TAIL = /(\|\|\s*(?:true|:|exit\s+0)|;\s*(?:true|exit\s+0))\s*$/;
|
|
33
|
+
const ZERO_FLAG = /(?:^|\s)--exit-zero(?:\s|$)/;
|
|
34
|
+
|
|
35
|
+
function cannotFail(body) {
|
|
36
|
+
if (typeof body !== "string") return undefined;
|
|
37
|
+
// Комментарий — не команда. `\s#` (а не просто `#`): в `echo "#1"` решётка стоит внутри строки.
|
|
38
|
+
const code = body.replace(/\s#[^"']*$/, "").trim();
|
|
39
|
+
if (!code) return undefined;
|
|
40
|
+
const off = OFF_TAIL.exec(code);
|
|
41
|
+
if (off) return { kind: "off", text: off[1].trim() };
|
|
42
|
+
if (ZERO_FLAG.test(code)) return { kind: "zero", text: "--exit-zero" };
|
|
43
|
+
// Заглушка: всё тело — печать и ничего больше. Заготовка npm
|
|
44
|
+
// (`echo "Error: no test specified" && exit 1`) провалиться МОЖЕТ — её обвинять нельзя,
|
|
45
|
+
// и её отсекает связка `&&`.
|
|
46
|
+
if (/^echo\b/.test(code) && !/[&|;]/.test(code)) return { kind: "stub", text: code.slice(0, 60) };
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
23
50
|
// Окружения tox, которые судят, а не гоняют тесты под матрицей версий. Голый `tox` не
|
|
24
51
|
// предлагаем: он проходит все интерпретаторы из envlist, и у человека без пяти питонов это
|
|
25
52
|
// красный прогон на пустом месте. Имена сняты с живых tox-файлов (rich, click, flask).
|
|
26
53
|
const TOX_CHECKS = new Set(["lint", "style", "typing", "types", "type", "mypy", "check", "typecheck"]);
|
|
27
54
|
|
|
55
|
+
// ОДНА И ТА ЖЕ КОМАНДА, ЗАПИСАННАЯ ПО-РАЗНОМУ. Сравнивать надо КОМАНДЫ, а не имена: гейт в
|
|
56
|
+
// чужом манифесте зовётся как хочет его владелец (`project-verify`), и по именам совпадения не
|
|
57
|
+
// будет никогда. Обёртка снимается: `bash scripts/check` и `scripts/check` — это одно, и
|
|
58
|
+
// советовать второе тому, у кого объявлено первое, значит советовать уже сделанное.
|
|
59
|
+
//
|
|
60
|
+
// Вхождение подстрокой здесь НЕ годится, хотя и соблазняет: `true` содержится в половине
|
|
61
|
+
// команд, и любой гейт-заглушка съел бы весь список. Равенство после нормализации ошибается
|
|
62
|
+
// в одну сторону — покажет лишнее, — и это дешевле молчания.
|
|
63
|
+
const WRAPPER = /^(?:bash|sh)\s+/;
|
|
64
|
+
|
|
65
|
+
function sameCommand(a, b) {
|
|
66
|
+
const norm = (s) => String(s || "").trim().replace(/\s+/g, " ");
|
|
67
|
+
const bare = (s) => norm(s).replace(WRAPPER, "");
|
|
68
|
+
return (norm(a) && norm(a) === norm(b)) || (bare(a) && bare(a) === bare(b));
|
|
69
|
+
}
|
|
70
|
+
|
|
28
71
|
// Источники в порядке доверия: при совпадении имени берётся первый. Замер 2026-09-11 на
|
|
29
72
|
// восьми python-проектах: package.json нет ни у кого, Makefile у трёх, `.pre-commit-config.yaml`
|
|
30
73
|
// у семи, tox у пяти, scripts/ у httpx. Шаги конвейера НЕ читаются: там `${{ matrix.x }}`,
|
|
31
74
|
// `PYTHONPATH=…` и обёртки `uv run --locked --group …` — строка, которая работает только в
|
|
32
75
|
// том конвейере, предложенная как локальный гейт, покраснела бы у человека в первую же минуту.
|
|
33
|
-
|
|
76
|
+
//
|
|
77
|
+
// `declared` — команды, УЖЕ объявленные в манифесте. Приходят списком строк, а не манифестом:
|
|
78
|
+
// иначе чтение чужих конфигов начало бы зависеть от нашего формата, и файл, заведённый ради
|
|
79
|
+
// разбора ЧУЖИХ источников, получил бы вторую причину меняться.
|
|
80
|
+
function proposeGates(files = {}, declared = []) {
|
|
34
81
|
const out = [];
|
|
35
82
|
const seen = new Set();
|
|
36
|
-
const
|
|
83
|
+
const already = (cmd) => (declared || []).some((d) => sameCommand(d, cmd));
|
|
84
|
+
// ТЕЛО передаётся отдельно от команды: человеку показывается каноничная `npm test`, а судим
|
|
85
|
+
// мы по тому, что за ней стоит. Где тела у нас нет (Makefile, tox, scripts/) — там и суждения
|
|
86
|
+
// нет: молчание тут честнее догадки.
|
|
87
|
+
const push = (name, cmd, source, body) => {
|
|
37
88
|
const key = name === "tests" ? "test" : name.replace(/^type-?check$|^types$/, "typecheck");
|
|
38
89
|
if (seen.has(key)) return;
|
|
39
90
|
seen.add(key);
|
|
40
|
-
|
|
91
|
+
if (already(cmd)) return;
|
|
92
|
+
const weak = cannotFail(body);
|
|
93
|
+
out.push(weak ? { name: key, cmd, source, weak } : { name: key, cmd, source });
|
|
41
94
|
};
|
|
42
95
|
|
|
43
96
|
const pkg = files["package.json"];
|
|
@@ -48,7 +101,8 @@ function proposeGates(files = {}) {
|
|
|
48
101
|
for (const name of Object.keys(scripts)) {
|
|
49
102
|
if (!CHECK_NAMES.has(name)) continue;
|
|
50
103
|
// `npm test` — каноничное написание для теста, остальное через `run`.
|
|
51
|
-
push(name, name === "test" || name === "tests" ? "npm test" : `npm run ${name}`,
|
|
104
|
+
push(name, name === "test" || name === "tests" ? "npm test" : `npm run ${name}`,
|
|
105
|
+
"package.json", scripts[name]);
|
|
52
106
|
}
|
|
53
107
|
}
|
|
54
108
|
}
|
package/tool/lib/ask.mjs
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// tool/lib/ask.mjs — КОГДА КОМПЛЕКТ ОБРАЩАЕТСЯ К ЧЕЛОВЕКУ, и как он помнит, что уже обращался.
|
|
2
|
+
//
|
|
3
|
+
// ЗАЧЕМ ОТДЕЛЬНЫЙ МОДУЛЬ. Знание «мы это показывали» жило в комплекте дважды и по-разному:
|
|
4
|
+
// · `~/.config/aqk/feedback-shown` — файл-флаг, раз на машину: заведён в `core.mjs`,
|
|
5
|
+
// прочитан в `project.mjs`;
|
|
6
|
+
// · `.aqk/advice-shown`, `.aqk/update-checked` — отметка временем плюс `adviceDue()` на
|
|
7
|
+
// сутки: заведены и прочитаны в `brief.mjs`.
|
|
8
|
+
// Одно решение — «не долби человека» — в двух местах и в двух форматах. Третье обращение
|
|
9
|
+
// завело бы третий формат, и дальше они расходятся молча.
|
|
10
|
+
//
|
|
11
|
+
// ЧТО ЗДЕСЬ ЕСТЬ И ЧЕГО НЕТ. Здесь только ОГРАНИЧИТЕЛЬ: можно ли сейчас обратиться и чем это
|
|
12
|
+
// запомнить. Чего здесь нет — текста обращения и решения, есть ли о чём говорить: текст живёт
|
|
13
|
+
// в каталогах строк, решение — у того, кто знает состояние проекта. Иначе модуль про «когда»
|
|
14
|
+
// начал бы меняться вместе с каждой правкой формулировки.
|
|
15
|
+
//
|
|
16
|
+
// ЛИСТ ДЕРЕВА: импортируются только встроенные модули Node. Каталоги передаются вызывающим, а
|
|
17
|
+
// не берутся из `core.mjs`, — иначе получилось бы кольцо: `core.mjs` берёт отсюда список
|
|
18
|
+
// служебных файлов для `.gitignore`.
|
|
19
|
+
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
|
|
22
|
+
const DAY = 24 * 60 * 60 * 1000;
|
|
23
|
+
|
|
24
|
+
// ТАБЛИЦА ВИДОВ. Вид объявляет две вещи, и обе нельзя угадать по имени:
|
|
25
|
+
// where — «project» (отметка про ЭТОТ репозиторий, лежит в его служебном каталоге) или
|
|
26
|
+
// «home» (отметка про ЭТУ МАШИНУ, лежит в доме пользователя);
|
|
27
|
+
// every — через сколько можно повторить; `null` означает «никогда», а не «очень нескоро».
|
|
28
|
+
//
|
|
29
|
+
// Имена файлов — прежние, до единой буквы: человек, у которого отметка уже лежит, не должен
|
|
30
|
+
// получить обращение заново только потому, что мы переставили код.
|
|
31
|
+
const ASKS = {
|
|
32
|
+
// Совет про непоставленную запись каталога — в короткой строке хука.
|
|
33
|
+
advice: { where: "project", file: "advice-shown", every: DAY },
|
|
34
|
+
// Проверка свежести версии — единственный исходящий запрос комплекта.
|
|
35
|
+
update: { where: "project", file: "update-checked", every: DAY },
|
|
36
|
+
// Просьба об отзыве после установки — раз на машину: второй `init` в другом репозитории на
|
|
37
|
+
// том же компьютере её не повторяет.
|
|
38
|
+
install: { where: "home", file: "feedback-shown", every: null },
|
|
39
|
+
// Просьба об отзыве ПО ДЕЛУ — раз на проект. Объявлена здесь до первого использования
|
|
40
|
+
// намеренно: строка в `.gitignore` обязана появиться РАНЬШЕ, чем файл будет записан, иначе
|
|
41
|
+
// отметка уедет в чужой коммит у всех, кто поставил комплект между двумя выпусками.
|
|
42
|
+
value: { where: "project", file: "feedback-asked", every: null },
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Опечатка в виде обращения обязана падать. Оба молчаливых умолчания неверны в половине
|
|
46
|
+
// случаев: «показывать всегда» превращает ограничитель в шум, «не показывать» — выключает
|
|
47
|
+
// обращение навсегда, и никто об этом не узнает.
|
|
48
|
+
function askKind(kind) {
|
|
49
|
+
const a = ASKS[kind];
|
|
50
|
+
if (!a) throw new Error(`неизвестный вид обращения: ${kind}. Известны: ${Object.keys(ASKS).join(", ")}`);
|
|
51
|
+
return a;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Служебные файлы ЭТОГО проекта — для `.gitignore`. Домашние сюда не идут: их git не видит.
|
|
55
|
+
const ASK_FILES = Object.values(ASKS).filter((a) => a.where === "project").map((a) => a.file);
|
|
56
|
+
|
|
57
|
+
// `project` — служебный каталог репозитория (тот самый `.aqk`), `home` — дом пользователя.
|
|
58
|
+
// Каталог передаётся целиком, а не собирается здесь: имя `.aqk` знает `core.mjs`, и второй
|
|
59
|
+
// его экземпляр однажды разошёлся бы с первым.
|
|
60
|
+
function askFile(kind, { project, home } = {}) {
|
|
61
|
+
const a = askKind(kind);
|
|
62
|
+
return a.where === "home" ? join(String(home ?? ""), ".config", "aqk", a.file) : join(String(project ?? ""), a.file);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Пора ли обращаться. `stamp` — содержимое отметки или null, если её нет.
|
|
66
|
+
//
|
|
67
|
+
// Разовое обращение: отметка есть — значит было, и повторить нельзя никогда, что бы в ней ни
|
|
68
|
+
// лежало. Старый формат хранил слово «shown», а не дату: читать его как испорченную дату и
|
|
69
|
+
// показывать заново значило бы повторить просьбу у всех, кто поставил комплект раньше.
|
|
70
|
+
//
|
|
71
|
+
// Суточное: «не знаем, когда показывали» и «показывали давно» — одно и то же решение, показать.
|
|
72
|
+
// Молчать из-за нечитаемого файла состояния значит потерять обращение навсегда и не сказать
|
|
73
|
+
// почему.
|
|
74
|
+
function askDue(kind, stamp, now = Date.now()) {
|
|
75
|
+
const a = askKind(kind);
|
|
76
|
+
if (stamp === null || stamp === undefined || String(stamp).trim() === "") return true;
|
|
77
|
+
if (a.every === null) return false;
|
|
78
|
+
const t = Date.parse(String(stamp).trim());
|
|
79
|
+
if (!Number.isFinite(t)) return true;
|
|
80
|
+
return now - t >= a.every;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Отметка с диска: строка или null. Файла нет, каталог не читается, прав не хватило — всё это
|
|
84
|
+
// «не показывали»: потерять обращение из-за нечитаемой отметки дешевле, чем молчать.
|
|
85
|
+
async function readStamp(kind, dirs) {
|
|
86
|
+
try {
|
|
87
|
+
return (await readFile(askFile(kind, dirs), "utf8")).trim();
|
|
88
|
+
} catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Запомнить обращение. Возвращает true, если записали, — СЛОВО О НЕУДАЧЕ ГОВОРИТ ВЫЗЫВАЮЩИЙ:
|
|
94
|
+
// у совета повтор это мелочь, а у разовой просьбы — та же просьба завтра, и человеку надо
|
|
95
|
+
// сказать, почему она вернулась. Дом бывает недоступен для записи: в контейнере, запущенном
|
|
96
|
+
// `--user 1001:127`, у этого uid нет записи в /etc/passwd, `homedir()` даёт «/», и запись
|
|
97
|
+
// падает на `/.config`. До 2026-09-09 это роняло весь `init` — то есть любого, кто набрал
|
|
98
|
+
// команду из нашей же документации по docker.
|
|
99
|
+
async function markAsked(kind, dirs, now = new Date()) {
|
|
100
|
+
const path = askFile(kind, dirs);
|
|
101
|
+
try {
|
|
102
|
+
await mkdir(join(path, ".."), { recursive: true });
|
|
103
|
+
await writeFile(path, `${now.toISOString()}\n`, "utf8");
|
|
104
|
+
return true;
|
|
105
|
+
} catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Пора ли обращаться, с чтением отметки. Две трети вызывающих хотят именно этого; чистый
|
|
111
|
+
// `askDue` остаётся для перебора случаев, которых на диске не бывает.
|
|
112
|
+
async function askAllowed(kind, dirs, now = Date.now()) {
|
|
113
|
+
return askDue(kind, await readStamp(kind, dirs), now);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Наружу — то, что зовут снаружи. `readStamp` внутренний: снаружи спрашивают «пора ли»,
|
|
117
|
+
// а не «что лежит в файле», и лишний экспорт читается как часть договора.
|
|
118
|
+
export { ASKS, ASK_FILES, askDue, askFile, markAsked, askAllowed };
|
package/tool/lib/brief.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { readFile
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { PKG_ROOT, SELF, c, stateDirs } from "./core.mjs";
|
|
4
|
+
import { askAllowed, markAsked } from "./ask.mjs";
|
|
4
5
|
import { L } from "../i18n/index.mjs";
|
|
5
6
|
import { canDrawArt } from "./banner.mjs";
|
|
6
7
|
// tool/lib/brief.mjs — короткая строка присутствия для прогона в хуке.
|
|
@@ -19,10 +20,6 @@ import { canDrawArt } from "./banner.mjs";
|
|
|
19
20
|
// то, что видишь тридцатый раз, перестаёт читаться — и пролистывается вместе с настоящими
|
|
20
21
|
// находками, стоящими рядом.
|
|
21
22
|
|
|
22
|
-
// Сутки. Не «раз в прогон» и не «раз в неделю»: за сутки человек успевает забыть, но не успевает
|
|
23
|
-
// устать. Число здесь спорное — важно, что ограничитель есть и он машинный.
|
|
24
|
-
const ADVICE_EVERY_MS = 24 * 60 * 60 * 1000;
|
|
25
|
-
|
|
26
23
|
// ЗНАЧОК ПРИСУТСТВИЯ — здесь, а не в каталогах строк. Символ один на оба языка, и держать его
|
|
27
24
|
// в двух местах значит однажды получить разные значки в ru и en: то же правило, по которому у
|
|
28
25
|
// нас один свод правил на две точки входа. Выбран владельцем из пятидесяти семи вариантов.
|
|
@@ -41,16 +38,6 @@ function briefLine(state, L, env = process.env) {
|
|
|
41
38
|
return `${head}\n${t.red(state.red.join(", "))}`;
|
|
42
39
|
}
|
|
43
40
|
|
|
44
|
-
// «Не знаем, когда показывали» и «показывали давно» — одно и то же решение: показать.
|
|
45
|
-
// Испорченная отметка попадает сюда же намеренно: молчать из-за нечитаемого файла состояния
|
|
46
|
-
// значит потерять совет навсегда и не сказать почему.
|
|
47
|
-
function adviceDue(lastIso, now = Date.now()) {
|
|
48
|
-
if (!lastIso) return true;
|
|
49
|
-
const t = Date.parse(String(lastIso));
|
|
50
|
-
if (!Number.isFinite(t)) return true;
|
|
51
|
-
return now - t >= ADVICE_EVERY_MS;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
41
|
// Первая из непоставленных, а не «самая важная»: важность мы не считаем, а порядок каталога
|
|
55
42
|
// осмыслен — записи в нём лежат от общего к частному. Выдавать порядок за приоритет нельзя.
|
|
56
43
|
function pickAdvice(todo = []) {
|
|
@@ -112,9 +99,7 @@ function beginBrief() {
|
|
|
112
99
|
|
|
113
100
|
// Печать краткого итога. Совет — не чаще раза в сутки и с явным способом отказаться: то, что
|
|
114
101
|
// видишь тридцатый раз, перестаёт читаться и пролистывается вместе с настоящими находками рядом.
|
|
115
|
-
//
|
|
116
|
-
// это состояние машины, а не проекта. Раньше здесь было написано «.aqk/ в .gitignore» — а
|
|
117
|
-
// `init` туда ничего не клал, и на живом проекте служебный файл уехал в коммит.
|
|
102
|
+
// Сам ограничитель и отметка — в `ask.mjs`, общие на все обращения комплекта к человеку.
|
|
118
103
|
async function finishBrief(buf, state, todoRecs, ok) {
|
|
119
104
|
if (!buf) return;
|
|
120
105
|
buf.restore();
|
|
@@ -129,18 +114,16 @@ async function finishBrief(buf, state, todoRecs, ok) {
|
|
|
129
114
|
if (!ok) { console.log(buf.lines.join("\n")); return; }
|
|
130
115
|
|
|
131
116
|
if (process.env.AQK_ADVICE === "0" || !state.todo) return;
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (!
|
|
117
|
+
// Ограничитель — общий на все обращения комплекта к человеку (ask.mjs): совет, проверка
|
|
118
|
+
// версии и просьба об отзыве считают «уже показывали» одним кодом и одним форматом.
|
|
119
|
+
const dirs = stateDirs();
|
|
120
|
+
if (!(await askAllowed("advice", dirs))) return;
|
|
136
121
|
const advice = pickAdvice(todoRecs);
|
|
137
122
|
if (!advice) return;
|
|
138
123
|
console.log(c.dim(L.brief.advise(advice.slug, advice.intent || "")));
|
|
139
124
|
console.log(c.dim(L.brief.adviseOff(`${SELF} why ${advice.slug}`, "AQK_ADVICE=0")));
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
await writeFile(stampFile, new Date().toISOString(), "utf8");
|
|
143
|
-
} catch { /* не смогли записать отметку — совет повторится, это не беда */ }
|
|
125
|
+
// Не записалось — совет повторится завтра, и это не беда: молчать об этом человеку незачем.
|
|
126
|
+
await markAsked("advice", dirs);
|
|
144
127
|
}
|
|
145
128
|
|
|
146
129
|
// Спрашивает реестр npm о своей версии. РАЗ В СУТКИ, НЕ В КОНВЕЙЕРЕ, С ТАЙМАУТОМ, И МОЛЧА
|
|
@@ -152,10 +135,8 @@ async function finishBrief(buf, state, todoRecs, ok) {
|
|
|
152
135
|
// и вместе с ним всё остальное, что печатает эта строка.
|
|
153
136
|
async function maybeUpdateNotice() {
|
|
154
137
|
if (!updateWanted()) return;
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
try { last = (await readFile(stamp, "utf8")).trim(); } catch { /* ещё не спрашивали */ }
|
|
158
|
-
if (!adviceDue(last)) return;
|
|
138
|
+
const dirs = stateDirs();
|
|
139
|
+
if (!(await askAllowed("update", dirs))) return;
|
|
159
140
|
|
|
160
141
|
let current = "";
|
|
161
142
|
try { current = JSON.parse(await readFile(join(PKG_ROOT, "package.json"), "utf8")).version || ""; } catch { return; }
|
|
@@ -163,10 +144,7 @@ async function maybeUpdateNotice() {
|
|
|
163
144
|
// ОТМЕТКА СТАВИТСЯ ДО ЗАПРОСА, а не после удачного ответа. Сперва было наоборот, и замер
|
|
164
145
|
// показал цену: человек без сети платил бы ожиданием на КАЖДОМ коммите, а не раз в сутки.
|
|
165
146
|
// Из двух ошибок выбрана дешёвая: пропущенное за день уведомление против ежедневного стопора.
|
|
166
|
-
|
|
167
|
-
await mkdir(join(CWD, TARGET_DIR), { recursive: true });
|
|
168
|
-
await writeFile(stamp, new Date().toISOString(), "utf8");
|
|
169
|
-
} catch { /* не смогли записать — спросим ещё раз, это не беда */ }
|
|
147
|
+
await markAsked("update", dirs);
|
|
170
148
|
|
|
171
149
|
let latest = "";
|
|
172
150
|
try {
|
|
@@ -189,6 +167,7 @@ async function maybeUpdateNotice() {
|
|
|
189
167
|
if (notice) console.log(c.dim(notice));
|
|
190
168
|
}
|
|
191
169
|
|
|
192
|
-
// Наружу — только то, что зовут снаружи. `cmpVer`
|
|
193
|
-
//
|
|
194
|
-
|
|
170
|
+
// Наружу — только то, что зовут снаружи. `cmpVer` внутренний: экспорт, который никто не
|
|
171
|
+
// импортирует, читается как часть договора и мешает менять внутренности. Ограничитель обращений
|
|
172
|
+
// уехал целиком в `ask.mjs` — вместе с проверками, которые его сторожили.
|
|
173
|
+
export { briefLine, pickAdvice, updateNotice, updateWanted, beginBrief, finishBrief };
|
package/tool/lib/core.mjs
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// размер файла. Зависимостей по-прежнему нет ни одной: только встроенные модули Node.
|
|
6
6
|
|
|
7
7
|
import { LANG } from "../i18n/index.mjs";
|
|
8
|
+
import { ASK_FILES } from "./ask.mjs";
|
|
8
9
|
import { access, readdir, mkdir, copyFile, writeFile } from "node:fs/promises";
|
|
9
10
|
import { constants } from "node:fs";
|
|
10
11
|
import { fileURLToPath } from "node:url";
|
|
@@ -39,8 +40,16 @@ const exists = async (p) => access(p, constants.F_OK).then(() => true, () => fal
|
|
|
39
40
|
// Как звать программу — зависит от того, как её запустили. Через npx команды `aqk` в системе
|
|
40
41
|
// нет: подсказка «aqk doctor» отправляет человека в «команда не найдена» на первом же шаге.
|
|
41
42
|
// Печатаем то, что можно скопировать и выполнить прямо сейчас.
|
|
42
|
-
// Имя в реестре, а не адрес репозитория: короче,
|
|
43
|
-
//
|
|
43
|
+
// Имя в реестре, а не адрес репозитория: короче, тянет пакет вместо клона всего репозитория и
|
|
44
|
+
// не заставляет человека ждать три минуты в тишине на первой же команде.
|
|
45
|
+
//
|
|
46
|
+
// ЧИСЛА ЗДЕСЬ БОЛЬШЕ НЕТ, И ЭТО РЕШЕНИЕ. Стояло «230 КБ»; замер 2026-09-10 дал 574 КБ, замер
|
|
47
|
+
// 2026-09-14 — 692 КБ. Число, которое никто не сторожит, врёт тем сильнее, чем дольше живёт, —
|
|
48
|
+
// наше же правило про утверждение без арбитра, нарушенное в собственном комментарии. Текущий
|
|
49
|
+
// размер считается одной командой: `npm pack --dry-run`. Из 2,2 МБ распакованного 584 КБ — это
|
|
50
|
+
// `tool/selfcheck`, то есть наши собственные тесты, уезжающие каждому пользователю; вырезать их
|
|
51
|
+
// из `files` можно только после проверки, что собранный пакет работает, — иначе поломка
|
|
52
|
+
// установки обойдётся дороже сэкономленного.
|
|
44
53
|
const REPO = "agent-quality-kit";
|
|
45
54
|
// Адрес репозитория отдельно от имени пакета. Когда имя стало коротким, ссылка «поставь
|
|
46
55
|
// звезду» собиралась из него и вела на github.com/agent-quality-kit — несуществующую
|
|
@@ -86,6 +95,8 @@ function commandRows(L) {
|
|
|
86
95
|
{ name: "prompt", args: "", text: h.prompt },
|
|
87
96
|
{ name: "badge", args: "", text: h.badge },
|
|
88
97
|
{ name: "vitals", args: "", text: h.vitals },
|
|
98
|
+
{ name: "feedback", args: "", text: h.feedback },
|
|
99
|
+
{ name: "feedback", args: "--send", text: h.feedbackSend },
|
|
89
100
|
{ name: "version", args: "", text: h.version },
|
|
90
101
|
];
|
|
91
102
|
}
|
|
@@ -105,10 +116,9 @@ const RATCHET_DIR = "ratchets";
|
|
|
105
116
|
// обёртка плюс реестр, и разносить их по разным каталогам значит прятать половину механизма.
|
|
106
117
|
const RATCHET_LIB = `${RATCHET_DIR}/_ratchet.sh`;
|
|
107
118
|
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
const FEEDBACK_MARK = join(homedir(), ".config", "aqk", "feedback-shown");
|
|
119
|
+
// Служебный каталог этого проекта и дом пользователя — два места, где комплект держит
|
|
120
|
+
// состояние. Кто и как часто туда пишет, решает `ask.mjs`; здесь только адреса.
|
|
121
|
+
const stateDirs = () => ({ project: join(CWD, TARGET_DIR), home: homedir() });
|
|
112
122
|
|
|
113
123
|
// Путь, попадающий в ДОКУМЕНТ, всегда пишется через «/». `relative()` отдаёт разделитель
|
|
114
124
|
// платформы, и на Windows склейка методичек и отчёт получались с «kit\\docs» вместо «kit/docs»:
|
|
@@ -148,7 +158,7 @@ async function writeIfAbsent(path, content, { force }) {
|
|
|
148
158
|
// читателей. Отзыв с живого проекта 2026-09-11: `.aqk/last-run.md` однажды закоммитили, и каждый
|
|
149
159
|
// `make check` оставлял изменённый файл. Целиком `.aqk/` не игнорируется: методички и правила в
|
|
150
160
|
// нём — содержимое проекта.
|
|
151
|
-
const RUNTIME_FILES = ["last-run.md", "last-probe.md",
|
|
161
|
+
const RUNTIME_FILES = ["last-run.md", "last-probe.md", ...ASK_FILES];
|
|
152
162
|
const L_IGNORE_NOTE = LANG === "en"
|
|
153
163
|
? "# aqk: this machine's state — rewritten by every run, it does not belong in git"
|
|
154
164
|
: "# aqk: состояние этой машины — переписывается каждым прогоном, в git ему не место";
|
|
@@ -170,20 +180,47 @@ async function ensureIgnored(cwd = CWD) {
|
|
|
170
180
|
}
|
|
171
181
|
|
|
172
182
|
// Стоит ли хук pre-commit НА САМОМ ДЕЛЕ — в `.git/hooks`, а не в `.pre-commit-config.yaml`:
|
|
173
|
-
// запись в конфиге — намерение, сработает только то, что лежит в гите.
|
|
174
|
-
//
|
|
175
|
-
//
|
|
183
|
+
// запись в конфиге — намерение, сработает только то, что лежит в гите.
|
|
184
|
+
//
|
|
185
|
+
// ЧЕТЫРЕ ОТВЕТА, А НЕ ТРИ. Раньше тело хука проверялось регуляркой `/pre-commit|aqk/i`, то есть
|
|
186
|
+
// ПО ИМЕНИ: у любого проекта с фреймворком pre-commit слово «pre-commit» в хуке есть всегда, и
|
|
187
|
+
// `vitals` печатал «прописан в .git/hooks» репозиторию, где AQK не вызывался ни разу. Разбор
|
|
188
|
+
// живой интеграции 2026-09-16: человек прочитал это как «обвязка на месте» и ушёл. Наш
|
|
189
|
+
// собственный класс «объявлено ≠ работает», у нас самих.
|
|
190
|
+
//
|
|
191
|
+
// null — не git либо файл не прочитать («не знаем» не сливается с «нет»);
|
|
192
|
+
// false — хука нет;
|
|
193
|
+
// "other" — хук есть, но AQK в нём не участвует: ставить не надо, надо дописать;
|
|
194
|
+
// true — AQK участвует.
|
|
195
|
+
//
|
|
196
|
+
// КАК ЭТО ДЕЛАЮТ СНАРУЖИ. pre-commit узнаёт свой хук функцией `is_our_script()` — ищет в теле
|
|
197
|
+
// собственный маркер (CURRENT_HASH плюс пять PRIOR_HASHES), а не имя. У них же есть режим
|
|
198
|
+
// миграции: при установке поверх чужого хука запускаются оба, старый уезжает в `.legacy`. То
|
|
199
|
+
// есть «в теле есть слово pre-commit» не доказывает даже, что хук ихний.
|
|
200
|
+
//
|
|
201
|
+
// ДВА ПУТИ ПОДКЛЮЧЕНИЯ, И СЧИТАТЬ НАДО ОБА. Прямой вызов в теле хука — и наш хук из
|
|
202
|
+
// `.pre-commit-hooks.yaml`, поставленный через фреймворк: там в `.git/hooks/pre-commit` лежит
|
|
203
|
+
// диспетчер, а что он запустит, написано в `.pre-commit-config.yaml` проекта. Смотреть только в
|
|
204
|
+
// тело значило бы соврать в обратную сторону — сказать «AQK не подключён» тому, кто подключил.
|
|
205
|
+
const AQK_IN_HOOK = /\baqk\b|agent[-_]quality[-_]kit/i;
|
|
206
|
+
const AQK_IN_CONFIG = /agent[-_]quality[-_]kit|Agent_Quality_Kit|(?:^|\s)-\s*id:\s*["']?aqk\b/im;
|
|
207
|
+
|
|
176
208
|
async function preCommitHook(cwd = CWD) {
|
|
177
209
|
const { readFile } = await import("node:fs/promises");
|
|
178
210
|
if (!(await exists(join(cwd, ".git")))) return null;
|
|
179
211
|
const hook = join(cwd, ".git", "hooks", "pre-commit");
|
|
180
212
|
if (!(await exists(hook))) return false;
|
|
181
|
-
|
|
213
|
+
let body = "";
|
|
214
|
+
try { body = await readFile(hook, "utf8"); } catch { return null; }
|
|
215
|
+
if (AQK_IN_HOOK.test(body)) return true;
|
|
216
|
+
let config = "";
|
|
217
|
+
try { config = await readFile(join(cwd, ".pre-commit-config.yaml"), "utf8"); } catch { /* нет конфига — значит подключения через фреймворк нет */ }
|
|
218
|
+
return AQK_IN_CONFIG.test(config) ? true : "other";
|
|
182
219
|
}
|
|
183
220
|
|
|
184
221
|
export {
|
|
185
222
|
copyDir, writeIfAbsent,
|
|
186
223
|
PKG_ROOT, CWD, DOCS_SRC, RULES_SRC, TARGET_DIR, docPath,
|
|
187
224
|
MANIFEST, GATES_SRC, PROJECT_GATES, RATCHET_DIR, RATCHET_LIB,
|
|
188
|
-
SELF, REPO_URL, c, exists, die,
|
|
225
|
+
SELF, REPO_URL, c, exists, die, stateDirs, commandRows, preCommitHook, RUNTIME_FILES, ensureIgnored,
|
|
189
226
|
};
|