agent-quality-kit 0.12.0 → 0.14.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 (74) hide show
  1. package/README.md +43 -7
  2. package/README.ru.md +44 -7
  3. package/kit/gates/api-contract-has-arbiter/README.md +16 -1
  4. package/kit/gates/api-contract-has-arbiter/check.sh +66 -23
  5. package/kit/gates/complexity-limit/gate.yml +4 -0
  6. package/kit/gates/dead-code/gate.yml +4 -0
  7. package/kit/gates/entry-commands-exist/README.md +64 -0
  8. package/kit/gates/entry-commands-exist/check.sh +110 -0
  9. package/kit/gates/entry-commands-exist/gate.yml +19 -0
  10. package/kit/gates/entry-commands-exist/green/AGENTS.md +13 -0
  11. package/kit/gates/entry-commands-exist/green/Makefile +6 -0
  12. package/kit/gates/entry-commands-exist/green/justfile +2 -0
  13. package/kit/gates/entry-commands-exist/green/package.json +10 -0
  14. package/kit/gates/entry-commands-exist/red/AGENTS.md +9 -0
  15. package/kit/gates/entry-commands-exist/red/Makefile +2 -0
  16. package/kit/gates/entry-commands-exist/red/package.json +9 -0
  17. package/kit/gates/env-secrets-not-committed/README.md +73 -0
  18. package/kit/gates/env-secrets-not-committed/check.sh +139 -0
  19. package/kit/gates/env-secrets-not-committed/gate.yml +21 -0
  20. package/kit/gates/env-secrets-not-committed/green/.aqk-tracked +10 -0
  21. package/kit/gates/env-secrets-not-committed/green/.env +10 -0
  22. package/kit/gates/env-secrets-not-committed/green/.env.production +5 -0
  23. package/kit/gates/env-secrets-not-committed/green/.env.test +2 -0
  24. package/kit/gates/env-secrets-not-committed/red/.aqk-tracked +5 -0
  25. package/kit/gates/env-secrets-not-committed/red/.env +7 -0
  26. package/kit/gates/no-print-in-prod/gate.yml +4 -0
  27. package/kit/gates/swallowed-error/gate.yml +4 -0
  28. package/kit/gates/todo-without-task/gate.yml +4 -0
  29. package/llms.txt +8 -2
  30. package/package.json +1 -1
  31. package/tool/commands/badge.mjs +1 -1
  32. package/tool/commands/context.mjs +81 -9
  33. package/tool/commands/doctor-catalog.mjs +222 -0
  34. package/tool/commands/doctor.mjs +81 -209
  35. package/tool/commands/learn.mjs +119 -19
  36. package/tool/commands/probe.mjs +50 -68
  37. package/tool/commands/project.mjs +6 -1
  38. package/tool/commands/prompt.mjs +69 -0
  39. package/tool/commands/report.mjs +1 -1
  40. package/tool/commands/vitals.mjs +15 -11
  41. package/tool/i18n/en-docs.mjs +22 -1
  42. package/tool/i18n/en-gates.mjs +6 -1
  43. package/tool/i18n/en.mjs +78 -4
  44. package/tool/i18n/index.mjs +42 -3
  45. package/tool/i18n/ru-docs.mjs +24 -1
  46. package/tool/i18n/ru-gates.mjs +6 -1
  47. package/tool/i18n/ru.mjs +87 -4
  48. package/tool/lib/adopt.mjs +15 -1
  49. package/tool/lib/advice.mjs +115 -0
  50. package/tool/lib/annotate.mjs +66 -0
  51. package/tool/lib/brief.mjs +3 -1
  52. package/tool/lib/cadence.mjs +40 -1
  53. package/tool/lib/core.mjs +40 -1
  54. package/tool/lib/gate-worker.mjs +18 -0
  55. package/tool/lib/history.mjs +34 -5
  56. package/tool/lib/manifest.mjs +65 -21
  57. package/tool/lib/repo.mjs +47 -35
  58. package/tool/lib/run.mjs +98 -9
  59. package/tool/program.mjs +4 -0
  60. package/tool/selfcheck/smoke/_fixture.mjs +8 -3
  61. package/tool/selfcheck/smoke/api-contract.test.mjs +37 -0
  62. package/tool/selfcheck/smoke/corpus.test.mjs +151 -0
  63. package/tool/selfcheck/smoke/first-run.test.mjs +144 -0
  64. package/tool/selfcheck/smoke/verdict.test.mjs +91 -3
  65. package/tool/selfcheck/smoke.sh +10 -2
  66. package/tool/selfcheck/units-annotate.mjs +67 -0
  67. package/tool/selfcheck/units-cadence.mjs +40 -1
  68. package/tool/selfcheck/units-context.mjs +64 -1
  69. package/tool/selfcheck/units-learn.mjs +32 -0
  70. package/tool/selfcheck/units-level.mjs +97 -3
  71. package/tool/selfcheck/units-probe.mjs +2 -1
  72. package/tool/selfcheck/units-prompt.mjs +106 -0
  73. package/tool/selfcheck/units-repo.mjs +44 -1
  74. package/tool/selfcheck/units-verdict.mjs +76 -0
@@ -13,10 +13,16 @@
13
13
  // настоящие правила («файл не трогай», «делай прогон с базой обязательно», «никаких
14
14
  // обходных временных путей») и разговорная шелуха примерно поровну.
15
15
  //
16
- // ЧЕГО ЗДЕСЬ НАМЕРЕННО НЕТ. Поиска ПОВТОРОВ — приёма, на котором построен session-analyzer у
17
- // agent-lint. Замер его не подтвердил: на 67 сессиях владелец не повторяет правило дословно, он
18
- // говорит его один раз и каждый раз иначе. Те «повторы», что нашлись, оказались задвоением
19
- // одной реплики в самом логе.
16
+ // ЧЕГО ЗДЕСЬ НАМЕРЕННО НЕТ. Поиска повторов ПО СХОДСТВУ ТЕКСТА — приёма, на котором построен
17
+ // session-analyzer у agent-lint. Замер его не подтвердил: на 67 сессиях владелец не повторяет
18
+ // правило дословно, он говорит его один раз и каждый раз иначе. Те «повторы», что нашлись,
19
+ // оказались задвоением одной реплики в самом логе.
20
+ //
21
+ // ЧТО ЕСТЬ ВМЕСТО НЕГО (2026-09-11). Повтор, который помечает САМ человек: «я же говорил»,
22
+ // «опять», «снова». Не угадывание, что две реплики об одном, а слова «это уже было». На логах
23
+ // двух проектов — 7 и 1 такая реплика, настоящих норм среди них 6 и 1; отбор по маркерам
24
+ // наставления не ловил ни одной. Такой повтор, совпавший с правилом свода, — «записано, а
25
+ // поправлять всё равно приходится»: правилу нужен сторож, текстом оно не держится.
20
26
  //
21
27
  // ПРИВАТНОСТЬ. Команда читает переписку. Поэтому: только логи ТЕКУЩЕГО проекта (или явно
22
28
  // названного), только в терминал, ни строки на диск, код возврата всегда 0. Отчёт, который
@@ -47,14 +53,86 @@ const MARKERS = new RegExp(
47
53
 
48
54
  // Признаки вставки, а не реплики: длина, код в тройных кавычках, много переносов, пути, ссылки.
49
55
  // Каждый добавлен по итогу прогона, а не на всякий случай.
56
+ function isPaste(text) {
57
+ const t = String(text || "").trim();
58
+ if (!t || t.length > 400) return true;
59
+ if (t.includes("```")) return true;
60
+ if ((t.match(/\n/g) || []).length > 6) return true;
61
+ if (/https?:\/\//.test(t)) return true;
62
+ return (t.match(/\S+\/\S+/g) || []).length >= 3;
63
+ }
64
+
50
65
  function looksLikeRule(text) {
66
+ return !isPaste(text) && MARKERS.test(String(text));
67
+ }
68
+
69
+ // ПОВТОР — сигнал, который даёт сам человек: «я же говорил», «опять», «снова». Разбор AgentLint
70
+ // 2026-09-11 (research/competitors/agentlint-0xmariowu.md): их SS2 сопоставляет поправку с
71
+ // правилом свода по словам. Замер на логах владельца: из 45 поправок к записанным правилам
72
+ // относятся от силы две — такой приём дал бы шум. А реплик с пометкой повтора в том же проекте
73
+ // 14, настоящих норм среди них 5–6 («опять не хочу плодить файлы», «я же не просил, ты опять не
74
+ // так понял») — и отбор по маркерам наставления выше не ловил НИ ОДНОЙ: слов «всегда»/«никогда»
75
+ // в них нет. `\b` здесь не годится — в JavaScript он не видит границ кириллических слов.
76
+ // Две силы пометки. Сильная — «я же говорил», «сколько раз» — повтор при любой форме реплики.
77
+ // Слабая — «опять», «снова» — только в утверждении: второй прогон на тех же логах показал, что
78
+ // вопрос с ней — недоумение («че опять rust?», «опять в env добавить?»), а не норма. Реплика со
79
+ // значка статуса (⬜ ✅ ❌) — вставленная цитата ответа агента, а не слова человека.
80
+ const STRONG = new RegExp(
81
+ "(^|[^а-яёa-z])(я же (говорил|говорю|просил|сказал|писал)|говорил же|сколько (раз|можно)|" +
82
+ "в который раз|(ещё|еще) раз говорю|i (already )?told you)([^а-яёa-z]|$)",
83
+ "i",
84
+ );
85
+ const WEAK = /(^|[^а-яёa-z])(опять(?! же)|снова|again)([^а-яёa-z]|$)/i;
86
+
87
+ function isRepeat(text) {
51
88
  const t = String(text || "").trim();
52
- if (!t || t.length > 400) return false;
53
- if (t.includes("```")) return false;
54
- if ((t.match(/\n/g) || []).length > 6) return false;
55
- if (/https?:\/\//.test(t)) return false;
56
- if ((t.match(/\S+\/\S+/g) || []).length >= 3) return false;
57
- return MARKERS.test(t);
89
+ if (isPaste(t) || /^[⬜✅❌☐☑]/u.test(t)) return false;
90
+ return STRONG.test(t) || (WEAK.test(t) && !t.includes("?"));
91
+ }
92
+
93
+ // Правила свода: пункты списка с меткой сторожа `<!-- aqk: … -->` — так их размечает комплект.
94
+ // Свод без меток — пункты под заголовком про правила. Заголовок правила — жирное начало или
95
+ // часть до двоеточия: по нему и сверяем, хвост пояснения совпал бы с чем угодно.
96
+ function entryRules(entryText) {
97
+ const items = [];
98
+ let cur = null;
99
+ let section = "";
100
+ for (const line of String(entryText).split(/\r?\n/)) {
101
+ const h = /^#{1,4}\s+(.+)$/.exec(line);
102
+ if (h) { section = h[1]; cur = null; continue; }
103
+ const b = /^[-*]\s+(.+)$/.exec(line);
104
+ if (b) { cur = { text: b[1], section }; items.push(cur); continue; }
105
+ if (cur && /^\s{2,}\S/.test(line)) cur.text += ` ${line.trim()}`;
106
+ else cur = null;
107
+ }
108
+ const marked = items.filter((r) => /<!--\s*aqk:/.test(r.text));
109
+ const pool = marked.length ? marked : items.filter((r) => /правил|rules|constraints|запрет/i.test(r.section));
110
+ return pool.map((r) => {
111
+ const body = r.text.replace(/<!--[\s\S]*?-->/g, "").trim();
112
+ const title = (/^\*\*([^*]+)\*\*/.exec(body) || /^([^:]{3,60}):/.exec(body) || [, body.slice(0, 100)])[1].trim();
113
+ return { title, arbiter: (/<!--\s*aqk:\s*(\S+?)\s*-->/.exec(r.text) || [])[1] || null };
114
+ });
115
+ }
116
+
117
+ // Слова-пометки повтора и служебные слова по правилу не сверяются: иначе «опять» совпало бы с
118
+ // любым правилом, где оно встретилось.
119
+ const NOT_TOPIC = new Set(["опять", "снова", "говор", "проси", "сказа", "писал", "тольк", "всегд",
120
+ "никог", "нужно", "можно", "котор", "когда", "чтобы", "этого", "again", "told", "always", "never"]);
121
+ const topic = (t) => new Set(keyWords(t).map(stem).filter((s) => !NOT_TOPIC.has(s)));
122
+
123
+ // Записано, а поправлять всё равно приходится: повтор, у которого с заголовком правила совпали
124
+ // две основы — или все, если заголовок короче трёх слов.
125
+ function repeatedRules(repeats, entryText) {
126
+ const out = [];
127
+ for (const r of entryRules(entryText)) {
128
+ const t = topic(r.title);
129
+ if (!t.size) continue;
130
+ for (const m of repeats) {
131
+ const common = [...topic(m.text)].filter((s) => t.has(s)).length;
132
+ if (common >= 2 || (t.size <= 2 && common === t.size)) out.push({ rule: r.title, arbiter: r.arbiter, ...m });
133
+ }
134
+ }
135
+ return out;
58
136
  }
59
137
 
60
138
  // Слова, по которым сверяем сказанное с записанным. Короткие отброшены: на них совпадёт что
@@ -127,6 +205,7 @@ async function cmdLearn(argv = process.argv) {
127
205
  try { files = (await readdir(root)).filter((f) => f.endsWith(".jsonl")); } catch { files = []; }
128
206
  const seen = new Set();
129
207
  const said = [];
208
+ const repeats = [];
130
209
  let typedTotal = 0;
131
210
  for (const f of files) {
132
211
  let raw = "";
@@ -136,24 +215,45 @@ async function cmdLearn(argv = process.argv) {
136
215
  const key = m.text.toLowerCase().slice(0, 200);
137
216
  if (seen.has(key)) continue;
138
217
  seen.add(key);
139
- if (looksLikeRule(m.text)) said.push(m);
218
+ if (isRepeat(m.text)) repeats.push(m);
219
+ else if (looksLikeRule(m.text)) said.push(m);
140
220
  }
141
221
  }
142
222
 
143
223
  const entry = await readEntry(await readManifest());
144
- const fresh = said.filter((m) => saidNotWritten(m.text, entry));
145
- fresh.sort((a, b) => String(b.when).localeCompare(String(a.when)));
224
+ const byDate = (a, b) => String(b.when).localeCompare(String(a.when));
225
+ const fresh = said.filter((m) => saidNotWritten(m.text, entry)).sort(byDate);
226
+ const ruleHits = repeatedRules(repeats, entry).sort(byDate);
227
+ const onRule = new Set(ruleHits.map((h) => h.text));
228
+ const again = repeats.filter((m) => !onRule.has(m.text)).sort(byDate);
146
229
 
147
- console.log(` ${c.dim(L.learn.counted(files.length, typedTotal, said.length, fresh.length))}\n`);
148
- if (!fresh.length) {
230
+ console.log(` ${c.dim(L.learn.counted(files.length, typedTotal, said.length, fresh.length, repeats.length))}\n`);
231
+ if (!fresh.length && !repeats.length) {
149
232
  console.log(` ${L.learn.nothing}\n`);
150
233
  return;
151
234
  }
152
- for (const m of fresh.slice(0, limit)) {
153
- console.log(` ${c.dim(m.when)} ${m.text.slice(0, 150)}`);
235
+ const show = (m) => console.log(` ${c.dim(m.when)} ${m.text.slice(0, 150)}`);
236
+ // Первым — правило, которое ЗАПИСАНО, а человек всё равно поправляет: текстом оно не держится.
237
+ if (ruleHits.length) {
238
+ console.log(` ${c.bold(L.learn.ruleTitle)}`);
239
+ for (const h of ruleHits.slice(0, limit)) {
240
+ console.log(` ${c.yellow("!")} ${h.rule}${h.arbiter ? c.dim(` · aqk: ${h.arbiter}`) : ""}`);
241
+ console.log(` ${c.dim(h.when)} ${h.text.slice(0, 140)}`);
242
+ }
243
+ console.log(c.dim(` ${L.learn.ruleHow}\n`));
244
+ }
245
+ if (again.length) {
246
+ console.log(` ${c.bold(L.learn.repeatTitle)}`);
247
+ again.slice(0, limit).forEach(show);
248
+ if (again.length > limit) console.log(c.dim(` ${L.learn.andMore(again.length - limit)}`));
249
+ console.log("");
250
+ }
251
+ if (fresh.length) {
252
+ if (repeats.length) console.log(` ${c.bold(L.learn.restTitle)}`);
253
+ fresh.slice(0, limit).forEach(show);
254
+ if (fresh.length > limit) console.log(c.dim(`\n ${L.learn.andMore(fresh.length - limit)}`));
154
255
  }
155
- if (fresh.length > limit) console.log(c.dim(`\n ${L.learn.andMore(fresh.length - limit)}`));
156
256
  console.log(`\n ${c.yellow(L.learn.warn)}\n`);
157
257
  }
158
258
 
159
- export { cmdLearn, logSlug, looksLikeRule, saidNotWritten, typedFrom };
259
+ export { cmdLearn, logSlug, looksLikeRule, saidNotWritten, typedFrom, isRepeat, repeatedRules };
@@ -26,10 +26,11 @@ import { readFileSync } from "node:fs";
26
26
  import { tmpdir } from "node:os";
27
27
  import { join, dirname, extname } from "node:path";
28
28
  import { readManifest } from "../lib/manifest.mjs";
29
- import { fixHotspots, probeSummary, probeVerdictPaired, countProbe } from "../lib/history.mjs";
29
+ import { fixHotspots, probeSummary, probeVerdictPaired, countProbe, namesPlant, catchVerdict } from "../lib/history.mjs";
30
30
  import { detectFacts, readCatalog, triggerVerdict } from "../lib/repo.mjs";
31
+ import { blindAdvice } from "../lib/advice.mjs";
31
32
  import { CWD, GATES_SRC, TARGET_DIR, c, SELF, exists } from "../lib/core.mjs";
32
- import { probeState, probeEvery, PROBE_EVERY, blindLines, parseBlind, parseRan } from "../lib/cadence.mjs";
33
+ import { probeState, probeEvery, PROBE_EVERY, blindLines, parseBlind, parseRan, parseCounts } from "../lib/cadence.mjs";
33
34
  import { L } from "../i18n/index.mjs";
34
35
  import { gateCommand } from "../lib/execution.mjs";
35
36
 
@@ -95,7 +96,7 @@ async function writeMark(now, blind, lines) {
95
96
  "",
96
97
  ...lines,
97
98
  "",
98
- "Файл эфемерный: его переписывает каждая проба. В .gitignore его стоит держать самому.",
99
+ "Файл эфемерный: его переписывает каждая проба. `aqk init` кладёт его в .gitignore.",
99
100
  ].join("\n");
100
101
  await writeFile(MARK(), body + "\n", "utf8");
101
102
  }
@@ -156,58 +157,11 @@ function sampleLines(path, max = 4) {
156
157
  .map((l) => l.slice(0, 88));
157
158
  }
158
159
 
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
-
206
160
  // Красный образец записи, подходящий по расширению горячего файла. Расширение обязано
207
161
  // совпадать: питоновский образец в проекте на TypeScript не проверит ничего, а покажет
208
162
  // «не прикрыто» — ложная тревога того же класса, что молчащий гейт, только наоборот.
209
- async function redSampleFor(entry, ext) {
210
- const dir = join(GATES_SRC, entry, "red");
163
+ async function sampleFor(entry, ext, kind = "red") {
164
+ const dir = join(GATES_SRC, entry, kind);
211
165
  if (!(await exists(dir))) return null;
212
166
  let names = [];
213
167
  try { names = await readdir(dir); } catch { return null; }
@@ -226,7 +180,8 @@ async function redSampleFor(entry, ext) {
226
180
  //
227
181
  // Способ взят из мутационного тестирования, где та же задача решена двадцать лет назад: Stryker
228
182
  // копирует проект во временный каталог, СИМЛИНКУЕТ `node_modules` и гоняет там родную команду.
229
- // Копируются только ОТСЛЕЖИВАЕМЫЕ файлы (`git archive HEAD`) — рабочее дерево не трогается, а
183
+ // Копируются отслеживаемые и неигнорируемые файлы (`git ls-files --cached --others
184
+ // --exclude-standard`) — файлы проекта не меняются (итог пишется в .aqk/last-probe.md), а
230
185
  // мусор сборки не тащится; тяжёлые каталоги зависимостей симлинкуются, иначе `npm test` в
231
186
  // песочнице падал бы с «модуль не найден», и это читалось бы как сбой инструмента.
232
187
  const DEP_DIRS = ["node_modules", ".venv", "venv", "vendor", "target", ".tox", ".bundle"];
@@ -284,20 +239,44 @@ async function plant(root, relPath, sample) {
284
239
  // Гейт запускается В ПЕСОЧНИЦЕ и командой КАК ЕСТЬ — ничего в неё не подставляется. Именно это
285
240
  // и делает пробу независимой от формы команды.
286
241
  //
287
- // `stopOnRed` ранний выход: как только гейт покраснел, вердикт «поймано» уже получен, и гонять
288
- // остальные незачем. На сухом прогоне выхода нет: там нужны ВСЕ длительности и все коды.
289
- function runGates(gates, sandbox, { stopOnRed = false } = {}) {
242
+ // Вывод гейта сохраняется: по нему видно, ИЗ-ЗА ЧЕГО он покраснел (см. catchVerdict).
243
+ function runGates(gates, sandbox) {
290
244
  const out = [];
291
245
  for (const [name, cmd] of gates) {
292
246
  const t0 = Date.now();
293
247
  const r = spawnSync(gateCommand(cmd), { shell: true, cwd: sandbox, encoding: "utf8", timeout: 120000 });
294
248
  const code = r.status === null ? 2 : r.status;
295
- out.push({ name, code, ms: Date.now() - t0 });
296
- if (stopOnRed && code === 1) break;
249
+ out.push({ name, code, ms: Date.now() - t0, out: `${r.stdout || ""}${r.stderr || ""}`.slice(0, 200000) });
297
250
  }
298
251
  return out;
299
252
  }
300
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
+
301
280
  // Каким гейтом пробовать и в каком порядке.
302
281
  //
303
282
  // Цена пробы = (файлы × записи) × сумма длительностей гейтов. На самом комплекте после перехода
@@ -368,7 +347,8 @@ async function cmdProbe(args, { auto = false } = {}) {
368
347
  // Пробуем только запланированными, в порядке плана.
369
348
  const byName = new Map(gates);
370
349
  const probeGates = plan.use.map((g) => [g.name, byName.get(g.name)]);
371
- 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 || "";
372
352
 
373
353
  console.log(c.dim(` ${P.method(hot.length, entries.length, plan.use.length)}\n`));
374
354
  if (plan.tooSlow.length) {
@@ -385,20 +365,20 @@ async function cmdProbe(args, { auto = false } = {}) {
385
365
  let probed = 0;
386
366
 
387
367
  for (const e of entries) {
388
- const sample = await redSampleFor(e.slug, ext);
368
+ const sample = await sampleFor(e.slug, ext, "red");
389
369
  if (!sample) continue;
390
370
  probed++;
391
- const restore = await plant(sandbox, rel, sample);
392
- let after;
393
- 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 });
394
373
  // Ранний выход обрывает список: гейты, до которых не дошли, считаются такими же, как на
395
374
  // сухом прогоне. Иначе их отсутствие прочиталось бы как сбой запуска.
396
375
  const seen = new Set(after.map((a) => a.name));
397
376
  const full = after.concat(baseline.filter((b) => !seen.has(b.name)));
398
377
  const r = probeVerdictPaired(baseline, full);
399
378
  const verdict = r.verdict;
400
- const caught = full.filter((a) => a.code === 1 && baseline.find((b) => b.name === a.name)?.code === 0)
401
- .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})`);
402
382
  records.push({ entry: e.slug, file: rel, verdict });
403
383
  if (verdict === "caught") {
404
384
  console.log(` ${c.green("✔")} ${e.intent.padEnd(48)} ${c.dim(P.caught(caught.join(", ")))}`);
@@ -419,7 +399,7 @@ async function cmdProbe(args, { auto = false } = {}) {
419
399
  if (adv.tool) console.log(c.dim(` ${P.blindTool(adv.tool)}`));
420
400
  console.log(c.dim(` ${P.install(`${SELF} add ${e.slug}`)}`));
421
401
  } else {
422
- 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)}`);
423
403
  }
424
404
  }
425
405
  if (!probed) { unprobedN++; console.log(c.dim(` ${P.noSampleFor(ext || "—")}`)); }
@@ -442,6 +422,7 @@ async function cmdProbe(args, { auto = false } = {}) {
442
422
  // Отметка нужна не для отчёта, а для КАДЕНЦИИ: по ней следующий прогон поймёт, что пора.
443
423
  // Без неё команда снова становится тем, о чём надо вспомнить.
444
424
  await writeMark(commitCount(), blind, [
425
+ `caught: ${n.caughtClasses}`, `unknown: ${n.unknownClasses}`,
445
426
  `ran: ${probeGates.map(([name]) => name).join(" ")}`, "",
446
427
  ...blindLines(records), "",
447
428
  ...hot.map(({ path: p2, fixes }) => `- ${p2} (${P.fixes(fixes)})`),
@@ -462,7 +443,8 @@ async function probeStatus() {
462
443
  if (every === null) return { state: "unknown", behind: null, badEvery: String(man?.probe) };
463
444
  if (every === 0) return { state: "off", behind: null };
464
445
  const mark = await readMark();
465
- return { ...probeState(mark, commitCount(), every), classes: parseBlind(mark?.text), ran: parseRan(mark?.text) };
446
+ const text = mark?.text;
447
+ return { ...probeState(mark, commitCount(), every), classes: parseBlind(text), ran: parseRan(text), counts: parseCounts(text) };
466
448
  }
467
449
 
468
- export { cmdProbe, probeStatus, probeableGates, gatesState, extAlternatives, planProbeGates, blindAdvice, isCode };
450
+ 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}`);
@@ -0,0 +1,69 @@
1
+ // tool/commands/prompt.mjs — `aqk prompt`: одно задание для агента — «почини вот это, это и это».
2
+ //
3
+ // ЗАЧЕМ. Между диагнозом и действием не было моста. `doctor` пишет человеку, `context` говорит
4
+ // агенту «как дела», а задание «сделай вот это» человек пересказывал сам — и пересказ терял
5
+ // команды. Идея — из разбора agentlint (research/competitors/agentlint.md, «Задание для агента»):
6
+ // правила поведения сверху, исправления по весу, в конце — как проверить.
7
+ //
8
+ // ЧЕГО У НИХ НЕ БЕРЁМ. У них «проверь» — это «балл вырос». У нас у КАЖДОГО пункта свой арбитр:
9
+ // команда, которая сейчас красная и должна стать зелёной. Иначе агент доложит «сделал» про
10
+ // пункт, который ничем не доказан, — тот самый отказ, против которого весь комплект.
11
+ //
12
+ // НИЧЕГО НЕ ЗАПУСКАЕТ. Читает то же, что `context`: манифест, `.aqk/last-run.md`, отметку пробы,
13
+ // каталог. Задание пишется за секунду, а прогон — работа агента, и она в задании первой строкой,
14
+ // если прогона нет или он устарел: иначе пустое задание прочиталось бы как «всё чисто».
15
+ import { readManifest } from "../lib/manifest.mjs";
16
+ import { claudeShimFor } from "../lib/repo.mjs";
17
+ import { CWD, SELF } from "../lib/core.mjs";
18
+ import { readRun, readAdvice, portableSelf } from "./context.mjs";
19
+ import { probeStatus } from "./probe.mjs";
20
+ import { L } from "../i18n/index.mjs";
21
+
22
+ // Больше пяти пунктов за раз агент не удержит — как и человек; остальное называется числом.
23
+ const MAX_ITEMS = 5;
24
+
25
+ // Чистая функция: состояние → строки задания. Порядок — по тому, насколько пункт ФАКТ о проекте
26
+ // и насколько без него невыполнимы остальные: манифест → прогон → красное → гейт стоит, но
27
+ // пропустил брак из пробы → брак, для которого гейта нет → проверки, которые у проекта уже есть
28
+ // → свод, невидимый Claude Code → «начните с этих трёх» из каталога. Класс из пробы в последнем
29
+ // списке не повторяется.
30
+ function taskText(st, T = L.prompt) {
31
+ const self = st.self || "aqk";
32
+ const it = [];
33
+ if (!st.manifest) it.push(T.item.init(self));
34
+ if (!st.run) it.push(T.item.runNone(self));
35
+ else if (st.run.stale) it.push(T.item.runStale(self, st.run.when));
36
+ for (const name of st.run?.red || []) it.push(T.item.red(name, self));
37
+ for (const m of st.missed || []) it.push(T.item.missed(m, self));
38
+ for (const b of st.blind || []) it.push(T.item.blind(b, self));
39
+ if ((st.adopt || []).length) it.push(T.item.adopt(st.adopt, self));
40
+ if (st.shim) it.push(T.item.shim[st.shim](self));
41
+ const seen = new Set((st.blind || []).map((b) => b.slug));
42
+ for (const s of st.start || []) if (!seen.has(s.slug)) it.push(T.item.start(s, self));
43
+
44
+ const out = [T.title, "", T.intro, "", T.rulesTitle, ...T.rules.map((r) => `- ${r}`), "", T.tasksTitle];
45
+ if (!it.length) out.push(T.empty);
46
+ it.slice(0, MAX_ITEMS).forEach((line, i) => out.push(`${i + 1}. ${line}`));
47
+ if (it.length > MAX_ITEMS) out.push(T.more(it.length - MAX_ITEMS, self));
48
+ out.push("", T.verifyTitle, ...T.verify(self).map((r) => `- ${r}`));
49
+ return out;
50
+ }
51
+
52
+ async function cmdPrompt() {
53
+ const man = await readManifest();
54
+ let probe = null;
55
+ try { probe = await probeStatus(); } catch { /* пробы нет — пунктов из неё не будет */ }
56
+ let advice = { adopt: [], blind: [], start: [], missed: [] };
57
+ try { advice = await readAdvice(man, probe); } catch { /* не посчитали — выдумывать пункты нельзя */ }
58
+ // Команда уходит в чужой контекст и, возможно, в чужие руки: абсолютный путь к нашей
59
+ // программе там не сработает — тот же довод, что у хука `context --install`.
60
+ console.log(taskText({
61
+ self: portableSelf(SELF),
62
+ manifest: Boolean(man),
63
+ run: await readRun(),
64
+ shim: await claudeShimFor(CWD),
65
+ ...advice,
66
+ }).join("\n"));
67
+ }
68
+
69
+ export { cmdPrompt, taskText };
@@ -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,9 +16,10 @@
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";
24
25
  import { gitBash } from "../lib/execution.mjs";
@@ -122,14 +123,7 @@ async function cmdVitals() {
122
123
  // Хук pre-commit проверяется в `.git/hooks`, а НЕ в `.pre-commit-config.yaml`. Запись в
123
124
  // конфиге — это намерение; сработает только то, что лежит в самом гите. Ровно та разница,
124
125
  // ради которой весь комплект: объявлено и работает — разные утверждения.
125
- let preCommit = null;
126
- const hook = join(CWD, ".git", "hooks", "pre-commit");
127
- if (await exists(join(CWD, ".git"))) {
128
- preCommit = false;
129
- if (await exists(hook)) {
130
- try { preCommit = /pre-commit|aqk/i.test(await readFile(hook, "utf8")); } catch { preCommit = null; }
131
- }
132
- }
126
+ const preCommit = await preCommitHook(CWD);
133
127
 
134
128
  let sessionHook = null;
135
129
  const settings = join(CWD, ".claude", "settings.json");
@@ -145,15 +139,25 @@ async function cmdVitals() {
145
139
 
146
140
  let version = null;
147
141
  if (updateWanted()) {
142
+ let current = "";
148
143
  try {
149
144
  const { PKG_ROOT } = await import("../lib/core.mjs");
150
- 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 || "";
151
146
  const r = await fetch("https://registry.npmjs.org/agent-quality-kit/latest", {
152
147
  signal: AbortSignal.timeout(3000),
153
148
  headers: { accept: "application/vnd.npm.install-v1+json" },
154
149
  });
155
150
  version = { current, latest: r.ok ? String((await r.json()).version || "") : "" };
156
- } 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
+ }
157
161
  }
158
162
 
159
163
  const rows = vitalsRows({ tools: [...seen.values()], unparsed, preCommit, sessionHook, version });
@@ -66,7 +66,7 @@ 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\".",
@@ -79,6 +79,26 @@ const enDocs = {
79
79
  (behind ? ` (${behind} commits behind)` : "") + ".",
80
80
  ratchets: (list) => `Ratchets: ${list}. The list may only get shorter, never longer.`,
81
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
+ ],
82
102
  mapTitle: "WHAT THIS TOOL CAN DO. The full list of commands — not a retelling, the same list\nthe help is built from:",
83
103
  rulesTitle: (e) => `THE RULEBOOK OF THIS PROJECT (${e}) — verbatim, in full. This is not an invitation\nto read it: it is already here.`,
84
104
  hookAlready: (p) => `the hook is already in ${p} — changing nothing.`,
@@ -196,6 +216,7 @@ const enDocs = {
196
216
  },
197
217
  ],
198
218
  report: {
219
+ skippedBySelect: "not run (by --only/--skip), state unknown",
199
220
  title: "aqk doctor --run",
200
221
  version: "version",
201
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,20 @@ 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`,
268
273
  blindWhere: (file, fixes) => `where: ${file} — ${fixes} fix commits in its history`,
269
274
  blindWhat: "what we planted into your file:",
270
275
  blindFix: (cmd) => `catch it right now, no kit needed: ${cmd}`,