agent-quality-kit 0.13.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 (43) hide show
  1. package/README.md +19 -5
  2. package/README.ru.md +20 -5
  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/entry-commands-exist/README.md +64 -0
  6. package/kit/gates/entry-commands-exist/check.sh +110 -0
  7. package/kit/gates/entry-commands-exist/gate.yml +19 -0
  8. package/kit/gates/entry-commands-exist/green/AGENTS.md +13 -0
  9. package/kit/gates/entry-commands-exist/green/Makefile +6 -0
  10. package/kit/gates/entry-commands-exist/green/justfile +2 -0
  11. package/kit/gates/entry-commands-exist/green/package.json +10 -0
  12. package/kit/gates/entry-commands-exist/red/AGENTS.md +9 -0
  13. package/kit/gates/entry-commands-exist/red/Makefile +2 -0
  14. package/kit/gates/entry-commands-exist/red/package.json +9 -0
  15. package/llms.txt +5 -1
  16. package/package.json +1 -1
  17. package/tool/commands/context.mjs +34 -15
  18. package/tool/commands/doctor-catalog.mjs +222 -0
  19. package/tool/commands/doctor.mjs +28 -211
  20. package/tool/commands/learn.mjs +119 -19
  21. package/tool/commands/probe.mjs +4 -2
  22. package/tool/commands/prompt.mjs +69 -0
  23. package/tool/i18n/en.mjs +65 -3
  24. package/tool/i18n/index.mjs +42 -3
  25. package/tool/i18n/ru.mjs +74 -3
  26. package/tool/lib/annotate.mjs +66 -0
  27. package/tool/lib/cadence.mjs +30 -1
  28. package/tool/lib/core.mjs +1 -0
  29. package/tool/lib/repo.mjs +45 -4
  30. package/tool/lib/run.mjs +20 -2
  31. package/tool/program.mjs +4 -0
  32. package/tool/selfcheck/smoke/_fixture.mjs +8 -3
  33. package/tool/selfcheck/smoke/api-contract.test.mjs +37 -0
  34. package/tool/selfcheck/smoke/corpus.test.mjs +151 -0
  35. package/tool/selfcheck/smoke/first-run.test.mjs +39 -0
  36. package/tool/selfcheck/smoke/verdict.test.mjs +41 -2
  37. package/tool/selfcheck/smoke.sh +4 -0
  38. package/tool/selfcheck/units-annotate.mjs +67 -0
  39. package/tool/selfcheck/units-cadence.mjs +26 -1
  40. package/tool/selfcheck/units-learn.mjs +32 -0
  41. package/tool/selfcheck/units-level.mjs +21 -1
  42. package/tool/selfcheck/units-prompt.mjs +106 -0
  43. package/tool/selfcheck/units-repo.mjs +20 -1
@@ -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 };
@@ -30,7 +30,7 @@ import { fixHotspots, probeSummary, probeVerdictPaired, countProbe, namesPlant,
30
30
  import { detectFacts, readCatalog, triggerVerdict } from "../lib/repo.mjs";
31
31
  import { blindAdvice } from "../lib/advice.mjs";
32
32
  import { CWD, GATES_SRC, TARGET_DIR, c, SELF, exists } from "../lib/core.mjs";
33
- 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";
34
34
  import { L } from "../i18n/index.mjs";
35
35
  import { gateCommand } from "../lib/execution.mjs";
36
36
 
@@ -422,6 +422,7 @@ async function cmdProbe(args, { auto = false } = {}) {
422
422
  // Отметка нужна не для отчёта, а для КАДЕНЦИИ: по ней следующий прогон поймёт, что пора.
423
423
  // Без неё команда снова становится тем, о чём надо вспомнить.
424
424
  await writeMark(commitCount(), blind, [
425
+ `caught: ${n.caughtClasses}`, `unknown: ${n.unknownClasses}`,
425
426
  `ran: ${probeGates.map(([name]) => name).join(" ")}`, "",
426
427
  ...blindLines(records), "",
427
428
  ...hot.map(({ path: p2, fixes }) => `- ${p2} (${P.fixes(fixes)})`),
@@ -442,7 +443,8 @@ async function probeStatus() {
442
443
  if (every === null) return { state: "unknown", behind: null, badEvery: String(man?.probe) };
443
444
  if (every === 0) return { state: "off", behind: null };
444
445
  const mark = await readMark();
445
- 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) };
446
448
  }
447
449
 
448
450
  export { cmdProbe, probeStatus, probeableGates, gatesState, extAlternatives, planProbeGates, isCode };
@@ -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 };
package/tool/i18n/en.mjs CHANGED
@@ -7,14 +7,20 @@ import { enDocs } from "./en-docs.mjs";
7
7
 
8
8
  import { templates } from "./templates-en.mjs";
9
9
 
10
+ const ago = (n) => (n === null || n === undefined ? "" : ` (${n} commit${n === 1 ? "" : "s"} ago)`);
11
+
10
12
  import { enGates } from "./en-gates.mjs";
11
13
 
12
14
  export const en = {
13
15
  learn: {
14
16
  title: "Said out loud, never written down",
15
17
  noLogs: (p) => `no logs for this project: ${p}\n The command reads Claude Code transcripts on this machine. Empty means nobody worked here.`,
16
- counted: (s, typed, said, fresh) =>
17
- `sessions: ${s} · typed by a human: ${typed} · looks like an instruction: ${said} · not in the entry point: ${fresh}`,
18
+ counted: (s, typed, said, fresh, again) =>
19
+ `sessions: ${s} · typed by a human: ${typed} · looks like an instruction: ${said} · not in the entry point: ${fresh} · repeated: ${again}`,
20
+ ruleTitle: "Written down, yet you still have to correct it:",
21
+ ruleHow: "The rule is in the entry point, and you are repeating it to the agent again — text alone does not hold it. It needs a machine guard: aqk find \"…\" or aqk new <name>.",
22
+ repeatTitle: "Repeated — you have said this before (\"I told you\", \"again\"):",
23
+ restTitle: "The rest that looks like a rule and is not written down:",
18
24
  nothing: "everything that looks like a rule is already in the entry point",
19
25
  andMore: (n) => `… and ${n} more`,
20
26
  warn:
@@ -45,6 +51,7 @@ export const en = {
45
51
  learn: "rule candidates from local transcripts: said out loud, never written down",
46
52
  context: "the project state in one block — for an agent's context, not for reading",
47
53
  vitals: "is what the kit runs on wired up: gate tools, hooks, version freshness",
54
+ prompt: "one task for the agent: what to fix, in order, and how to prove it is done",
48
55
  contextInstall: "the same in full — the map and the rulebook — installed as a hook",
49
56
  report: "the mandatory report form: what is in place, what is not, what was not read; --since <ref> adds what proves the diff",
50
57
  badge: "a level badge for your README — and a check that it does not lie",
@@ -73,6 +80,14 @@ export const en = {
73
80
  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
81
  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
82
  selectSkipped: (names) => `not run (by --only/--skip): ${names} — their state is unknown, they are not "green"`,
83
+ claudeShim: {
84
+ missing: "Claude Code is set up here (.claude/), but the rules live in AGENTS.md — it does not read that file. Fix: a CLAUDE.md with the single line \"@AGENTS.md\".",
85
+ noImport: "CLAUDE.md does not import AGENTS.md — Claude Code only sees CLAUDE.md. Fix: add the line \"@AGENTS.md\" to CLAUDE.md (mentioning the file in prose does not load it).",
86
+ },
87
+ annotDropped: (n, more) => `pull request annotations: ${n}, ${more} more not shown — GitHub takes about ten per step; every finding is in the log above`,
88
+ heldQuiet: (n, cmd) => `held by the machine: ${n} — by name: ${cmd}`,
89
+ skipQuiet: (n, cmd) => `not applicable to this repository: ${n} — by name and why: ${cmd}`,
90
+ passedQuiet: (n) => `${n} more passed — by name: --verbose`,
76
91
  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.`,
77
92
  rulesByHuman: (total, machine, human) =>
78
93
  `${human} of ${total} rules in the entry point are guarded by a HUMAN, ${machine} by a machine.`,
@@ -95,6 +110,17 @@ export const en = {
95
110
  toReach: (n) => `To reach AQK-${n}:`,
96
111
  gives: (what) => `What it buys you: ${what}`,
97
112
  allDone: "All levels reached.",
113
+ limitsTitle: "A level measures tooling, not reliability. What it does not prove:",
114
+ limitsProbe: {
115
+ never: (_, cmd) => `defects in your files: the probe has never run — ${cmd}`,
116
+ off: () => "defects in your files: the probe is off (probe: 0) — whether your checks catch them is unknown",
117
+ blind: ({ names, behind }) => `defects in your files: the probe${ago(behind)} did NOT catch — ${names.join(", ")}`,
118
+ partial: ({ caught, unknown, behind }, cmd) => `defects in your files: the probe${ago(behind)} caught ${caught} classes, ${unknown} unproven (${cmd})`,
119
+ caught: ({ caught, behind }) => `defects in your files: the probe${ago(behind)} caught all ${caught} planted classes — only those the catalog has`,
120
+ nothing: ({ behind }, cmd) => `defects in your files: the probe${ago(behind)} planted nothing — ${cmd}`,
121
+ old: ({ behind }, cmd) => `defects in your files: the probe ran${ago(behind)}; ${cmd} shows the result`,
122
+ },
123
+ limitsCi: "pipeline: whether it passed is not visible from here — we only check that gates are declared in it",
98
124
 
99
125
  gatesHeading: "Gates",
100
126
  langs: "languages",
@@ -216,7 +242,7 @@ export const en = {
216
242
  has_agent_entry: ["no entry point for an agent here", "an entry point for an agent exists"],
217
243
  has_ui: ["no stylesheets or UI components in sight", "a UI exists: stylesheets or components"],
218
244
  has_mcp: ["no MCP tools are wired up for the agent here", "MCP servers are declared"],
219
- has_api_spec: ["no API specification in sight", "an API specification exists"],
245
+ has_api_spec: ["no API contract in sight: no OpenAPI file, no tRPC, ts-rest or Fastify type provider", "an API contract exists: a specification file or schemas in code"],
220
246
  },
221
247
  },
222
248
 
@@ -242,5 +268,41 @@ export const en = {
242
268
  alreadyDeclared: "already declared",
243
269
  },
244
270
 
271
+ prompt: {
272
+ title: "# Task: get this project's checks actually working",
273
+ intro: "Written by AQK from the current state of the repository. Do the items in order.",
274
+ rulesTitle: "## Ground rules",
275
+ rules: [
276
+ "Use only commands from this task and from the repository. Do not invent any.",
277
+ "Run an item's check first and see it fail. Then fix. Done means the same command passes.",
278
+ "Fix the code, not the check: do not loosen a threshold, add exclusions or switch a gate off. If you think a check is wrong, stop and ask the owner.",
279
+ "A decision only the owner can make (what to add to the project, which rules to adopt) — ask, do not guess.",
280
+ "Change only what these items need.",
281
+ ],
282
+ tasksTitle: "## What to do",
283
+ empty: "Nothing to do: no gate is red, the probe found nothing, nothing to add. Still run the verification below.",
284
+ done: "Done —",
285
+ item: {
286
+ init: (s) => `Create the manifest: \`${s} init\` — without it the other commands refuse. Done — \`${s} doctor\` shows a level.`,
287
+ runNone: (s) => `There has been no run yet. Run \`${s} doctor --run\` and fix whatever turns red, one gate at a time: \`${s} doctor --run --only <name>\`. Done — the run passes.`,
288
+ runStale: (s, when) => `The run from ${when} is older than the last commit — the red list below may describe other code. Run \`${s} doctor --run\` again. Done — you have a fresh result and have checked the items below against it.`,
289
+ missed: ({ slug, file }, s) => `Gate \`${slug}\` is installed but missed the defect the probe planted into \`${file}\`. Find out why — a common cause is that the gate does not look at this file type or folder; \`${s} probe\` has the details. Done — \`${s} probe\` no longer names this class.`,
290
+ red: (name, s) => `Gate \`${name}\` is red. Run \`${s} doctor --run --only ${name}\`, read the findings and fix the code. Done — that command passes.`,
291
+ blind: ({ slug, file, command }, s) => `The probe planted a \`${slug}\` defect into \`${file}\` and the project's checks did not notice. Add a check: \`${s} add ${slug}\`${command ? ` (the same as one line, without the kit: \`${command}\`)` : ""}. Done — \`${s} doctor --run --only ${slug}\` passes and \`${s} probe\` no longer names this class.`,
292
+ adopt: (gates, s) => `The project already has its own checks: ${gates.map((g) => `\`${g.cmd}\` (${g.source})`).join(", ")}. Declare them under gates: in .aqk.yml — ${gates.map((g) => `\`${g.name}: "${g.cmd}"\``).join(", ")}. Done — \`${s} doctor --run\` runs them.`,
293
+ shim: {
294
+ missing: (s) => `Claude Code is set up here, but the rules live in AGENTS.md — it only reads CLAUDE.md. Create a CLAUDE.md with the single line \`@AGENTS.md\`. Done — \`${s} doctor\` no longer warns about it.`,
295
+ noImport: (s) => `CLAUDE.md does not import AGENTS.md — Claude Code only sees CLAUDE.md. Add the line \`@AGENTS.md\` to CLAUDE.md (mentioning the file in prose does not load it). Done — \`${s} doctor\` no longer warns about it.`,
296
+ },
297
+ start: ({ slug, intent, command }, s) => `Propose the \`${slug}\` check to the owner${intent ? ` — ${intent}` : ""}. If they agree — \`${s} add ${slug}\`${command ? ` (the same as one line, without the kit: \`${command}\`)` : ""}. If it fails on existing code, do not silence it — show the findings to the owner. Done — \`${s} doctor --run --only ${slug}\` passes and \`${s} prove\` shows it proven.`,
298
+ },
299
+ more: (n, s) => `And ${n} more — the full list: \`${s} doctor\`. Finish these first.`,
300
+ verifyTitle: "## How to verify it is done",
301
+ verify: (s) => [
302
+ `\`${s} doctor --run\` — the run passes.`,
303
+ `\`${s} prove\` — no gate is broken: each one fails on its own red sample.`,
304
+ "Name these commands and their results in your report. \"Looks like it works\" is not done.",
305
+ ],
306
+ },
245
307
  ...enGates,
246
308
  };
@@ -29,13 +29,19 @@ import { en } from "./en.mjs";
29
29
  // Переменная окружения оставлена ВЫШЕ манифеста намеренно: человек, набравший AQK_LANG=en
30
30
  // руками, хочет английский именно сейчас, и спорить с ним манифестом значит отнять у него
31
31
  // последнее средство. Манифест выше локали — он про проект, локаль про машину.
32
- function pickLang(env = process.env, man = null) {
32
+ //
33
+ // Третьим — ЯЗЫК СВОДА ПРОЕКТА, между манифестом и локалью. Отзыв с живого проекта 2026-09-11:
34
+ // свод и методички на русском, Windows без LANG — весь вывод английский, пока руками не впишешь
35
+ // `lang:`. Текст, который проект сам о себе написал, — такое же свойство проекта, как поле
36
+ // манифеста, только необъявленное: поэтому ниже поля и выше машины.
37
+ function pickLang(env = process.env, man = null, docs = "") {
33
38
  const forced = String(env.AQK_LANG || "").toLowerCase();
34
39
  if (forced.startsWith("ru")) return "ru";
35
40
  if (forced.startsWith("en")) return "en";
36
41
  const declared = String(man?.lang || "").toLowerCase();
37
42
  if (declared.startsWith("ru")) return "ru";
38
43
  if (declared.startsWith("en")) return "en";
44
+ if (docs === "ru" || docs === "en") return docs;
39
45
  const locale = String(env.LC_ALL || env.LC_MESSAGES || env.LANG || "").toLowerCase();
40
46
  if (locale.startsWith("ru")) return "ru";
41
47
  return "en";
@@ -59,8 +65,41 @@ function manifestLang(cwd = process.cwd()) {
59
65
  }
60
66
  }
61
67
 
68
+ // Язык прозы: код, пути и адреса вырезаются — в русском своде половина слов это `npm run check`
69
+ // и `packages/contract`, и по всем буквам подряд он вышел бы английским. Порог с запасом в обе
70
+ // стороны: кириллицы больше половины букв — русский, меньше десятой при хотя бы двухстах
71
+ // буквах — английский; между ними и на коротком тексте свод молчит, и решает локаль.
72
+ function langFromDocs(text) {
73
+ const prose = String(text)
74
+ .replace(/```[\s\S]*?```/g, " ")
75
+ .replace(/`[^`\n]*`/g, " ")
76
+ .replace(/<!--[\s\S]*?-->/g, " ")
77
+ .replace(/https?:\/\/\S+/g, " ");
78
+ const cyr = (prose.match(/[а-яё]/gi) || []).length;
79
+ const lat = (prose.match(/[a-z]/gi) || []).length;
80
+ const all = cyr + lat;
81
+ if (all < 200) return "";
82
+ if (cyr / all >= 0.5) return "ru";
83
+ if (cyr / all <= 0.1) return "en";
84
+ return "";
85
+ }
86
+
87
+ // Первые 8 КБ каждого из трёх файлов: язык виден по первому экрану, а читать мегабайтный README
88
+ // ради одной буквы в начале каждой команды — дорого.
89
+ function docsLang(cwd = process.cwd()) {
90
+ let text = "";
91
+ for (const name of ["AGENTS.md", "CLAUDE.md", "README.md"]) {
92
+ try {
93
+ text += readFileSync(join(cwd, name), "utf8").slice(0, 8192) + "\n";
94
+ } catch {
95
+ // Файла нет — язык решат остальные.
96
+ }
97
+ }
98
+ return langFromDocs(text);
99
+ }
100
+
62
101
  const CATALOGS = { ru, en };
63
- const LANG = pickLang(process.env, manifestLang());
102
+ const LANG = pickLang(process.env, manifestLang(), docsLang());
64
103
  const L = CATALOGS[LANG];
65
104
 
66
- export { L, LANG, pickLang, langFromText, CATALOGS };
105
+ export { L, LANG, pickLang, langFromText, langFromDocs, CATALOGS };
package/tool/i18n/ru.mjs CHANGED
@@ -10,12 +10,20 @@ import { templates } from "./templates-ru.mjs";
10
10
 
11
11
  import { ruGates } from "./ru-gates.mjs";
12
12
 
13
+ // «(коммитов с тех пор: 3)» — без склонений: число рядом со словом в родительном падеже
14
+ // читается одинаково при любом числе.
15
+ const ago = (n) => (n === null || n === undefined ? "" : ` (коммитов с тех пор: ${n})`);
16
+
13
17
  export const ru = {
14
18
  learn: {
15
19
  title: "Сказано вслух и не записано",
16
20
  noLogs: (p) => `логов этого проекта нет: ${p}\n Команда читает переписку Claude Code на этой машине. Пусто — значит здесь не работали.`,
17
- counted: (s, typed, said, fresh) =>
18
- `сессий: ${s} · напечатано человеком: ${typed} · похоже на наставление: ${said} · нет в точке входа: ${fresh}`,
21
+ counted: (s, typed, said, fresh, again) =>
22
+ `сессий: ${s} · напечатано человеком: ${typed} · похоже на наставление: ${said} · нет в точке входа: ${fresh} · повторено: ${again}`,
23
+ ruleTitle: "Записано, а поправлять всё равно приходится:",
24
+ ruleHow: "Правило стоит в своде, а вы повторяете его агенту снова — текстом оно не держится. Ему нужен сторож-машина: aqk find \"…\" или aqk new <имя>.",
25
+ repeatTitle: "Повторено — вы говорили это не в первый раз («я же говорил», «опять», «снова»):",
26
+ restTitle: "Остальное, похожее на правило и не записанное:",
19
27
  nothing: "всё, что похоже на правило, уже стоит в точке входа",
20
28
  andMore: (n) => `… и ещё ${n}`,
21
29
  warn:
@@ -46,6 +54,7 @@ export const ru = {
46
54
  learn: "кандидаты в правила из локальной переписки: сказано вслух и не записано",
47
55
  context: "состояние проекта одним блоком — для контекста агента, а не для чтения",
48
56
  vitals: "подключено ли то, чем комплект работает: инструменты гейтов, хуки, свежесть версии",
57
+ prompt: "одно задание для агента: что починить, по порядку, и чем доказать, что готово",
49
58
  contextInstall: "то же самое, но целиком — карта и свод правил — и хуком в контекст",
50
59
  report: "обязательная форма отчёта: что стоит, что нет, что не прочитано; --since <ссылка> — ещё и чем доказан диф",
51
60
  badge: "значок уровня для README — и проверка, что он не врёт",
@@ -74,6 +83,15 @@ export const ru = {
74
83
  coversCantCheck: (entry, gate) => `заявку «${gate} держит ${entry}» проверить не умею: линтер гейта не распознан или правил записи для него нет — принято на слово`,
75
84
  selectUnknown: (names, groups) => `--only/--skip: не знаю «${names}» — это не гейт из gates: и не группа из groups:${groups ? ` (группы: ${groups})` : ""}. Прогон всего подряд вместо пропуска был бы неправдой, поэтому стоп.`,
76
85
  selectSkipped: (names) => `не запускались (по --only/--skip): ${names} — их состояние неизвестно, это не «зелёные»`,
86
+ // Claude Code читает CLAUDE.md, а не AGENTS.md — документация Claude Code, раздел «AGENTS.md».
87
+ claudeShim: {
88
+ missing: "Claude Code здесь настроен (.claude/), а свод лежит в AGENTS.md — он его не читает. Почини: CLAUDE.md с одной строкой «@AGENTS.md».",
89
+ noImport: "CLAUDE.md не подключает AGENTS.md — Claude Code видит только CLAUDE.md. Почини: строка «@AGENTS.md» в CLAUDE.md (упоминание словами не загружает файл).",
90
+ },
91
+ annotDropped: (n, more) => `пометок в pull request: ${n}, ещё ${more} не показано — GitHub принимает около десяти за шаг; все находки — в логе выше`,
92
+ heldQuiet: (n, cmd) => `держит машина: ${n} — поимённо: ${cmd}`,
93
+ skipQuiet: (n, cmd) => `не применимо к этому репозиторию: ${n} — поимённо и почему: ${cmd}`,
94
+ passedQuiet: (n) => `прошли ещё ${n} — поимённо: --verbose`,
77
95
  jobsBad: (v) => `--jobs ждёт целое число от 1: «${v}» не подходит. Прогон по одному под видом параллельного был бы неправдой, поэтому стоп.`,
78
96
  rulesByHuman: (total, machine, human) =>
79
97
  `правил в точке входа: ${total}. Сторож — ЧЕЛОВЕК у ${human}, машина у ${machine}.`,
@@ -96,6 +114,19 @@ export const ru = {
96
114
  toReach: (n) => `Чтобы достичь AQK-${n}:`,
97
115
  gives: (what) => `Что это даст: ${what}`,
98
116
  allDone: "Все ступени пройдены.",
117
+ // Чего уровень не доказывает. Ступени меряют оснащённость: гейты показаны на образцах
118
+ // каталога. Про файлы проекта знает только проба — строка говорит ровно то, что знает она.
119
+ limitsTitle: "Уровень — это оснащённость, а не надёжность. Чего он не доказывает:",
120
+ limitsProbe: {
121
+ never: (_, cmd) => `брак в ваших файлах: проба не запускалась — ${cmd}`,
122
+ off: () => "брак в ваших файлах: проба выключена (probe: 0) — ловят ли его проверки, неизвестно",
123
+ blind: ({ names, behind }) => `брак в ваших файлах: проба${ago(behind)} НЕ поймала — ${names.join(", ")}`,
124
+ partial: ({ caught, unknown, behind }, cmd) => `брак в ваших файлах: проба${ago(behind)} — поймано классов ${caught}, у ${unknown} поимка не доказана (${cmd})`,
125
+ caught: ({ caught, behind }) => `брак в ваших файлах: проба${ago(behind)} поймала все ${caught} подсаженных классов — только тех, что есть в каталоге`,
126
+ nothing: ({ behind }, cmd) => `брак в ваших файлах: проба${ago(behind)} ничего не подсадила — ${cmd}`,
127
+ old: ({ behind }, cmd) => `брак в ваших файлах: проба была${ago(behind)}, итог покажет ${cmd}`,
128
+ },
129
+ limitsCi: "конвейер: прошёл ли он, отсюда не видно — проверяется только, что гейты в нём объявлены",
99
130
 
100
131
  gatesHeading: "Гейты",
101
132
  langs: "языки",
@@ -222,7 +253,7 @@ export const ru = {
222
253
  has_agent_entry: ["свода для агента здесь нет", "свод для агента есть"],
223
254
  has_ui: ["не видно стилей и компонентов интерфейса", "интерфейс есть: стили или компоненты"],
224
255
  has_mcp: ["агенту здесь не подключали внешних инструментов через MCP", "MCP-серверы объявлены"],
225
- has_api_spec: ["не видно спецификации API", "спецификация API есть"],
256
+ has_api_spec: ["не видно договора API: ни файла OpenAPI, ни tRPC, ts-rest или провайдера типов Fastify", "договор API есть: файл спецификации или схемы в коде"],
226
257
  },
227
258
  },
228
259
 
@@ -249,5 +280,45 @@ export const ru = {
249
280
  alreadyDeclared: "уже объявлен",
250
281
  },
251
282
 
283
+ // Задание агенту одним текстом (`aqk prompt`). У КАЖДОГО пункта хвост «Готово — …» с
284
+ // командой-арбитром: пункт без неё агент закроет словами «сделал» — это сторожит модульная проверка.
285
+ prompt: {
286
+ title: "# Задание: довести проверки проекта до рабочих",
287
+ intro: "Составлено AQK по состоянию репозитория. Выполняй пункты по порядку.",
288
+ rulesTitle: "## Правила",
289
+ rules: [
290
+ "Команды бери только из этого задания и из репозитория. Не выдумывай.",
291
+ "Сначала запусти проверку пункта и убедись, что она красная. Потом чини. Готово — когда та же команда зелёная.",
292
+ "Чини код, а не проверку: не ослабляй порог, не добавляй исключений, не выключай гейт. Считаешь проверку неправой — остановись и спроси владельца.",
293
+ "Решение, которое может принять только владелец (что ставить в проект, какие правила вводить), — спроси, а не угадывай.",
294
+ "Меняй только то, что нужно для этих пунктов.",
295
+ ],
296
+ tasksTitle: "## Что сделать",
297
+ empty: "Делать нечего: красных гейтов нет, проба ничего не нашла, ставить нечего. Всё равно выполни проверку ниже.",
298
+ done: "Готово —",
299
+ item: {
300
+ init: (s) => `Заведи манифест: \`${s} init\` — без него остальные команды отказывают. Готово — \`${s} doctor\` показывает уровень.`,
301
+ runNone: (s) => `Прогона ещё не было. Запусти \`${s} doctor --run\` и почини то, что покраснеет, по одному гейту: \`${s} doctor --run --only <имя>\`. Готово — прогон зелёный.`,
302
+ runStale: (s, when) => `Прогон ${when} старше последнего коммита — список красных ниже может быть про другой код. Запусти \`${s} doctor --run\` заново. Готово — у тебя свежий итог, и пункты ниже сверены с ним.`,
303
+ missed: ({ slug, file }, s) => `Гейт \`${slug}\` стоит, но пропустил брак, который проба подсадила в \`${file}\`. Разберись почему — частая причина: гейт не смотрит этот тип файлов или каталог; подробности даст \`${s} probe\`. Готово — \`${s} probe\` больше не называет этот класс.`,
304
+ red: (name, s) => `Гейт \`${name}\` красный. Запусти \`${s} doctor --run --only ${name}\`, прочитай находки и почини код. Готово — эта команда зелёная.`,
305
+ blind: ({ slug, file, command }, s) => `Проба подсадила брак класса \`${slug}\` в \`${file}\`, и проверки проекта его не заметили. Поставь проверку: \`${s} add ${slug}\`${command ? ` (то же одной строкой, без комплекта: \`${command}\`)` : ""}. Готово — \`${s} doctor --run --only ${slug}\` проходит, а \`${s} probe\` больше не называет этот класс.`,
306
+ adopt: (gates, s) => `У проекта уже есть свои проверки: ${gates.map((g) => `\`${g.cmd}\` (${g.source})`).join(", ")}. Впиши их в gates: манифеста .aqk.yml — ${gates.map((g) => `\`${g.name}: "${g.cmd}"\``).join(", ")}. Готово — \`${s} doctor --run\` их запускает.`,
307
+ shim: {
308
+ missing: (s) => `Claude Code здесь настроен, а правила лежат в AGENTS.md — он читает только CLAUDE.md. Создай CLAUDE.md с одной строкой \`@AGENTS.md\`. Готово — \`${s} doctor\` об этом больше не предупреждает.`,
309
+ noImport: (s) => `CLAUDE.md не подключает AGENTS.md — Claude Code видит только CLAUDE.md. Добавь в CLAUDE.md строку \`@AGENTS.md\` (упоминание словами файл не загружает). Готово — \`${s} doctor\` об этом больше не предупреждает.`,
310
+ },
311
+ // Запись каталога — предложение, а не факт о проекте: ставить или нет, решает владелец.
312
+ // Красный гейт и брак из пробы — факты, там «почини»; здесь — «предложи».
313
+ start: ({ slug, intent, command }, s) => `Предложи владельцу проверку \`${slug}\`${intent ? ` — ${intent}` : ""}. Согласен — \`${s} add ${slug}\`${command ? ` (то же одной строкой, без комплекта: \`${command}\`)` : ""}. Краснеет на существующем коде — не глуши её, покажи находки владельцу. Готово — \`${s} doctor --run --only ${slug}\` проходит, а \`${s} prove\` показывает её доказанной.`,
314
+ },
315
+ more: (n, s) => `И ещё ${n} — полный список: \`${s} doctor\`. Сначала закончи эти.`,
316
+ verifyTitle: "## Как проверить, что готово",
317
+ verify: (s) => [
318
+ `\`${s} doctor --run\` — прогон зелёный.`,
319
+ `\`${s} prove\` — ни один гейт не сломан: каждый краснеет на своём красном образце.`,
320
+ "В отчёте назови эти команды и их итог. «Выглядит рабочим» — не готово.",
321
+ ],
322
+ },
252
323
  ...ruGates,
253
324
  };