agent-quality-kit 0.11.0 → 0.12.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 CHANGED
@@ -46,6 +46,8 @@ Go, Rust, Java, Ruby, PHP, C#, Kotlin, Swift, Scala. Where the project already h
46
46
  says so out loud when it falls back.
47
47
 
48
48
  **Requirements:** Node 18+ and an `sh` shell. Present on macOS, Linux and WSL; Git Bash on Windows.
49
+ On Windows the gates run through Git Bash even when `bash` on your PATH is the WSL stub in
50
+ `System32` — it is found next to `git.exe`; set `AQK_BASH` to point elsewhere.
49
51
 
50
52
  ### One movement, and everything follows from it
51
53
 
@@ -345,7 +347,7 @@ Already using [pre-commit](https://pre-commit.com)? Three lines in the file you
345
347
  ```yaml
346
348
  repos:
347
349
  - repo: https://github.com/arsen-ask-lx/Agent_Quality_Kit
348
- rev: v0.11.0
350
+ rev: v0.12.0
349
351
  hooks:
350
352
  - id: aqk # runs what the repository declares; blocks below AQK-1
351
353
  # - id: aqk-doctor # read-only: the level and what is missing, blocks nothing
@@ -366,7 +368,7 @@ layer AQK adds.
366
368
  [![on the GitHub Marketplace](https://img.shields.io/badge/GitHub%20Marketplace-Agent%20Quality%20Kit-2ea44f?logo=github)](https://github.com/marketplace/actions/agent-quality-kit-aqk)
367
369
 
368
370
  ```yaml
369
- - uses: arsen-ask-lx/Agent_Quality_Kit@v0.11.0
371
+ - uses: arsen-ask-lx/Agent_Quality_Kit@v0.12.0
370
372
  with:
371
373
  min: 1 # the build fails below AQK-1, or if any declared gate failed
372
374
  ```
package/README.ru.md CHANGED
@@ -48,6 +48,8 @@ TypeScript, Go, Rust, Java, Ruby, PHP, C#, Kotlin, Swift, Scala. Там, где
48
48
  сообщает, когда откатилась на переносимый.
49
49
 
50
50
  **Требуется:** Node 18+ и оболочка `sh`. Есть на macOS, Linux и в WSL; на Windows — Git Bash.
51
+ На Windows гейты идут через Git Bash, даже если `bash` в PATH — заглушка WSL из `System32`:
52
+ он ищется рядом с `git.exe`; другой путь задаётся `AQK_BASH`.
51
53
 
52
54
  ### Одно движение, из которого следует всё остальное
53
55
 
@@ -348,7 +350,7 @@ aqk badge --check # в конвейере: код 1 в тот день, ког
348
350
  ```yaml
349
351
  repos:
350
352
  - repo: https://github.com/arsen-ask-lx/Agent_Quality_Kit
351
- rev: v0.11.0
353
+ rev: v0.12.0
352
354
  hooks:
353
355
  - id: aqk # запускает объявленное; роняет коммит ниже AQK-1
354
356
  # - id: aqk-doctor # только осмотр: уровень и чего не хватает, ничего не роняет
@@ -367,7 +369,7 @@ repos:
367
369
  [![в GitHub Marketplace](https://img.shields.io/badge/GitHub%20Marketplace-Agent%20Quality%20Kit-2ea44f?logo=github)](https://github.com/marketplace/actions/agent-quality-kit-aqk)
368
370
 
369
371
  ```yaml
370
- - uses: arsen-ask-lx/Agent_Quality_Kit@v0.11.0
372
+ - uses: arsen-ask-lx/Agent_Quality_Kit@v0.12.0
371
373
  with:
372
374
  min: 1 # сборка падает ниже AQK-1 или если упал любой объявленный гейт
373
375
  ```
@@ -4,6 +4,11 @@ intent_en: a function does not grow past the complexity you can hold in your hea
4
4
  trigger:
5
5
  always: true
6
6
 
7
+ # Домашние страницы готовых инструментов, которые зовут рецепты ниже. Адреса сверены
8
+ # по реестру github 2026-09-10, а не написаны по памяти: три выдуманных адреса чужих
9
+ # репозиториев — записанная шишка комплекта.
10
+ tool: https://github.com/astral-sh/ruff · https://github.com/eslint/eslint
11
+
7
12
  recipes:
8
13
  # Переносимая мера грубая — глубина вложенности. Готовые правила считают ветвления и
9
14
  # инструкции, это точнее; см. kit/docs/ready-made-rules.md
@@ -10,6 +10,11 @@ trigger:
10
10
  # Переносимого рецепта здесь нет намеренно: чтобы понять, вызывают ли функцию, нужен граф
11
11
  # вызовов, а не поиск по тексту. Это тот случай, когда без готового инструмента не обойтись —
12
12
  # и он есть под каждый распространённый стек.
13
+ # Домашние страницы готовых инструментов, которые зовут рецепты ниже. Адреса сверены
14
+ # по реестру github 2026-09-10, а не написаны по памяти: три выдуманных адреса чужих
15
+ # репозиториев — записанная шишка комплекта.
16
+ tool: https://github.com/jendrikseipp/vulture · https://github.com/webpro-nl/knip
17
+
13
18
  recipes:
14
19
  python: vulture --min-confidence 60 {dir}
15
20
  typescript: npx --yes knip@6 --directory {dir}
@@ -4,6 +4,11 @@ intent_en: the same code does not multiply into copies across the project
4
4
  trigger:
5
5
  files_gt: 20
6
6
 
7
+ # Домашние страницы готовых инструментов, которые зовут рецепты ниже. Адреса сверены
8
+ # по реестру github 2026-09-10, а не написаны по памяти: три выдуманных адреса чужих
9
+ # репозиториев — записанная шишка комплекта.
10
+ tool: https://github.com/kucherenko/jscpd · https://github.com/pylint-dev/pylint
11
+
7
12
  recipes:
8
13
  any: bash {gate}/check.sh {dir}
9
14
  # jscpd умеет и Python, и TypeScript одним прогоном и считает похожесть, а не совпадение.
@@ -11,6 +11,11 @@ trigger:
11
11
  # печать в примерах документации, в тестах, в бенчмарках и в выводе командной строки.
12
12
  # Отличить их от отладки можно только разбором кода, а не поиском по тексту, — и это
13
13
  # ровно то, что уже делают ruff и eslint.
14
+ # Домашние страницы готовых инструментов, которые зовут рецепты ниже. Адреса сверены
15
+ # по реестру github 2026-09-10, а не написаны по памяти: три выдуманных адреса чужих
16
+ # репозиториев — записанная шишка комплекта.
17
+ tool: https://github.com/astral-sh/ruff · https://github.com/eslint/eslint
18
+
14
19
  recipes:
15
20
  python: ruff check -q --output-format=concise --select T20 {dir}
16
21
  typescript: eslint --no-config-lookup --ignore-pattern 'gates/*/red/**' --ignore-pattern 'gates/*/green/**' --rule '{"no-console":"error"}' {dir}
@@ -4,7 +4,27 @@ intent_en: keys, passwords and private keys do not end up in the code
4
4
  trigger:
5
5
  always: true
6
6
 
7
+ # Домашние страницы готовых инструментов, которые зовут рецепты ниже. Адреса сверены
8
+ # по реестру github 2026-09-10, а не написаны по памяти: три выдуманных адреса чужих
9
+ # репозиториев — записанная шишка комплекта.
10
+ tool: https://github.com/gitleaks/gitleaks
11
+
7
12
  recipes:
13
+ # Готовый аналог сильнее нашего, и об этом прямо сказано в README записи: `gitleaks` знает
14
+ # сотни форматов токенов и умеет читать историю, а не только рабочее дерево. Наша проверка —
15
+ # запасная, для тех, кто не хочет ставить лишний бинарь.
16
+ #
17
+ # Ключ `native`, а не язык: секреты ищутся в любом файле, и раскладывать одну команду по
18
+ # восьми языкам значило бы завести ровно тот повтор, против которого у нас есть гейт.
19
+ #
20
+ # `dir` — рабочее дерево; чтение истории (`gitleaks git`) сюда не берём: гейт обязан отвечать
21
+ # про ТЕКУЩЕЕ состояние, а прошлое лечится не проверкой, а ротацией ключа.
22
+ #
23
+ # ОСТОРОЖНО С КОДОМ ВОЗВРАТА: у `gitleaks` 1 означает «нашёл ЛИБО сломался» — по документации,
24
+ # проверено 2026-09-10. То есть для него «находка» и «сбой инструмента» неразличимы, и это
25
+ # ровно тот класс, который мы разделяем у себя. Пока принимаем как есть и говорим об этом
26
+ # вслух, а не делаем вид, что кодов три.
27
+ native: gitleaks dir --no-banner {dir}
8
28
  any: bash {gate}/check.sh {dir}
9
29
 
10
30
  proof: kit/docs/ai/project-baseline.md, пункт 5 — «секреты не хранятся в коде вообще: ни в истории, ни в примерах, ни в тестах»
@@ -7,6 +7,11 @@ intent_en: an error is not silently swallowed — it is handled and logged, or r
7
7
  trigger:
8
8
  langs: python, javascript, typescript, go
9
9
 
10
+ # Домашние страницы готовых инструментов, которые зовут рецепты ниже. Адреса сверены
11
+ # по реестру github 2026-09-10, а не написаны по памяти: три выдуманных адреса чужих
12
+ # репозиториев — записанная шишка комплекта.
13
+ tool: https://github.com/astral-sh/ruff · https://github.com/eslint/eslint · https://github.com/kisielk/errcheck
14
+
10
15
  recipes:
11
16
  # BLE — ловля голого исключения, TRY400 — запись без трейса, SIM105 — перехват ради тишины.
12
17
  python: ruff check -q --output-format=concise --select BLE,TRY400,SIM105 {dir}
@@ -4,6 +4,11 @@ intent_en: "fix later" markers are absent from finished code — a filed task re
4
4
  trigger:
5
5
  always: true
6
6
 
7
+ # Домашние страницы готовых инструментов, которые зовут рецепты ниже. Адреса сверены
8
+ # по реестру github 2026-09-10, а не написаны по памяти: три выдуманных адреса чужих
9
+ # репозиториев — записанная шишка комплекта.
10
+ tool: https://github.com/astral-sh/ruff · https://github.com/eslint/eslint
11
+
7
12
  recipes:
8
13
  any: bash {gate}/check.sh {dir}
9
14
  # Готовое правило точнее самописного и не требует поддержки.
package/llms.txt CHANGED
@@ -70,7 +70,7 @@ Zero runtime dependencies. Node 18+ and an `sh` shell. MIT.
70
70
  the files `init` writes are owned by root, so you cannot edit your own manifest. Debian-based
71
71
  on purpose: the gates are `sh`, `grep`, `awk`, `find` — under alpine's busybox they behave
72
72
  differently, and an image where the gates behave differently is worse than no image
73
- - As a GitHub Action: `uses: arsen-ask-lx/Agent_Quality_Kit@v0.11.0` with `min: 1`
73
+ - As a GitHub Action: `uses: arsen-ask-lx/Agent_Quality_Kit@v0.12.0` with `min: 1`
74
74
  (https://github.com/marketplace/actions/agent-quality-kit-aqk)
75
75
 
76
76
  ## What makes it different
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-quality-kit",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "Turns the rules an agent is supposed to follow into commands with exit codes, and reports which of them actually run. Zero dependencies.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -80,7 +80,9 @@ function contextBlock(state, T = L.context) {
80
80
  if (pr.state === "never") out.push(T.probeNever);
81
81
  else if (pr.state === "off") out.push(T.probeOff);
82
82
  else if (pr.state === "unknown") out.push(T.probeUnknown);
83
- else if (pr.blind > 0) out.push(T.probeBlind(pr.blind, pr.state === "stale" ? pr.behind : 0));
83
+ // Имена агенту они нужнее числа: «один класс» не говорит, какой файл трогать осторожно.
84
+ else if (pr.blind > 0) out.push(T.probeBlind(pr.blind, pr.state === "stale" ? pr.behind : 0,
85
+ (pr.classes || []).map((b) => `${b.slug} (${b.file})`).join(", ")));
84
86
  else out.push(T.probeClean(pr.state === "stale" ? pr.behind : 0));
85
87
  }
86
88
 
@@ -5,15 +5,16 @@ import { join, resolve } from "node:path";
5
5
  import { spawnSync } from "node:child_process";
6
6
  import { scopeOutput, splitAdvice, changedFiles } from "../lib/scope.mjs";
7
7
  import { CWD, PKG_ROOT, TARGET_DIR, MANIFEST, SELF, c, exists, die } from "../lib/core.mjs";
8
- import { cmdProbe, probeStatus } from "./probe.mjs";
8
+ import { cmdProbe, probeStatus, blindAdvice } from "./probe.mjs";
9
9
  import { readManifest, assessLevel, unknownKeys, KNOWN_KEYS, advisorySet, layoutChecks, coversOf, coversUnproven, unparsedLines } from "../lib/manifest.mjs";
10
10
  import { proveGates } from "../lib/prove.mjs";
11
- import { detectFacts, readCatalog, triggerVerdict, browserServerAdvice } from "../lib/repo.mjs";
11
+ import { detectFacts, readCatalog, triggerVerdict, browserServerAdvice, startWith } from "../lib/repo.mjs";
12
+ import { proposeGates, ADOPT_FILES, ADOPT_SCRIPTS } from "../lib/adopt.mjs";
12
13
  import { assessBaseline, DEP_FILES, BASELINE_TOTAL } from "../lib/baseline.mjs";
13
14
  import { L } from "../i18n/index.mjs";
14
15
  import { countArbiters } from "./context.mjs";
15
16
  import { beginBrief, finishBrief } from "../lib/brief.mjs";
16
- import { declaredGates, sinceRef, runGates } from "../lib/run.mjs";
17
+ import { declaredGates, sinceRef, runGates, progress } from "../lib/run.mjs";
17
18
 
18
19
  // Обязательный минимум проекта — прогоном, а не по памяти. До сих пор это было единственное
19
20
  // место, где комплект просил верить на слово, что человек прочитал методичку и сверился.
@@ -50,7 +51,7 @@ async function reportBaseline(man, facts) {
50
51
  );
51
52
  }
52
53
 
53
- async function reportCatalog(man, facts) {
54
+ async function reportCatalog(man, facts, probe = null) {
54
55
  const catalog = await readCatalog();
55
56
  if (!catalog.length) return;
56
57
 
@@ -117,6 +118,70 @@ async function reportCatalog(man, facts) {
117
118
  console.log(c.dim(`\n ${L.doctor.notApplicable(skip.length)}`));
118
119
  for (const [rec, why] of skip) console.log(c.dim(` · ${rec.slug.padEnd(22)} ${why}`));
119
120
  }
121
+ // ЧТО У ВАС УЖЕ ЕСТЬ — до итога и до списка крестов. Комплект, поставленный в проект с
122
+ // eslint, mocha и конвейером, показывал двадцать крестов и «держит машина 0»: мы считали
123
+ // только СВОИ записи, а чужие проверки не читали вовсе. С точки зрения владельца это
124
+ // неправда, и первое, что он видел, было обвинением. Предлагаем, а не вписываем: гейт в
125
+ // чужом манифесте без спроса — наше решение в чужом файле.
126
+ if (!declaredGates(man).length) {
127
+ const files = {};
128
+ for (const n of ADOPT_FILES) {
129
+ try { files[n] = await readFile(join(CWD, n), "utf8"); } catch { /* нет — и ладно */ }
130
+ }
131
+ for (const n of ADOPT_SCRIPTS) if (await exists(join(CWD, n))) files[n] = "";
132
+ const found = proposeGates(files);
133
+ if (found.length) {
134
+ console.log(`\n ${c.bold(L.doctor.haveAlready(found.length))}`);
135
+ for (const g of found) {
136
+ console.log(` ${c.green("✔")} ${g.name.padEnd(12)} ${c.dim(`${g.cmd} ← ${g.source}`)}`);
137
+ }
138
+ console.log(c.dim(` ${L.doctor.haveAlreadyHow(found.map((g) => `${g.name}: "${g.cmd}"`).join(" "))}`));
139
+ }
140
+ }
141
+
142
+ // ЧТО ВАШИ ПРОВЕРКИ ПРОПУСТИЛИ. Проба знала имена непойманных классов и писала в отметку одно
143
+ // число; человек в `doctor` не видел ничего. Это самое конкретное, что мы знаем о проекте, —
144
+ // не «хорошая практика», а брак, подсаженный в ЕГО файл и ЕГО проверками не замеченный, —
145
+ // поэтому стоит выше списка «с чего начать». Читается из файла: ничего не запускает.
146
+ const blindOnes = (probe?.classes || []).map((b) => [b, catalog.find((r) => r.slug === b.slug)]).filter(([, r]) => r);
147
+ if (blindOnes.length) {
148
+ console.log(`\n ${c.yellow("⚠")} ${c.bold(L.doctor.blindHeading(probe.behind))}`);
149
+ for (const [b, rec] of blindOnes) {
150
+ // Три случая, и сливать их нельзя. Гейт стоял и проба его ГОНЯЛА — «стоит, но здесь не
151
+ // ловит», самое ценное. Гейт объявлен, но проба его не гоняла (поставлен позже или
152
+ // медленный) — «поймает ли, покажет следующая», а не «пойман». Гейта нет — совет.
153
+ const ranIt = probe.ran?.has(rec.slug);
154
+ const now = facts.gateKeys.includes(rec.slug);
155
+ console.log(` ${now && !ranIt ? c.dim("~") : c.red("✘")} ${rec.slug.padEnd(22)} ${c.dim(`${rec.intent || ""} ← ${b.file}`)}`);
156
+ if (ranIt) { console.log(c.dim(` ${L.doctor.blindRan(rec.slug)}`)); continue; }
157
+ if (now) { console.log(c.dim(` ${L.doctor.blindInstalled}`)); continue; }
158
+ const adv = blindAdvice(rec, facts, {});
159
+ if (adv.command) console.log(c.dim(` ${L.doctor.startCmd(adv.command)}`));
160
+ else console.log(c.dim(` ${L.doctor.install(`${SELF} add ${rec.slug}`)}`));
161
+ }
162
+ console.log(c.dim(` ${L.doctor.blindMore(`${SELF} probe`)}`));
163
+ } else if (probe?.state === "never" && declaredGates(man).length) {
164
+ console.log(c.dim(`\n ${L.doctor.probeNever(`${SELF} probe`)}`));
165
+ }
166
+
167
+ // С ЧЕГО НАЧАТЬ. Двадцать одинаковых крестов — это ноль требований: закрывают первое
168
+ // попавшееся или не закрывают ничего. Порядок не по нашему вкусу: сперва то, что родилось из
169
+ // настоящего отказа И закрывается одной готовой командой.
170
+ if (todo.length > 3) {
171
+ const first = startWith(todo, facts, 3);
172
+ console.log(`\n ${c.bold(L.doctor.startWith)}`);
173
+ for (const rec of first) {
174
+ const adv = blindAdvice(rec, facts, {});
175
+ console.log(` ${c.yellow("→")} ${rec.slug.padEnd(22)} ${c.dim(rec.intent || "")}`);
176
+ if (adv.command) console.log(c.dim(` ${L.doctor.startCmd(adv.command)}`));
177
+ if (adv.tool) console.log(c.dim(` ${L.doctor.startTool(adv.tool)}`));
178
+ }
179
+ // Одна проверка руками — это разовый героизм. Сказать про хук здесь, а не в конце: человек
180
+ // читает первые строки и закрывает, а именно сейчас у него в руках список того, что стоит
181
+ // повесить перед пушем.
182
+ console.log(c.dim(`\n ${L.doctor.startHook}`));
183
+ }
184
+
120
185
  console.log(
121
186
  `\n ${c.bold(L.doctor.total)} ${L.doctor.totalHeld(held.length)}, ${L.doctor.totalTodo(c.yellow(todo.length))}, ` +
122
187
  (byOther.length ? `${L.doctor.totalCovered(byOther.length)}, ` : "") +
@@ -227,7 +292,11 @@ async function cmdDoctor() {
227
292
  // Доказательство считается только при прогоне: узнать, ловит ли гейт брак, нельзя иначе как
228
293
  // запустив его по образцу. Без прогона ступени со второй помечаются «не доказано» — это
229
294
  // честнее, чем показывать их выполненными по наличию папок.
295
+ // Доказательство — секунды тишины до первой строки уровня; строка «идёт» их называет.
296
+ const bar = progress();
297
+ if (process.argv.includes("--run")) bar.show(c.dim(` ⋯ ${L.doctor.proving}`));
230
298
  const proof = process.argv.includes("--run") ? await proveGates(man) : null;
299
+ bar.clear();
231
300
  const { reached, steps } = await assessLevel(man, proof);
232
301
 
233
302
  console.log(c.bold(`\n ${L.doctor.levelHeading}\n`));
@@ -274,7 +343,10 @@ async function cmdDoctor() {
274
343
  await reportBaseline(man, facts);
275
344
  process.exit(0);
276
345
  }
277
- const cat = (await reportCatalog(man, facts)) || { held: 0, todo: 0, todoRecs: [] };
346
+ // Состояние пробы из файла отметки, миллисекунды. Нет его блок про пробу просто молчит.
347
+ let probe = null;
348
+ try { probe = await probeStatus(); } catch { /* пробы нет — и ладно */ }
349
+ const cat = (await reportCatalog(man, facts, probe)) || { held: 0, todo: 0, todoRecs: [] };
278
350
 
279
351
  // «Объявлен» ≠ «работает». Без --run говорим это вслух, а не молчим.
280
352
  const wantRun = process.argv.includes("--run");
@@ -15,6 +15,7 @@ import {
15
15
  } from "../lib/repo.mjs";
16
16
  import { GATE_YML_TEMPLATE, CHECK_SH_TEMPLATE, README_TEMPLATE } from "../lib/templates.mjs";
17
17
  import { L } from "../i18n/index.mjs";
18
+ import { gateCommand } from "../lib/execution.mjs";
18
19
 
19
20
  // Ставит гейт из каталога в проект. Проверка КОПИРУЕТСЯ в репозиторий, а не остаётся
20
21
  // ссылкой в пакет: при установке через npx пакет временный, и завтра команда в манифесте
@@ -230,7 +231,7 @@ async function cmdRatchet(args) {
230
231
 
231
232
  // Снимок текущих нарушений — это и есть долг. Ключ без номера строки: правка соседней
232
233
  // строки не должна читаться как новое нарушение.
233
- const r = spawnSync(inner, { shell: true, cwd: CWD, encoding: "utf8", timeout: 300000 });
234
+ const r = spawnSync(gateCommand(inner), { shell: true, cwd: CWD, encoding: "utf8", timeout: 300000 });
234
235
  if (r.status === 127 || (r.error && r.error.code === "ENOENT")) {
235
236
  die(L.ratchet.notRunnable(slug, inner));
236
237
  }
@@ -438,7 +439,7 @@ async function cmdWhy(args) {
438
439
 
439
440
  // --- 3. объявлен: спрашиваем у него самого ---------------------------------
440
441
  console.log(c.dim(` ${L.why.declaredAs(cmd)}`));
441
- const r = spawnSync(String(cmd), { shell: true, cwd: CWD, encoding: "utf8", timeout: 300000 });
442
+ const r = spawnSync(gateCommand(String(cmd)), { shell: true, cwd: CWD, encoding: "utf8", timeout: 300000 });
442
443
  const ci = await runsInCi(slug, String(cmd));
443
444
 
444
445
  if (r.status === 127 || (r.error && r.error.code === "ENOENT")) {
@@ -22,14 +22,16 @@
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
29
  import { fixHotspots, probeSummary, probeVerdictPaired, countProbe } from "../lib/history.mjs";
29
30
  import { detectFacts, readCatalog, triggerVerdict } from "../lib/repo.mjs";
30
31
  import { CWD, GATES_SRC, TARGET_DIR, c, SELF, exists } from "../lib/core.mjs";
31
- import { probeState, probeEvery, PROBE_EVERY } from "../lib/cadence.mjs";
32
+ import { probeState, probeEvery, PROBE_EVERY, blindLines, parseBlind, parseRan } from "../lib/cadence.mjs";
32
33
  import { L } from "../i18n/index.mjs";
34
+ import { gateCommand } from "../lib/execution.mjs";
33
35
 
34
36
  // Тот же набор расширений, что у привязки доказательства к дифу. Список один на программу:
35
37
  // второй через месяц разошёлся бы с первым.
@@ -137,6 +139,70 @@ function extAlternatives(ext) {
137
139
  return fam ? [ext, ...fam.filter((e) => e !== ext)] : [ext];
138
140
  }
139
141
 
142
+ // Показать САМ ОБРАЗЕЦ, а не пересказ. «Класс не прикрыт» остаётся словами, пока человек не
143
+ // увидел, что именно мы подсадили в его файл.
144
+ //
145
+ // Первая версия печатала одну «показательную» строку — и угадывала плохо: у мёртвого кода дефект
146
+ // во ВТОРОЙ функции, у отладочной печати во второй строке тела. Угадывать не надо: образцы
147
+ // каталога маленькие по норме, и четырёх строк хватает, чтобы стало видно. Комментарии
148
+ // выброшены: в наших образцах они объясняют замысел коллеге, а не показывают дефект.
149
+ function sampleLines(path, max = 4) {
150
+ let text = "";
151
+ try { text = readFileSync(path, "utf8"); } catch { return []; }
152
+ return text.split("\n")
153
+ .map((l) => l.replace(/\s+$/, ""))
154
+ .filter((l) => l.trim() && !/^\s*(#|\/\/|\/\*|\*|--|<!--)/.test(l))
155
+ .slice(0, max)
156
+ .map((l) => l.slice(0, 88));
157
+ }
158
+
159
+ // Совет по НЕПОКРЫТОМУ классу: команда, которую можно вставить прямо сейчас.
160
+ //
161
+ // Проба находит настоящие дыры и печатала про них «close it: aqk add <имя>» — то есть «поставь
162
+ // нашу штуку». Человек, впервые увидевший комплект, закрывает окно. А готовая однострочная
163
+ // команда под его стек У НАС УЖЕ ЛЕЖИТ в `recipes` записи каталога; мы её не показывали.
164
+ //
165
+ // Замер руками на `requests` (самый скачиваемый python-пакет) 2026-09-10: в
166
+ // `src/requests/utils.py` — 75 коммитов-починок; дописана функция с `except Exception: pass`;
167
+ // их собственные `ruff` и `pytest` дали 0 и на чистой копии, и на подсаженной. Строка, которая
168
+ // поймала бы это, лежала в нашем каталоге всё это время.
169
+ //
170
+ // Переносимый рецепт (`any`) в совет НЕ идёт: он зовёт файл из комплекта, и человеку без
171
+ // комплекта вставить его некуда. Нет родного рецепта под стек — команды нет, и это честнее
172
+ // выдуманной.
173
+ function blindAdvice(entry, facts, hot = {}) {
174
+ const recipes = entry?.recipes && typeof entry.recipes === "object" ? entry.recipes : {};
175
+ // `langs` приходит МНОЖЕСТВОМ, а не массивом — `Array.isArray` тихо давал пустой список, и
176
+ // совет не печатался вовсе. Поймано на живом `requests`: langs = Set(1) { python }.
177
+ const langs = facts?.langs ? [...facts.langs] : [];
178
+ // Тот же порядок, что у `pickRecipe`: свой язык → безъязыковой родной → ничего. Переносимый
179
+ // (`any`) сюда не идёт никогда: он зовёт файл из комплекта, и человеку без комплекта вставить
180
+ // его некуда.
181
+ let cmd = null;
182
+ for (const key of [...langs, "native"]) {
183
+ const r = recipes[key];
184
+ if (!r || /\{gate\}/.test(r)) continue;
185
+ cmd = String(r).replace(/\{dir\}/g, ".")
186
+ // Вычистить то, что относится к НАМ, а не к его проекту. Исключение наших красных
187
+ // образцов нужно УСТАНОВЛЕННОМУ гейту — рядом с ним лежат образцы. Человеку, который
188
+ // команду только копирует, этих каталогов не существует, и флаги про них подрывают
189
+ // доверие: инструмент говорит про чужое хозяйство вместо его кода.
190
+ .replace(/\s--ignore-pattern\s+'[^']*gates\/[^']*'/g, "")
191
+ .replace(/\s--ignore-paths=?\s*'[^']*gates\/[^']*'/g, "")
192
+ .replace(/\s+/g, " ")
193
+ .trim();
194
+ break;
195
+ }
196
+ // Адрес — того инструмента, которым команда начинается: поле `tool` общее на все языки, и
197
+ // python-проекту показывалась ссылка на eslint. Первые три слова, а не одно: `npx knip`,
198
+ // `python -m vulture`. Не совпало — весь список: лишняя ссылка лучше, чем ни одной.
199
+ const urls = entry?.tool ? String(entry.tool).split(/\s+·\s+/) : [];
200
+ const head = cmd ? cmd.split(" ").slice(0, 3) : [];
201
+ const own = urls.find((u) => head.includes(u.replace(/\/+$/, "").split("/").pop()));
202
+ const tool = own ?? (entry?.tool ? String(entry.tool) : null);
203
+ return { command: cmd, tool, file: hot.file ?? null, fixes: hot.fixes ?? null, slug: entry?.slug ?? null };
204
+ }
205
+
140
206
  // Красный образец записи, подходящий по расширению горячего файла. Расширение обязано
141
207
  // совпадать: питоновский образец в проекте на TypeScript не проверит ничего, а покажет
142
208
  // «не прикрыто» — ложная тревога того же класса, что молчащий гейт, только наоборот.
@@ -224,7 +290,7 @@ function runGates(gates, sandbox, { stopOnRed = false } = {}) {
224
290
  const out = [];
225
291
  for (const [name, cmd] of gates) {
226
292
  const t0 = Date.now();
227
- const r = spawnSync(cmd, { shell: true, cwd: sandbox, encoding: "utf8", timeout: 120000 });
293
+ const r = spawnSync(gateCommand(cmd), { shell: true, cwd: sandbox, encoding: "utf8", timeout: 120000 });
228
294
  const code = r.status === null ? 2 : r.status;
229
295
  out.push({ name, code, ms: Date.now() - t0 });
230
296
  if (stopOnRed && code === 1) break;
@@ -338,6 +404,19 @@ async function cmdProbe(args, { auto = false } = {}) {
338
404
  console.log(` ${c.green("✔")} ${e.intent.padEnd(48)} ${c.dim(P.caught(caught.join(", ")))}`);
339
405
  } else if (verdict === "blind") {
340
406
  console.log(` ${c.red("✘")} ${e.intent.padEnd(48)} ${c.red(P.blind)}`);
407
+ // Объяснить, а не назвать. Три строки, каждая отвечает на свой вопрос человека:
408
+ // «почему именно здесь», «что вы вообще подсадили» и «что мне сделать ПРЯМО СЕЙЧАС».
409
+ // Последняя обязана работать БЕЗ комплекта: польза до установки — единственный
410
+ // способ заслужить установку.
411
+ const adv = blindAdvice(e, facts, { file: rel, fixes });
412
+ console.log(c.dim(` ${P.blindWhere(rel, fixes)}`));
413
+ const lines = sampleLines(sample);
414
+ if (lines.length) {
415
+ console.log(c.dim(` ${P.blindWhat}`));
416
+ for (const l of lines) console.log(c.dim(` ${l}`));
417
+ }
418
+ if (adv.command) console.log(` ${c.yellow(P.blindFix(adv.command))}`);
419
+ if (adv.tool) console.log(c.dim(` ${P.blindTool(adv.tool)}`));
341
420
  console.log(c.dim(` ${P.install(`${SELF} add ${e.slug}`)}`));
342
421
  } else {
343
422
  console.log(` ${c.dim("~")} ${c.dim(e.intent.padEnd(48))} ${c.dim(P.unknown)}`);
@@ -362,7 +441,11 @@ async function cmdProbe(args, { auto = false } = {}) {
362
441
 
363
442
  // Отметка нужна не для отчёта, а для КАДЕНЦИИ: по ней следующий прогон поймёт, что пора.
364
443
  // Без неё команда снова становится тем, о чём надо вспомнить.
365
- await writeMark(commitCount(), blind, hot.map(({ path: p2, fixes }) => `- ${p2} (${P.fixes(fixes)})`));
444
+ await writeMark(commitCount(), blind, [
445
+ `ran: ${probeGates.map(([name]) => name).join(" ")}`, "",
446
+ ...blindLines(records), "",
447
+ ...hot.map(({ path: p2, fixes }) => `- ${p2} (${P.fixes(fixes)})`),
448
+ ]);
366
449
  } finally {
367
450
  await rm(sandbox, { recursive: true, force: true });
368
451
  }
@@ -378,7 +461,8 @@ async function probeStatus() {
378
461
  const every = probeEvery(man);
379
462
  if (every === null) return { state: "unknown", behind: null, badEvery: String(man?.probe) };
380
463
  if (every === 0) return { state: "off", behind: null };
381
- return probeState(await readMark(), commitCount(), every);
464
+ const mark = await readMark();
465
+ return { ...probeState(mark, commitCount(), every), classes: parseBlind(mark?.text), ran: parseRan(mark?.text) };
382
466
  }
383
467
 
384
- export { cmdProbe, probeStatus, probeableGates, gatesState, extAlternatives, planProbeGates, isCode };
468
+ export { cmdProbe, probeStatus, probeableGates, gatesState, extAlternatives, planProbeGates, blindAdvice, isCode };
@@ -21,6 +21,7 @@ import { join } from "node:path";
21
21
  import { CWD, MANIFEST, SELF, c, exists } from "../lib/core.mjs";
22
22
  import { readManifest, unparsedLines, gateRequires } from "../lib/manifest.mjs";
23
23
  import { whichSync } from "../lib/repo.mjs";
24
+ import { gitBash } from "../lib/execution.mjs";
24
25
  import { updateWanted } from "../lib/brief.mjs";
25
26
  import { L } from "../i18n/index.mjs";
26
27
 
@@ -94,7 +95,11 @@ async function cmdVitals() {
94
95
  for (const [gate, cmd] of Object.entries(gates)) {
95
96
  const prog = progOf(cmd);
96
97
  if (!prog || seen.has(prog)) continue;
97
- seen.set(prog, { gate, prog, found: Boolean(whichSync(prog)) });
98
+ // На Windows слово `bash` в PATH — часто заглушка WSL, и «найден» было бы неправдой: гейт
99
+ // запустится через Git Bash (gateCommand) или не запустится вовсе. Спрашиваем того же, кого
100
+ // спросит прогон, — иначе vitals и doctor --run снова разойдутся.
101
+ const found = prog === "bash" && process.platform === "win32" ? Boolean(gitBash()) : Boolean(whichSync(prog));
102
+ seen.set(prog, { gate, prog, found });
98
103
  }
99
104
 
100
105
  // Первого слова мало. Запись каталога бывает обёрткой: команда начинается с `bash`, который
@@ -70,8 +70,9 @@ const enDocs = {
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" +
@@ -265,6 +265,10 @@ export const enGates = {
265
265
  caught: (names) => `caught by: ${names}`,
266
266
  blind: "NOTHING CATCHES IT",
267
267
  unknown: "nothing to check with — the gate did not run (delegated tool missing)",
268
+ blindWhere: (file, fixes) => `where: ${file} — ${fixes} fix commits in its history`,
269
+ blindWhat: "what we planted into your file:",
270
+ blindFix: (cmd) => `catch it right now, no kit needed: ${cmd}`,
271
+ blindTool: (url) => `the tool: ${url}`,
268
272
  install: (cmd) => `close it: ${cmd}`,
269
273
  noSampleFor: (ext) => `the catalogue has no red sample for "${ext}" — nothing to check with`,
270
274
  noGates: (cmd) => `no gates declared — nothing to probe with. First: ${cmd}`,
package/tool/i18n/en.mjs CHANGED
@@ -104,6 +104,17 @@ export const en = {
104
104
  `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
105
  coversUnprovenHow: (cmd) => `settle it: add those rules to the linter, or install the entry — ${cmd}`,
106
106
  totalCovered: (n) => `held by another arbiter ${n}`,
107
+ blindHeading: (behind) => `The probe${behind ? ` (${behind} commits ago)` : ""} planted defects in your files — your checks did NOT catch them:`,
108
+ blindRan: (g) => `gate ${g} is declared and was run — and still missed the defect in this file`,
109
+ blindInstalled: "declared, but the probe did not run this gate (added later or too slow) — the next probe will show whether it catches",
110
+ blindMore: (cmd) => `what was planted and where — ${cmd}`,
111
+ 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 files are not touched).`,
112
+ startWith: "Start with these three — born from a real failure, and each closes with one ready command:",
113
+ startCmd: (cmd) => `one line, no kit needed: ${cmd}`,
114
+ startTool: (url) => `the tool: ${url}`,
115
+ 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.",
116
+ haveAlready: (n) => `Checks you ALREADY have (${n}) — found in your own files, not invented:`,
117
+ haveAlreadyHow: (line) => `declare them and a machine holds them, not your attention. In .aqk.yml, under gates: ${line}`,
107
118
  total: "Total:",
108
119
  totalHeld: (n) => `held by a machine ${n}`,
109
120
  totalTodo: (n) => `applicable but not installed ${n}`,
@@ -120,6 +131,8 @@ export const en = {
120
131
  `either fix them and drop them from advisory, or admit the rule does not exist.`,
121
132
  runHeading: "Running the declared gates",
122
133
  timeout: "did not finish within 5 minutes",
134
+ running: (i, n) => `[${i}/${n}] running…`,
135
+ proving: "checking that the gates catch defects on their own samples…",
123
136
  exitCode: (code) => `exit ${code}`,
124
137
  moreLines: (n) => `… and ${n} more lines`,
125
138
  declaredNotRun: (n) => `${n} gates declared, but never run.`,
@@ -72,8 +72,9 @@ const ruDocs = {
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
  "Проба покрытия: в проверенных местах каждый применимый класс кто-то ловит" +
@@ -267,6 +267,10 @@ export const ruGates = {
267
267
  caught: (names) => `ловит: ${names}`,
268
268
  blind: "НЕ ЛОВИТ НИКТО",
269
269
  unknown: "проверить нечем — гейт не состоялся (нет делегированной программы)",
270
+ blindWhere: (file, fixes) => `где: ${file} — починок в истории: ${fixes}`,
271
+ blindWhat: "что подсадили в ваш файл:",
272
+ blindFix: (cmd) => `поймать прямо сейчас, без комплекта: ${cmd}`,
273
+ blindTool: (url) => `инструмент: ${url}`,
270
274
  install: (cmd) => `закрыть: ${cmd}`,
271
275
  noSampleFor: (ext) => `в каталоге нет красного образца под «${ext}» — проверить нечем`,
272
276
  noGates: (cmd) => `гейтов не объявлено — пробовать нечем. Сначала: ${cmd}`,
package/tool/i18n/ru.mjs CHANGED
@@ -105,6 +105,17 @@ export const ru = {
105
105
  `заявка не подтверждена: «${e}» объявлена закрытой гейтом «${g}», но ни его команда, ни\n конфиг линтера не называют правил ${codes} — запись может быть не закрыта ничем`,
106
106
  coversUnprovenHow: (cmd) => `подтверди: добавь эти правила в линтер, либо поставь запись — ${cmd}`,
107
107
  totalCovered: (n) => `закрыто другим арбитром ${n}`,
108
+ blindHeading: (behind) => `Проба${behind ? ` (коммитов с тех пор: ${behind})` : ""} подсадила брак в ваши файлы — ваши проверки его НЕ ПОЙМАЛИ:`,
109
+ blindRan: (g) => `гейт ${g} стоит и прогонялся — а брак в этом файле пропустил`,
110
+ blindInstalled: "объявлено, но проба этот гейт не гоняла (поставлен позже или медленный) — поймает ли, покажет следующая",
111
+ blindMore: (cmd) => `что именно подсадили и куда — ${cmd}`,
112
+ probeNever: (cmd) => `Ловят ли ваши проверки настоящий брак, ещё не проверялось: ${cmd} подсадит его в копию проекта и покажет (минута-две, рабочие файлы не трогает).`,
113
+ startWith: "Начните с этих трёх — они родились из настоящего отказа и закрываются одной готовой командой:",
114
+ startCmd: (cmd) => `одной строкой, без комплекта: ${cmd}`,
115
+ startTool: (url) => `инструмент: ${url}`,
116
+ startHook: "Прогнать руками — разовый героизм. Чтобы это случалось перед каждым пушем: pre-commit (репозиторий https://github.com/arsen-ask-lx/Agent_Quality_Kit, хуки aqk / aqk-doctor) либо обычный .git/hooks/pre-push.",
117
+ haveAlready: (n) => `Проверки, которые у вас УЖЕ ЕСТЬ (${n}) — прочитаны в ваших файлах, не выдуманы:`,
118
+ haveAlreadyHow: (line) => `объявите их — и держать будет машина, а не ваше внимание. В .aqk.yml, в gates: ${line}`,
108
119
  total: "Итого:",
109
120
  totalHeld: (n) => `держит машина ${n}`,
110
121
  totalTodo: (n) => `применимо но не поставлено ${n}`,
@@ -121,6 +132,8 @@ export const ru = {
121
132
  `либо почини и убери из advisory, либо признай, что правила нет.`,
122
133
  runHeading: "Прогон объявленных гейтов",
123
134
  timeout: "не уложился в 5 минут",
135
+ running: (i, n) => `[${i}/${n}] идёт…`,
136
+ proving: "проверяю, что гейты ловят брак на своих образцах…",
124
137
  exitCode: (code) => `код ${code}`,
125
138
  moreLines: (n) => `… и ещё ${n} строк`,
126
139
  declaredNotRun: (n) => `${n} гейтов объявлено, но не запускалось.`,