agent-quality-kit 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +28 -4
  2. package/README.ru.md +28 -4
  3. package/kit/gates/complexity-limit/gate.yml +9 -0
  4. package/kit/gates/dead-code/gate.yml +9 -0
  5. package/kit/gates/duplicate-code/gate.yml +5 -0
  6. package/kit/gates/env-secrets-not-committed/README.md +73 -0
  7. package/kit/gates/env-secrets-not-committed/check.sh +139 -0
  8. package/kit/gates/env-secrets-not-committed/gate.yml +21 -0
  9. package/kit/gates/env-secrets-not-committed/green/.aqk-tracked +10 -0
  10. package/kit/gates/env-secrets-not-committed/green/.env +10 -0
  11. package/kit/gates/env-secrets-not-committed/green/.env.production +5 -0
  12. package/kit/gates/env-secrets-not-committed/green/.env.test +2 -0
  13. package/kit/gates/env-secrets-not-committed/red/.aqk-tracked +5 -0
  14. package/kit/gates/env-secrets-not-committed/red/.env +7 -0
  15. package/kit/gates/no-print-in-prod/gate.yml +9 -0
  16. package/kit/gates/secrets-not-in-code/gate.yml +20 -0
  17. package/kit/gates/swallowed-error/gate.yml +9 -0
  18. package/kit/gates/todo-without-task/gate.yml +9 -0
  19. package/llms.txt +4 -2
  20. package/package.json +1 -1
  21. package/tool/commands/badge.mjs +1 -1
  22. package/tool/commands/context.mjs +59 -4
  23. package/tool/commands/doctor.mjs +171 -44
  24. package/tool/commands/gates.mjs +3 -2
  25. package/tool/commands/probe.mjs +86 -22
  26. package/tool/commands/project.mjs +6 -1
  27. package/tool/commands/report.mjs +1 -1
  28. package/tool/commands/vitals.mjs +21 -12
  29. package/tool/i18n/en-docs.mjs +24 -2
  30. package/tool/i18n/en-gates.mjs +10 -1
  31. package/tool/i18n/en.mjs +25 -0
  32. package/tool/i18n/ru-docs.mjs +26 -2
  33. package/tool/i18n/ru-gates.mjs +10 -1
  34. package/tool/i18n/ru.mjs +25 -0
  35. package/tool/lib/adopt.mjs +112 -0
  36. package/tool/lib/advice.mjs +115 -0
  37. package/tool/lib/brief.mjs +3 -1
  38. package/tool/lib/cadence.mjs +46 -1
  39. package/tool/lib/core.mjs +39 -1
  40. package/tool/lib/execution.mjs +50 -1
  41. package/tool/lib/gate-worker.mjs +18 -0
  42. package/tool/lib/history.mjs +45 -6
  43. package/tool/lib/manifest.mjs +65 -21
  44. package/tool/lib/prove.mjs +2 -2
  45. package/tool/lib/repo.mjs +18 -0
  46. package/tool/lib/run.mjs +108 -8
  47. package/tool/selfcheck/gates.sh +12 -2
  48. package/tool/selfcheck/smoke/first-run.test.mjs +105 -0
  49. package/tool/selfcheck/smoke/verdict.test.mjs +51 -2
  50. package/tool/selfcheck/smoke.sh +6 -2
  51. package/tool/selfcheck/units-cadence.mjs +51 -1
  52. package/tool/selfcheck/units-context.mjs +64 -1
  53. package/tool/selfcheck/units-execution.mjs +78 -1
  54. package/tool/selfcheck/units-level.mjs +99 -1
  55. package/tool/selfcheck/units-probe.mjs +104 -0
  56. package/tool/selfcheck/units-repo.mjs +146 -0
  57. package/tool/selfcheck/units-verdict.mjs +76 -0
  58. package/tool/selfcheck/units.mjs +34 -1
@@ -22,14 +22,17 @@
22
22
  // не порог. Порог — у `doctor --run --min`.
23
23
  import { spawnSync } from "node:child_process";
24
24
  import { mkdtemp, mkdir, copyFile, rm, readdir, writeFile, readFile, symlink } from "node:fs/promises";
25
+ import { readFileSync } from "node:fs";
25
26
  import { tmpdir } from "node:os";
26
27
  import { join, dirname, extname } from "node:path";
27
28
  import { readManifest } from "../lib/manifest.mjs";
28
- import { fixHotspots, probeSummary, probeVerdictPaired, countProbe } from "../lib/history.mjs";
29
+ import { fixHotspots, probeSummary, probeVerdictPaired, countProbe, namesPlant, catchVerdict } from "../lib/history.mjs";
29
30
  import { detectFacts, readCatalog, triggerVerdict } from "../lib/repo.mjs";
31
+ import { blindAdvice } from "../lib/advice.mjs";
30
32
  import { CWD, GATES_SRC, TARGET_DIR, c, SELF, exists } from "../lib/core.mjs";
31
- import { probeState, probeEvery, PROBE_EVERY } from "../lib/cadence.mjs";
33
+ import { probeState, probeEvery, PROBE_EVERY, blindLines, parseBlind, parseRan } from "../lib/cadence.mjs";
32
34
  import { L } from "../i18n/index.mjs";
35
+ import { gateCommand } from "../lib/execution.mjs";
33
36
 
34
37
  // Тот же набор расширений, что у привязки доказательства к дифу. Список один на программу:
35
38
  // второй через месяц разошёлся бы с первым.
@@ -93,7 +96,7 @@ async function writeMark(now, blind, lines) {
93
96
  "",
94
97
  ...lines,
95
98
  "",
96
- "Файл эфемерный: его переписывает каждая проба. В .gitignore его стоит держать самому.",
99
+ "Файл эфемерный: его переписывает каждая проба. `aqk init` кладёт его в .gitignore.",
97
100
  ].join("\n");
98
101
  await writeFile(MARK(), body + "\n", "utf8");
99
102
  }
@@ -137,11 +140,28 @@ function extAlternatives(ext) {
137
140
  return fam ? [ext, ...fam.filter((e) => e !== ext)] : [ext];
138
141
  }
139
142
 
143
+ // Показать САМ ОБРАЗЕЦ, а не пересказ. «Класс не прикрыт» остаётся словами, пока человек не
144
+ // увидел, что именно мы подсадили в его файл.
145
+ //
146
+ // Первая версия печатала одну «показательную» строку — и угадывала плохо: у мёртвого кода дефект
147
+ // во ВТОРОЙ функции, у отладочной печати во второй строке тела. Угадывать не надо: образцы
148
+ // каталога маленькие по норме, и четырёх строк хватает, чтобы стало видно. Комментарии
149
+ // выброшены: в наших образцах они объясняют замысел коллеге, а не показывают дефект.
150
+ function sampleLines(path, max = 4) {
151
+ let text = "";
152
+ try { text = readFileSync(path, "utf8"); } catch { return []; }
153
+ return text.split("\n")
154
+ .map((l) => l.replace(/\s+$/, ""))
155
+ .filter((l) => l.trim() && !/^\s*(#|\/\/|\/\*|\*|--|<!--)/.test(l))
156
+ .slice(0, max)
157
+ .map((l) => l.slice(0, 88));
158
+ }
159
+
140
160
  // Красный образец записи, подходящий по расширению горячего файла. Расширение обязано
141
161
  // совпадать: питоновский образец в проекте на TypeScript не проверит ничего, а покажет
142
162
  // «не прикрыто» — ложная тревога того же класса, что молчащий гейт, только наоборот.
143
- async function redSampleFor(entry, ext) {
144
- const dir = join(GATES_SRC, entry, "red");
163
+ async function sampleFor(entry, ext, kind = "red") {
164
+ const dir = join(GATES_SRC, entry, kind);
145
165
  if (!(await exists(dir))) return null;
146
166
  let names = [];
147
167
  try { names = await readdir(dir); } catch { return null; }
@@ -160,7 +180,8 @@ async function redSampleFor(entry, ext) {
160
180
  //
161
181
  // Способ взят из мутационного тестирования, где та же задача решена двадцать лет назад: Stryker
162
182
  // копирует проект во временный каталог, СИМЛИНКУЕТ `node_modules` и гоняет там родную команду.
163
- // Копируются только ОТСЛЕЖИВАЕМЫЕ файлы (`git archive HEAD`) — рабочее дерево не трогается, а
183
+ // Копируются отслеживаемые и неигнорируемые файлы (`git ls-files --cached --others
184
+ // --exclude-standard`) — файлы проекта не меняются (итог пишется в .aqk/last-probe.md), а
164
185
  // мусор сборки не тащится; тяжёлые каталоги зависимостей симлинкуются, иначе `npm test` в
165
186
  // песочнице падал бы с «модуль не найден», и это читалось бы как сбой инструмента.
166
187
  const DEP_DIRS = ["node_modules", ".venv", "venv", "vendor", "target", ".tox", ".bundle"];
@@ -218,20 +239,44 @@ async function plant(root, relPath, sample) {
218
239
  // Гейт запускается В ПЕСОЧНИЦЕ и командой КАК ЕСТЬ — ничего в неё не подставляется. Именно это
219
240
  // и делает пробу независимой от формы команды.
220
241
  //
221
- // `stopOnRed` ранний выход: как только гейт покраснел, вердикт «поймано» уже получен, и гонять
222
- // остальные незачем. На сухом прогоне выхода нет: там нужны ВСЕ длительности и все коды.
223
- function runGates(gates, sandbox, { stopOnRed = false } = {}) {
242
+ // Вывод гейта сохраняется: по нему видно, ИЗ-ЗА ЧЕГО он покраснел (см. catchVerdict).
243
+ function runGates(gates, sandbox) {
224
244
  const out = [];
225
245
  for (const [name, cmd] of gates) {
226
246
  const t0 = Date.now();
227
- const r = spawnSync(cmd, { shell: true, cwd: sandbox, encoding: "utf8", timeout: 120000 });
247
+ const r = spawnSync(gateCommand(cmd), { shell: true, cwd: sandbox, encoding: "utf8", timeout: 120000 });
228
248
  const code = r.status === null ? 2 : r.status;
229
- out.push({ name, code, ms: Date.now() - t0 });
230
- if (stopOnRed && code === 1) break;
249
+ out.push({ name, code, ms: Date.now() - t0, out: `${r.stdout || ""}${r.stderr || ""}`.slice(0, 200000) });
231
250
  }
232
251
  return out;
233
252
  }
234
253
 
254
+ // Один класс брака на одном файле: гейты по одному, в порядке плана. Покраснел и назвал файл —
255
+ // контроль ЗЕЛЁНЫМ образцом той же записи в том же месте (catchVerdict). Ранний выход — только на
256
+ // ПОДТВЕРЖДЁННОЙ поимке: первым мог покраснеть форматтер, и остановка на нём скрыла бы линтер,
257
+ // который брак действительно поймал.
258
+ async function probeOne({ sandbox, rel, red, green, gates, baseOut }) {
259
+ const after = [];
260
+ let restore = await plant(sandbox, rel, red);
261
+ try {
262
+ for (const g of gates) {
263
+ const [res] = runGates([g], sandbox);
264
+ after.push(res);
265
+ if (res.code !== 1) continue;
266
+ let ctl = null;
267
+ if (green && namesPlant(baseOut(res.name), res.out, rel)) {
268
+ await restore(); restore = await plant(sandbox, rel, green);
269
+ [ctl] = runGates([g], sandbox);
270
+ await restore(); restore = await plant(sandbox, rel, red);
271
+ }
272
+ res.why = catchVerdict(baseOut(res.name), res, ctl, rel);
273
+ res.named = res.why === "caught";
274
+ if (res.named) break;
275
+ }
276
+ } finally { await restore(); }
277
+ return after;
278
+ }
279
+
235
280
  // Каким гейтом пробовать и в каком порядке.
236
281
  //
237
282
  // Цена пробы = (файлы × записи) × сумма длительностей гейтов. На самом комплекте после перехода
@@ -302,7 +347,8 @@ async function cmdProbe(args, { auto = false } = {}) {
302
347
  // Пробуем только запланированными, в порядке плана.
303
348
  const byName = new Map(gates);
304
349
  const probeGates = plan.use.map((g) => [g.name, byName.get(g.name)]);
305
- const baseline = plan.use.map((g) => ({ name: g.name, code: g.code }));
350
+ const baseline = plan.use.map((g) => ({ name: g.name, code: g.code, out: g.out }));
351
+ const baseOut = (name) => baseline.find((b) => b.name === name)?.out || "";
306
352
 
307
353
  console.log(c.dim(` ${P.method(hot.length, entries.length, plan.use.length)}\n`));
308
354
  if (plan.tooSlow.length) {
@@ -319,28 +365,41 @@ async function cmdProbe(args, { auto = false } = {}) {
319
365
  let probed = 0;
320
366
 
321
367
  for (const e of entries) {
322
- const sample = await redSampleFor(e.slug, ext);
368
+ const sample = await sampleFor(e.slug, ext, "red");
323
369
  if (!sample) continue;
324
370
  probed++;
325
- const restore = await plant(sandbox, rel, sample);
326
- let after;
327
- try { after = runGates(probeGates, sandbox, { stopOnRed: true }); } finally { await restore(); }
371
+ const green = await sampleFor(e.slug, ext, "green");
372
+ const after = await probeOne({ sandbox, rel, red: sample, green, gates: probeGates, baseOut });
328
373
  // Ранний выход обрывает список: гейты, до которых не дошли, считаются такими же, как на
329
374
  // сухом прогоне. Иначе их отсутствие прочиталось бы как сбой запуска.
330
375
  const seen = new Set(after.map((a) => a.name));
331
376
  const full = after.concat(baseline.filter((b) => !seen.has(b.name)));
332
377
  const r = probeVerdictPaired(baseline, full);
333
378
  const verdict = r.verdict;
334
- const caught = full.filter((a) => a.code === 1 && baseline.find((b) => b.name === a.name)?.code === 0)
335
- .map((a) => a.name);
379
+ const redNow = full.filter((a) => a.code === 1 && baseline.find((b) => b.name === a.name)?.code === 0);
380
+ const caught = redNow.filter((a) => a.named !== false).map((a) => a.name);
381
+ const nameless = redNow.filter((a) => a.named === false).map((a) => `${a.name} (${a.why === "planting" ? P.planting : P.nameless})`);
336
382
  records.push({ entry: e.slug, file: rel, verdict });
337
383
  if (verdict === "caught") {
338
384
  console.log(` ${c.green("✔")} ${e.intent.padEnd(48)} ${c.dim(P.caught(caught.join(", ")))}`);
339
385
  } else if (verdict === "blind") {
340
386
  console.log(` ${c.red("✘")} ${e.intent.padEnd(48)} ${c.red(P.blind)}`);
387
+ // Объяснить, а не назвать. Три строки, каждая отвечает на свой вопрос человека:
388
+ // «почему именно здесь», «что вы вообще подсадили» и «что мне сделать ПРЯМО СЕЙЧАС».
389
+ // Последняя обязана работать БЕЗ комплекта: польза до установки — единственный
390
+ // способ заслужить установку.
391
+ const adv = blindAdvice(e, facts, { file: rel, fixes });
392
+ console.log(c.dim(` ${P.blindWhere(rel, fixes)}`));
393
+ const lines = sampleLines(sample);
394
+ if (lines.length) {
395
+ console.log(c.dim(` ${P.blindWhat}`));
396
+ for (const l of lines) console.log(c.dim(` ${l}`));
397
+ }
398
+ if (adv.command) console.log(` ${c.yellow(P.blindFix(adv.command))}`);
399
+ if (adv.tool) console.log(c.dim(` ${P.blindTool(adv.tool)}`));
341
400
  console.log(c.dim(` ${P.install(`${SELF} add ${e.slug}`)}`));
342
401
  } else {
343
- console.log(` ${c.dim("~")} ${c.dim(e.intent.padEnd(48))} ${c.dim(P.unknown)}`);
402
+ console.log(` ${c.dim("~")} ${c.dim(e.intent.padEnd(48))} ${c.dim(nameless.length ? P.unattributed(nameless.join(", ")) : P.unknown)}`);
344
403
  }
345
404
  }
346
405
  if (!probed) { unprobedN++; console.log(c.dim(` ${P.noSampleFor(ext || "—")}`)); }
@@ -362,7 +421,11 @@ async function cmdProbe(args, { auto = false } = {}) {
362
421
 
363
422
  // Отметка нужна не для отчёта, а для КАДЕНЦИИ: по ней следующий прогон поймёт, что пора.
364
423
  // Без неё команда снова становится тем, о чём надо вспомнить.
365
- await writeMark(commitCount(), blind, hot.map(({ path: p2, fixes }) => `- ${p2} (${P.fixes(fixes)})`));
424
+ await writeMark(commitCount(), blind, [
425
+ `ran: ${probeGates.map(([name]) => name).join(" ")}`, "",
426
+ ...blindLines(records), "",
427
+ ...hot.map(({ path: p2, fixes }) => `- ${p2} (${P.fixes(fixes)})`),
428
+ ]);
366
429
  } finally {
367
430
  await rm(sandbox, { recursive: true, force: true });
368
431
  }
@@ -378,7 +441,8 @@ async function probeStatus() {
378
441
  const every = probeEvery(man);
379
442
  if (every === null) return { state: "unknown", behind: null, badEvery: String(man?.probe) };
380
443
  if (every === 0) return { state: "off", behind: null };
381
- return probeState(await readMark(), commitCount(), every);
444
+ const mark = await readMark();
445
+ return { ...probeState(mark, commitCount(), every), classes: parseBlind(mark?.text), ran: parseRan(mark?.text) };
382
446
  }
383
447
 
384
448
  export { cmdProbe, probeStatus, probeableGates, gatesState, extAlternatives, planProbeGates, isCode };
@@ -6,7 +6,7 @@ import { spawnSync } from "node:child_process";
6
6
  import { join, dirname, relative } from "node:path";
7
7
  import {
8
8
  CWD, PKG_ROOT, DOCS_SRC, RULES_SRC, TARGET_DIR, MANIFEST, SELF, REPO_URL, c, exists, die,
9
- copyDir, writeIfAbsent, FEEDBACK_MARK, docPath } from "../lib/core.mjs";
9
+ copyDir, writeIfAbsent, FEEDBACK_MARK, docPath, ensureIgnored } from "../lib/core.mjs";
10
10
  import { AGENTS_MD, CLAUDE_MD, MANIFEST_YML } from "../lib/templates.mjs";
11
11
  import { banner } from "../lib/banner.mjs";
12
12
  import { readManifest } from "../lib/manifest.mjs";
@@ -41,6 +41,10 @@ async function cmdInit(args) {
41
41
  const claude = join(CWD, "CLAUDE.md");
42
42
  track(await writeIfAbsent(claude, CLAUDE_MD, { force }), claude);
43
43
 
44
+ // Служебные файлы — в .gitignore сразу, до первого прогона: иначе первый же `doctor --run`
45
+ // оставит в дереве файл, который попадёт в коммит (так и случилось на живом проекте).
46
+ const ignored = await ensureIgnored(CWD);
47
+
44
48
  // Заставка в начале init — первая встреча человека с комплектом. Второй раз он увидит её
45
49
  // только если сам спросит `--version`: то, что видишь тридцатый раз, перестаёт читаться.
46
50
  console.log(`\n${banner()}\n`);
@@ -56,6 +60,7 @@ async function cmdInit(args) {
56
60
  if (LANG === "en" && created.some((f) => f.includes(`${TARGET_DIR}/docs/`) || f.includes(`${TARGET_DIR}\\docs\\`))) {
57
61
  console.log(c.dim(`\n ${L.init.docsRu}`));
58
62
  }
63
+ if (ignored.length) console.log(c.dim(`\n ${L.init.ignored(ignored.join(", "))}`));
59
64
  if (skipped.length) {
60
65
  console.log(c.yellow(`\n ${L.init.kept(skipped.length)}`));
61
66
  for (const f of skipped) console.log(` ${f}`);
@@ -86,7 +86,7 @@ async function cmdReport() {
86
86
 
87
87
  // Прогон, а не чтение манифеста: «объявлен» и «работает» — разные утверждения, и весь
88
88
  // смысл этой команды в том, чтобы в отчёт попало второе.
89
- const run = declared.length ? runGates(man) : { results: [], failed: 0 };
89
+ const run = declared.length ? await runGates(man) : { results: [], failed: 0 };
90
90
 
91
91
  const held = [], broken = [], todo = [], skip = [];
92
92
  for (const res of run.results) {
@@ -16,11 +16,13 @@
16
16
  // сознательное решение: pre-commit локально мы не ставим, проверки идут в CI. Команда, которая
17
17
  // кричит «сломано» про выбор, — ровно та, которую выключают в первый день, и вместе с ней
18
18
  // перестают читать настоящие отказы. Кода возврата касается только `✘`.
19
+ import { spawnSync } from "node:child_process";
19
20
  import { readFile } from "node:fs/promises";
20
21
  import { join } from "node:path";
21
- import { CWD, MANIFEST, SELF, c, exists } from "../lib/core.mjs";
22
+ import { CWD, MANIFEST, SELF, c, exists, preCommitHook } from "../lib/core.mjs";
22
23
  import { readManifest, unparsedLines, gateRequires } from "../lib/manifest.mjs";
23
24
  import { whichSync } from "../lib/repo.mjs";
25
+ import { gitBash } from "../lib/execution.mjs";
24
26
  import { updateWanted } from "../lib/brief.mjs";
25
27
  import { L } from "../i18n/index.mjs";
26
28
 
@@ -94,7 +96,11 @@ async function cmdVitals() {
94
96
  for (const [gate, cmd] of Object.entries(gates)) {
95
97
  const prog = progOf(cmd);
96
98
  if (!prog || seen.has(prog)) continue;
97
- seen.set(prog, { gate, prog, found: Boolean(whichSync(prog)) });
99
+ // На Windows слово `bash` в PATH — часто заглушка WSL, и «найден» было бы неправдой: гейт
100
+ // запустится через Git Bash (gateCommand) или не запустится вовсе. Спрашиваем того же, кого
101
+ // спросит прогон, — иначе vitals и doctor --run снова разойдутся.
102
+ const found = prog === "bash" && process.platform === "win32" ? Boolean(gitBash()) : Boolean(whichSync(prog));
103
+ seen.set(prog, { gate, prog, found });
98
104
  }
99
105
 
100
106
  // Первого слова мало. Запись каталога бывает обёрткой: команда начинается с `bash`, который
@@ -117,14 +123,7 @@ async function cmdVitals() {
117
123
  // Хук pre-commit проверяется в `.git/hooks`, а НЕ в `.pre-commit-config.yaml`. Запись в
118
124
  // конфиге — это намерение; сработает только то, что лежит в самом гите. Ровно та разница,
119
125
  // ради которой весь комплект: объявлено и работает — разные утверждения.
120
- let preCommit = null;
121
- const hook = join(CWD, ".git", "hooks", "pre-commit");
122
- if (await exists(join(CWD, ".git"))) {
123
- preCommit = false;
124
- if (await exists(hook)) {
125
- try { preCommit = /pre-commit|aqk/i.test(await readFile(hook, "utf8")); } catch { preCommit = null; }
126
- }
127
- }
126
+ const preCommit = await preCommitHook(CWD);
128
127
 
129
128
  let sessionHook = null;
130
129
  const settings = join(CWD, ".claude", "settings.json");
@@ -140,15 +139,25 @@ async function cmdVitals() {
140
139
 
141
140
  let version = null;
142
141
  if (updateWanted()) {
142
+ let current = "";
143
143
  try {
144
144
  const { PKG_ROOT } = await import("../lib/core.mjs");
145
- const current = JSON.parse(await readFile(join(PKG_ROOT, "package.json"), "utf8")).version || "";
145
+ current = JSON.parse(await readFile(join(PKG_ROOT, "package.json"), "utf8")).version || "";
146
146
  const r = await fetch("https://registry.npmjs.org/agent-quality-kit/latest", {
147
147
  signal: AbortSignal.timeout(3000),
148
148
  headers: { accept: "application/vnd.npm.install-v1+json" },
149
149
  });
150
150
  version = { current, latest: r.ok ? String((await r.json()).version || "") : "" };
151
- } catch { /* сети нет строку про версию просто не покажем */ }
151
+ } catch { /* прямой запрос не прошёл ниже спросим npm */ }
152
+ // ПРЯМОЙ ЗАПРОС НЕ ПРОШЁЛ — СПРОСИТЬ NPM. Отзыв с живого проекта 2026-09-11: vitals писал «не
153
+ // достучался до реестра», а `npm view` на той же машине работал. `fetch` в Node не знает
154
+ // прокси и зеркала из .npmrc, а npm знает. Только здесь, не в хуке: там секунды дороже.
155
+ if (current && !version?.latest) {
156
+ const r = spawnSync("npm", ["view", "agent-quality-kit", "version"],
157
+ { encoding: "utf8", timeout: 10000, shell: process.platform === "win32" });
158
+ const latest = r.status === 0 ? String(r.stdout || "").trim().split("\n").pop() : "";
159
+ version = { current, latest };
160
+ }
152
161
  }
153
162
 
154
163
  const rows = vitalsRows({ tools: [...seen.values()], unparsed, preCommit, sessionHook, version });
@@ -66,18 +66,39 @@ const enDocs = {
66
66
  runClean: (when) => `Last run ${when} — nothing red.`,
67
67
  runRed: (when, names) => `Last run ${when} — RED: ${names}.`,
68
68
  andMore: (n) => `and ${n} more`,
69
- skipped: (n) => `Not run: ${n} — the tool is absent on this machine, their state is unknown.`,
69
+ skipped: (n) => `Not run: ${n} — skipped by --only/--skip or the tool is absent on this machine; their state is unknown.`,
70
70
  probeNever: "No coverage probe has run — what is covered by nothing here is UNKNOWN. That is not \"covered\": `aqk probe`.",
71
71
  probeOff: "The coverage probe is switched off in the manifest (`probe: 0`) — nobody counts what is covered by nothing here.",
72
72
  probeUnknown: "The coverage probe could not run — what is covered by nothing here is UNKNOWN. That is not \"covered\".",
73
- probeBlind: (n, behind) =>
73
+ probeBlind: (n, behind, names = "") =>
74
74
  `Covered by nothing: ${n} defect classes in the places people most often come back to fix` +
75
+ (names ? ` — ${names}` : "") +
75
76
  (behind ? ` (the probe is ${behind} commits behind)` : "") + ". Details: `aqk probe`.",
76
77
  probeClean: (behind) =>
77
78
  "Coverage probe: in the places probed, every applicable class is caught by something" +
78
79
  (behind ? ` (${behind} commits behind)` : "") + ".",
79
80
  ratchets: (list) => `Ratchets: ${list}. The list may only get shorter, never longer.`,
80
81
  where: (entry) => `The rulebook: ${entry}. What proves a diff: \`aqk report --since main\`.`,
82
+ nextTitle: "Next, in order — computed from this repository, not generic advice:",
83
+ nextStep: {
84
+ init: () => "Set up the standard: `aqk init` — without .aqk.yml `aqk add` refuses.",
85
+ adopt: (s) => `Declare the checks this project already has: in .aqk.yml, under gates: ${s.gates.map((g) => `${g.name}: "${g.cmd}"`).join(", ")}. Then \`aqk doctor --run\`.`,
86
+ blind: (s) => `The "${s.slug}" defect the probe planted in ${s.file} was NOT caught by your checks.` +
87
+ (s.command ? ` Catch it now: \`${s.command}\`.` : "") + ` Keep it caught: \`aqk add ${s.slug}\`.`,
88
+ start: (s) => `Install ${s.slug}: \`aqk add ${s.slug}\`` + (s.command ? ` (one line, no kit needed: \`${s.command}\`)` : "") +
89
+ ", then `aqk prove` — the gate must go red on its own red sample.",
90
+ },
91
+ nextMore: (n) => `And ${n} more — the full list: \`aqk doctor\`.`,
92
+ whenTitle: "When to do what:",
93
+ whenCommit: (hook) => hook === true
94
+ ? "Before a commit → the `.git/hooks/pre-commit` hook runs the checks itself; do not bypass it (`--no-verify`)."
95
+ : "Before a commit → `aqk doctor --run --since main`: there is no pre-commit hook, it will not happen by itself.",
96
+ whenRules: [
97
+ "Added or changed a check → `aqk prove`: the gate must go red on its own red sample, otherwise it checks nothing.",
98
+ "Writing a rule into the rulebook → put the arbiter mark next to it, `<!-- aqk: gate-name -->`; no gate — `<!-- aqk: human -->`, which is an admission, not a check.",
99
+ "A check is in your way → do not weaken it (`|| true`, `--exit-zero`, a suppression without a rule code): stop and ask the owner.",
100
+ "You think it is done → `aqk report --since main`: every changed file must be named by a check.",
101
+ ],
81
102
  mapTitle: "WHAT THIS TOOL CAN DO. The full list of commands — not a retelling, the same list\nthe help is built from:",
82
103
  rulesTitle: (e) => `THE RULEBOOK OF THIS PROJECT (${e}) — verbatim, in full. This is not an invitation\nto read it: it is already here.`,
83
104
  hookAlready: (p) => `the hook is already in ${p} — changing nothing.`,
@@ -195,6 +216,7 @@ const enDocs = {
195
216
  },
196
217
  ],
197
218
  report: {
219
+ skippedBySelect: "not run (by --only/--skip), state unknown",
198
220
  title: "aqk doctor --run",
199
221
  version: "version",
200
222
  level: "level",
@@ -158,6 +158,7 @@ export const enGates = {
158
158
  "the guides in .aqk/docs/ are in Russian — a deliberate decision, not a broken install.\n The rules in .aqk/rules/ are in English; the guides are prose an agent may ignore anyway,\n and what a machine holds lives in .aqk.yml and the gates. Translation waits for someone who needs it.",
159
159
  created: (n) => `created (${n}):`,
160
160
  andMore: (n) => `… and ${n} more`,
161
+ ignored: (list) => `the kit's runtime files were added to .gitignore — machine state, it does not belong in git: ${list}`,
161
162
  kept: (n) => `already there, left untouched (${n}):`,
162
163
  overwrite: (cmd) => `overwrite: ${cmd}`,
163
164
  nextTitle: "What to do next, in order:",
@@ -255,16 +256,24 @@ export const enGates = {
255
256
  badEvery: (v) => `the manifest says probe: "${v}", which is not a commit count. The probe does NOT run: silently using the default would mean doing something other than what is written.`,
256
257
  autoFirst: "no coverage probe has ever run here — running it myself. Turn off: AQK_PROBE=0",
257
258
  auto: (n) => `${n} commits since the last probe — running it myself. Turn off: AQK_PROBE=0`,
259
+ autoNotInCi: (cmd) => `in CI the probe does not run by itself — it would be minutes of surprise in a fast check; run it as a separate job: ${cmd} (or AQK_PROBE=1)`,
258
260
  title: "aqk probe — what the declared checks cannot see",
259
261
  method: (files, entries, gates) =>
260
262
  `method: a red sample from a catalogue entry is planted into a COPY of the project, then the ` +
261
263
  `DECLARED gates are run there — the command is used as written, nothing is substituted into it. ` +
262
264
  `Files: ${files}, applicable entries: ${entries}, gates green on a clean checkout: ${gates}. ` +
263
- `The working tree is not touched.`,
265
+ `Project files are not changed; the probe result goes to .aqk/last-probe.md (in .gitignore after init).`,
264
266
  fixes: (n) => `fixes in history: ${n}`,
265
267
  caught: (names) => `caught by: ${names}`,
266
268
  blind: "NOTHING CATCHES IT",
267
269
  unknown: "nothing to check with — the gate did not run (delegated tool missing)",
270
+ nameless: "did not name the planted file",
271
+ planting: "goes red on the green sample too — fails from the planting itself",
272
+ unattributed: (g) => `went red: ${g} — the catch is not proven`,
273
+ blindWhere: (file, fixes) => `where: ${file} — ${fixes} fix commits in its history`,
274
+ blindWhat: "what we planted into your file:",
275
+ blindFix: (cmd) => `catch it right now, no kit needed: ${cmd}`,
276
+ blindTool: (url) => `the tool: ${url}`,
268
277
  install: (cmd) => `close it: ${cmd}`,
269
278
  noSampleFor: (ext) => `the catalogue has no red sample for "${ext}" — nothing to check with`,
270
279
  noGates: (cmd) => `no gates declared — nothing to probe with. First: ${cmd}`,
package/tool/i18n/en.mjs CHANGED
@@ -44,6 +44,7 @@ export const en = {
44
44
  blob: "assemble the guides into a single GOD_AI.md",
45
45
  learn: "rule candidates from local transcripts: said out loud, never written down",
46
46
  context: "the project state in one block — for an agent's context, not for reading",
47
+ vitals: "is what the kit runs on wired up: gate tools, hooks, version freshness",
47
48
  contextInstall: "the same in full — the map and the rulebook — installed as a hook",
48
49
  report: "the mandatory report form: what is in place, what is not, what was not read; --since <ref> adds what proves the diff",
49
50
  badge: "a level badge for your README — and a check that it does not lie",
@@ -65,6 +66,14 @@ export const en = {
65
66
  gitignore: "repository hygiene",
66
67
  git: "project under version control",
67
68
 
69
+ runtimeTracked: (f, cmd) => `${f} is tracked by git — every run rewrites it, and the tree always shows a modified file. Take it out: ${cmd}`,
70
+ runtimeNotIgnored: (f, cmd) => `${f} is this machine's state, yet git sees it: one \`git add .\` and it is in a commit. Hide it: ${cmd}`,
71
+ layoutAdvice: "missing — advice, it does not fail the run",
72
+ coversImpossible: (entry, gate, linter) => `the claim "${gate} holds ${entry}" is wrong: ${linter} has no rule for this class — there is nothing to hold it with`,
73
+ coversCantCheck: (entry, gate) => `cannot check the claim "${gate} holds ${entry}": the gate's linter is not recognised or the entry has no rules for it — taken on trust`,
74
+ selectUnknown: (names, groups) => `--only/--skip: "${names}" is neither a gate from gates: nor a group from groups:${groups ? ` (groups: ${groups})` : ""}. Running everything instead of skipping would be a lie, so stopping.`,
75
+ selectSkipped: (names) => `not run (by --only/--skip): ${names} — their state is unknown, they are not "green"`,
76
+ jobsBad: (v) => `--jobs expects a whole number from 1: "${v}" will not do. A one-by-one run passed off as parallel would be a lie, so stopping.`,
68
77
  rulesByHuman: (total, machine, human) =>
69
78
  `${human} of ${total} rules in the entry point are guarded by a HUMAN, ${machine} by a machine.`,
70
79
  rulesByHumanWhy: "A rule guarded by a human is guarded by nobody the day the human is busy. That is the hole this kit exists to close — and the reminder belongs to the human, not only to the agent. Counted in the ENTRY POINT only: a promise kept in any other file is checked by nothing at all, and this kit will not tell you it exists.",
@@ -104,6 +113,19 @@ export const en = {
104
113
  `claim unverified: "${e}" is declared held by gate "${g}", but neither its command nor the\n linter config names rules ${codes} — the entry may be held by nothing`,
105
114
  coversUnprovenHow: (cmd) => `settle it: add those rules to the linter, or install the entry — ${cmd}`,
106
115
  totalCovered: (n) => `held by another arbiter ${n}`,
116
+ blindHeading: (behind) => `The probe${behind ? ` (${behind} commits ago)` : ""} planted defects in your files — your checks did NOT catch them:`,
117
+ blindRan: (g) => `gate ${g} is declared and was run — and still missed the defect in this file`,
118
+ blindInstalled: "declared, but the probe did not run this gate (added later or too slow) — the next probe will show whether it catches",
119
+ blindMore: (cmd) => `what was planted and where — ${cmd}`,
120
+ probeNever: (cmd) => `Whether your checks catch a real defect has not been tested yet: ${cmd} plants one in a copy of the project and shows (a minute or two, your project files are not changed).`,
121
+ todoRest: (n) => `The other entries that apply here (${n}):`,
122
+ todoRestHow: (self) => `install any: ${self} add <name> · what it catches and why: ${self} why <name>`,
123
+ startWith: "Start with these three — born from a real failure, and each closes with one ready command:",
124
+ startCmd: (cmd) => `one line, no kit needed: ${cmd}`,
125
+ startTool: (url) => `the tool: ${url}`,
126
+ startHook: "Running these by hand is a one-off. To have them run before every push: pre-commit (repo: https://github.com/arsen-ask-lx/Agent_Quality_Kit, hooks aqk / aqk-doctor), or a plain .git/hooks/pre-push.",
127
+ haveAlready: (n) => `Checks you ALREADY have (${n}) — found in your own files, not invented:`,
128
+ haveAlreadyHow: (line) => `declare them and a machine holds them, not your attention. In .aqk.yml, under gates: ${line}`,
107
129
  total: "Total:",
108
130
  totalHeld: (n) => `held by a machine ${n}`,
109
131
  totalTodo: (n) => `applicable but not installed ${n}`,
@@ -120,6 +142,8 @@ export const en = {
120
142
  `either fix them and drop them from advisory, or admit the rule does not exist.`,
121
143
  runHeading: "Running the declared gates",
122
144
  timeout: "did not finish within 5 minutes",
145
+ running: (i, n) => `[${i}/${n}] running…`,
146
+ proving: "checking that the gates catch defects on their own samples…",
123
147
  exitCode: (code) => `exit ${code}`,
124
148
  moreLines: (n) => `… and ${n} more lines`,
125
149
  declaredNotRun: (n) => `${n} gates declared, but never run.`,
@@ -186,6 +210,7 @@ export const en = {
186
210
  has_docker: ["no Dockerfile or compose", "docker is already here"],
187
211
  has_deps: ["no dependency file in sight", "dependencies are declared"],
188
212
  has_tests: ["no tests in sight", "tests exist"],
213
+ has_biome: ["the linter is not Biome", "the project's linter is Biome"],
189
214
  has_env: ["no environment file", "an environment file exists"],
190
215
  has_agent_config: ["the agent was never configured here", "agent settings exist"],
191
216
  has_agent_entry: ["no entry point for an agent here", "an entry point for an agent exists"],
@@ -68,18 +68,41 @@ const ruDocs = {
68
68
  runClean: (when) => `Последний прогон ${when} — красных нет.`,
69
69
  runRed: (when, names) => `Последний прогон ${when} — КРАСНЫЕ: ${names}.`,
70
70
  andMore: (n) => `и ещё ${n}`,
71
- skipped: (n) => `Не запускались: ${n} — инструмента нет на этой машине, их состояние неизвестно.`,
71
+ skipped: (n) => `Не запускались: ${n} — пропущены по --only/--skip или инструмента нет на этой машине; их состояние неизвестно.`,
72
72
  probeNever: "Проба покрытия не делалась — что здесь не прикрыто ничем, НЕИЗВЕСТНО. Это не «прикрыто»: `aqk probe`.",
73
73
  probeOff: "Проба покрытия выключена в манифесте (`probe: 0`) — что здесь не прикрыто ничем, никто не считает.",
74
74
  probeUnknown: "Пробу покрытия провести не удалось — что здесь не прикрыто ничем, НЕИЗВЕСТНО. Это не «прикрыто».",
75
- probeBlind: (n, behind) =>
75
+ probeBlind: (n, behind, names = "") =>
76
76
  `Не прикрыто ничем: ${n} классов брака в местах, куда чаще всего возвращаются с починкой` +
77
+ (names ? ` — ${names}` : "") +
77
78
  (behind ? ` (проба отстала на ${behind} коммитов)` : "") + ". Подробно: `aqk probe`.",
78
79
  probeClean: (behind) =>
79
80
  "Проба покрытия: в проверенных местах каждый применимый класс кто-то ловит" +
80
81
  (behind ? ` (отстала на ${behind} коммитов)` : "") + ".",
81
82
  ratchets: (list) => `Храповики: ${list}. Список может только укорачиваться, увеличивать нельзя.`,
82
83
  where: (entry) => `Свод правил: ${entry}. Чем доказан диф: \`aqk report --since main\`.`,
84
+ nextTitle: "Дальше, по порядку — вычислено из этого репозитория, а не общий совет:",
85
+ // По виду шага — таблица, а не лесенка тернарников: её не держат в голове, и наш же
86
+ // complexity-limit её поймал.
87
+ nextStep: {
88
+ init: () => "Заведи стандарт: `aqk init` — без .aqk.yml `aqk add` откажет.",
89
+ adopt: (s) => `Объяви проверки, которые у проекта уже есть: в .aqk.yml, в gates: ${s.gates.map((g) => `${g.name}: "${g.cmd}"`).join(", ")}. Затем \`aqk doctor --run\`.`,
90
+ blind: (s) => `Брак «${s.slug}», подсаженный пробой в ${s.file}, ваши проверки НЕ поймали.` +
91
+ (s.command ? ` Поймать сейчас: \`${s.command}\`.` : "") + ` Держать всегда: \`aqk add ${s.slug}\`.`,
92
+ start: (s) => `Поставь ${s.slug}: \`aqk add ${s.slug}\`` + (s.command ? ` (одной строкой, без комплекта: \`${s.command}\`)` : "") +
93
+ ", затем `aqk prove` — гейт обязан покраснеть на своём красном образце.",
94
+ },
95
+ nextMore: (n) => `И ещё ${n} — весь список: \`aqk doctor\`.`,
96
+ whenTitle: "Когда что делать:",
97
+ whenCommit: (hook) => hook === true
98
+ ? "Перед коммитом → хук `.git/hooks/pre-commit` прогонит проверки сам; не обходи его (`--no-verify`)."
99
+ : "Перед коммитом → `aqk doctor --run --since main`: хука перед коммитом нет, само это не случится.",
100
+ whenRules: [
101
+ "Добавил или поменял проверку → `aqk prove`: гейт обязан покраснеть на своём красном образце, иначе он не проверяет ничего.",
102
+ "Пишешь правило в свод → рядом метка арбитра `<!-- aqk: имя-гейта -->`; гейта нет — `<!-- aqk: человек -->`, и это признание, а не проверка.",
103
+ "Проверка мешает → не ослабляй её (`|| true`, `--exit-zero`, подавление без кода правила): остановись и спроси владельца.",
104
+ "Считаешь, что готово → `aqk report --since main`: каждый изменённый файл должен быть назван проверкой.",
105
+ ],
83
106
  mapTitle: "ЧТО УМЕЕТ ЭТОТ ИНСТРУМЕНТ. Полный список команд — не пересказ, а тот же список,\nиз которого собрана справка:",
84
107
  rulesTitle: (e) => `СВОД ПРАВИЛ ЭТОГО ПРОЕКТА (${e}) — дословно, целиком. Это не приглашение\nпрочитать: он уже здесь.`,
85
108
  hookAlready: (p) => `хук уже стоит в ${p} — ничего не меняю.`,
@@ -197,6 +220,7 @@ const ruDocs = {
197
220
  },
198
221
  ],
199
222
  report: {
223
+ skippedBySelect: "не запускался (по --only/--skip), состояние неизвестно",
200
224
  title: "aqk doctor --run",
201
225
  version: "версия",
202
226
  level: "уровень",
@@ -160,6 +160,7 @@ export const ruGates = {
160
160
  "методички в .aqk/docs/ остаются на русском — решение, а не недоделка.",
161
161
  created: (n) => `создано (${n}):`,
162
162
  andMore: (n) => `… и ещё ${n}`,
163
+ ignored: (list) => `служебные файлы комплекта дописаны в .gitignore — это состояние машины, в git ему не место: ${list}`,
163
164
  kept: (n) => `уже были на месте, не тронуты (${n}):`,
164
165
  overwrite: (cmd) => `перезаписать: ${cmd}`,
165
166
  nextTitle: "Что дальше — по порядку:",
@@ -257,16 +258,24 @@ export const ruGates = {
257
258
  badEvery: (v) => `в манифесте probe: «${v}» — это не число коммитов. Проба НЕ делается: подставить умолчание значило бы делать не то, что написано.`,
258
259
  autoFirst: "пробы покрытия здесь ещё не делали — делаю её сам. Выключить: AQK_PROBE=0",
259
260
  auto: (n) => `прошло ${n} коммитов с прошлой пробы — делаю её сам. Выключить: AQK_PROBE=0`,
261
+ autoNotInCi: (cmd) => `в конвейере проба сама не запускается — это минуты сюрпризом в быстрой проверке; поставьте её отдельной задачей: ${cmd} (или AQK_PROBE=1)`,
260
262
  title: "aqk probe — чего объявленные проверки не видят",
261
263
  method: (files, entries, gates) =>
262
264
  `способ: красный образец записи каталога подсаживается в КОПИЮ проекта, и там гоняются ` +
263
265
  `ОБЪЯВЛЕННЫЕ гейты — команда берётся как написана, в неё ничего не подставляется. ` +
264
266
  `Файлов: ${files}, применимых записей: ${entries}, гейтов зелёных на чистом дереве: ${gates}. ` +
265
- `Рабочее дерево не трогается.`,
267
+ `Файлы проекта не меняются; итог пробы пишется в .aqk/last-probe.md (он в .gitignore после init).`,
266
268
  fixes: (n) => `починок в истории: ${n}`,
267
269
  caught: (names) => `ловит: ${names}`,
268
270
  blind: "НЕ ЛОВИТ НИКТО",
269
271
  unknown: "проверить нечем — гейт не состоялся (нет делегированной программы)",
272
+ nameless: "не назвал подсаженный файл",
273
+ planting: "краснеет и на зелёном образце — падает от самой подсадки",
274
+ unattributed: (g) => `покраснело: ${g} — поимка не доказана`,
275
+ blindWhere: (file, fixes) => `где: ${file} — починок в истории: ${fixes}`,
276
+ blindWhat: "что подсадили в ваш файл:",
277
+ blindFix: (cmd) => `поймать прямо сейчас, без комплекта: ${cmd}`,
278
+ blindTool: (url) => `инструмент: ${url}`,
270
279
  install: (cmd) => `закрыть: ${cmd}`,
271
280
  noSampleFor: (ext) => `в каталоге нет красного образца под «${ext}» — проверить нечем`,
272
281
  noGates: (cmd) => `гейтов не объявлено — пробовать нечем. Сначала: ${cmd}`,
package/tool/i18n/ru.mjs CHANGED
@@ -45,6 +45,7 @@ export const ru = {
45
45
  blob: "собрать методички в один файл GOD_AI.md",
46
46
  learn: "кандидаты в правила из локальной переписки: сказано вслух и не записано",
47
47
  context: "состояние проекта одним блоком — для контекста агента, а не для чтения",
48
+ vitals: "подключено ли то, чем комплект работает: инструменты гейтов, хуки, свежесть версии",
48
49
  contextInstall: "то же самое, но целиком — карта и свод правил — и хуком в контекст",
49
50
  report: "обязательная форма отчёта: что стоит, что нет, что не прочитано; --since <ссылка> — ещё и чем доказан диф",
50
51
  badge: "значок уровня для README — и проверка, что он не врёт",
@@ -66,6 +67,14 @@ export const ru = {
66
67
  gitignore: "гигиена репозитория",
67
68
  git: "проект под контролем версий",
68
69
 
70
+ runtimeTracked: (f, cmd) => `${f} отслеживается git — каждый прогон его переписывает, и в дереве вечно висит изменённый файл. Вынуть: ${cmd}`,
71
+ runtimeNotIgnored: (f, cmd) => `${f} — состояние этой машины, а git его видит: одно \`git add .\`, и он в коммите. Спрятать: ${cmd}`,
72
+ layoutAdvice: "нет — это совет, прогон не роняет",
73
+ coversImpossible: (entry, gate, linter) => `заявка «${gate} держит ${entry}» неверна: у ${linter} нет правила под этот класс — закрывать его нечем`,
74
+ coversCantCheck: (entry, gate) => `заявку «${gate} держит ${entry}» проверить не умею: линтер гейта не распознан или правил записи для него нет — принято на слово`,
75
+ selectUnknown: (names, groups) => `--only/--skip: не знаю «${names}» — это не гейт из gates: и не группа из groups:${groups ? ` (группы: ${groups})` : ""}. Прогон всего подряд вместо пропуска был бы неправдой, поэтому стоп.`,
76
+ selectSkipped: (names) => `не запускались (по --only/--skip): ${names} — их состояние неизвестно, это не «зелёные»`,
77
+ jobsBad: (v) => `--jobs ждёт целое число от 1: «${v}» не подходит. Прогон по одному под видом параллельного был бы неправдой, поэтому стоп.`,
69
78
  rulesByHuman: (total, machine, human) =>
70
79
  `правил в точке входа: ${total}. Сторож — ЧЕЛОВЕК у ${human}, машина у ${machine}.`,
71
80
  rulesByHumanWhy: "Правило со сторожем-человеком не сторожит никто в тот день, когда человек занят. Это та самая дыра, ради которой комплект и написан, — и напоминание про неё нужно человеку, а не только агенту. Считаны правила ТОЧКИ ВХОДА: обещание, живущее в любом другом файле, не проверяет вообще ничто, и комплект даже не скажет вам, что оно есть.",
@@ -105,6 +114,19 @@ export const ru = {
105
114
  `заявка не подтверждена: «${e}» объявлена закрытой гейтом «${g}», но ни его команда, ни\n конфиг линтера не называют правил ${codes} — запись может быть не закрыта ничем`,
106
115
  coversUnprovenHow: (cmd) => `подтверди: добавь эти правила в линтер, либо поставь запись — ${cmd}`,
107
116
  totalCovered: (n) => `закрыто другим арбитром ${n}`,
117
+ blindHeading: (behind) => `Проба${behind ? ` (коммитов с тех пор: ${behind})` : ""} подсадила брак в ваши файлы — ваши проверки его НЕ ПОЙМАЛИ:`,
118
+ blindRan: (g) => `гейт ${g} стоит и прогонялся — а брак в этом файле пропустил`,
119
+ blindInstalled: "объявлено, но проба этот гейт не гоняла (поставлен позже или медленный) — поймает ли, покажет следующая",
120
+ blindMore: (cmd) => `что именно подсадили и куда — ${cmd}`,
121
+ probeNever: (cmd) => `Ловят ли ваши проверки настоящий брак, ещё не проверялось: ${cmd} подсадит его в копию проекта и покажет (минута-две, файлы проекта не меняет).`,
122
+ todoRest: (n) => `Остальные записи, применимые у вас (${n}):`,
123
+ todoRestHow: (self) => `поставить любую: ${self} add <имя> · что она ловит и зачем: ${self} why <имя>`,
124
+ startWith: "Начните с этих трёх — они родились из настоящего отказа и закрываются одной готовой командой:",
125
+ startCmd: (cmd) => `одной строкой, без комплекта: ${cmd}`,
126
+ startTool: (url) => `инструмент: ${url}`,
127
+ startHook: "Прогнать руками — разовый героизм. Чтобы это случалось перед каждым пушем: pre-commit (репозиторий https://github.com/arsen-ask-lx/Agent_Quality_Kit, хуки aqk / aqk-doctor) либо обычный .git/hooks/pre-push.",
128
+ haveAlready: (n) => `Проверки, которые у вас УЖЕ ЕСТЬ (${n}) — прочитаны в ваших файлах, не выдуманы:`,
129
+ haveAlreadyHow: (line) => `объявите их — и держать будет машина, а не ваше внимание. В .aqk.yml, в gates: ${line}`,
108
130
  total: "Итого:",
109
131
  totalHeld: (n) => `держит машина ${n}`,
110
132
  totalTodo: (n) => `применимо но не поставлено ${n}`,
@@ -121,6 +143,8 @@ export const ru = {
121
143
  `либо почини и убери из advisory, либо признай, что правила нет.`,
122
144
  runHeading: "Прогон объявленных гейтов",
123
145
  timeout: "не уложился в 5 минут",
146
+ running: (i, n) => `[${i}/${n}] идёт…`,
147
+ proving: "проверяю, что гейты ловят брак на своих образцах…",
124
148
  exitCode: (code) => `код ${code}`,
125
149
  moreLines: (n) => `… и ещё ${n} строк`,
126
150
  declaredNotRun: (n) => `${n} гейтов объявлено, но не запускалось.`,
@@ -192,6 +216,7 @@ export const ru = {
192
216
  has_docker: ["нет Dockerfile или compose", "docker уже есть"],
193
217
  has_deps: ["не видно файла зависимостей", "зависимости объявлены"],
194
218
  has_tests: ["не видно тестов", "тесты есть"],
219
+ has_biome: ["линтер не Biome", "линтер проекта — Biome"],
195
220
  has_env: ["нет файла окружения", "файл окружения есть"],
196
221
  has_agent_config: ["агента здесь не настраивали", "настройки агента есть"],
197
222
  has_agent_entry: ["свода для агента здесь нет", "свод для агента есть"],