agent-quality-kit 0.13.0 → 0.15.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 (57) hide show
  1. package/README.md +46 -8
  2. package/README.ru.md +49 -9
  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 +59 -44
  18. package/tool/commands/doctor-catalog.mjs +222 -0
  19. package/tool/commands/doctor.mjs +46 -237
  20. package/tool/commands/feedback.mjs +157 -0
  21. package/tool/commands/learn.mjs +119 -19
  22. package/tool/commands/probe.mjs +4 -2
  23. package/tool/commands/project.mjs +11 -13
  24. package/tool/commands/prompt.mjs +70 -0
  25. package/tool/i18n/en-docs.mjs +1 -0
  26. package/tool/i18n/en-gates.mjs +27 -0
  27. package/tool/i18n/en.mjs +70 -3
  28. package/tool/i18n/index.mjs +42 -3
  29. package/tool/i18n/ru-docs.mjs +1 -0
  30. package/tool/i18n/ru-gates.mjs +28 -0
  31. package/tool/i18n/ru.mjs +81 -3
  32. package/tool/lib/annotate.mjs +66 -0
  33. package/tool/lib/ask.mjs +118 -0
  34. package/tool/lib/brief.mjs +17 -38
  35. package/tool/lib/cadence.mjs +30 -1
  36. package/tool/lib/core.mjs +8 -6
  37. package/tool/lib/gate-worker.mjs +4 -1
  38. package/tool/lib/repo.mjs +45 -4
  39. package/tool/lib/run.mjs +140 -9
  40. package/tool/program.mjs +10 -0
  41. package/tool/selfcheck/smoke/_fixture.mjs +8 -3
  42. package/tool/selfcheck/smoke/api-contract.test.mjs +37 -0
  43. package/tool/selfcheck/smoke/corpus.test.mjs +151 -0
  44. package/tool/selfcheck/smoke/fail-closed.test.mjs +96 -1
  45. package/tool/selfcheck/smoke/first-run.test.mjs +39 -0
  46. package/tool/selfcheck/smoke/verdict.test.mjs +41 -2
  47. package/tool/selfcheck/smoke.sh +4 -0
  48. package/tool/selfcheck/units-annotate.mjs +67 -0
  49. package/tool/selfcheck/units-ask.mjs +85 -0
  50. package/tool/selfcheck/units-brief.mjs +3 -13
  51. package/tool/selfcheck/units-cadence.mjs +26 -1
  52. package/tool/selfcheck/units-context.mjs +2 -1
  53. package/tool/selfcheck/units-feedback.mjs +137 -0
  54. package/tool/selfcheck/units-learn.mjs +32 -0
  55. package/tool/selfcheck/units-level.mjs +21 -1
  56. package/tool/selfcheck/units-prompt.mjs +106 -0
  57. 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 };
@@ -6,7 +6,8 @@ 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, ensureIgnored } from "../lib/core.mjs";
9
+ copyDir, writeIfAbsent, stateDirs, docPath, ensureIgnored } from "../lib/core.mjs";
10
+ import { askAllowed, markAsked } from "../lib/ask.mjs";
10
11
  import { AGENTS_MD, CLAUDE_MD, MANIFEST_YML } from "../lib/templates.mjs";
11
12
  import { banner } from "../lib/banner.mjs";
12
13
  import { readManifest } from "../lib/manifest.mjs";
@@ -95,7 +96,10 @@ ${c.dim(L.init.burned(`${SELF} note "…"`))}
95
96
  // Ничего не постится само: ссылки печатаются, дальше решает человек. Обратная связь важнее
96
97
  // звезды, но без звезды меньше шансов, что кто-то вообще дойдёт до фидбека.
97
98
  async function maybeAskFeedback() {
98
- if (await exists(FEEDBACK_MARK)) return;
99
+ // Ограничитель — общий на все обращения комплекта (ask.mjs). Вид `install` разовый и живёт
100
+ // в доме пользователя: второй init в другом репозитории на том же компьютере молчит.
101
+ const dirs = stateDirs();
102
+ if (!(await askAllowed("install", dirs))) return;
99
103
  const url = REPO_URL;
100
104
  console.log(`
101
105
  ${c.bold(L.feedback.title)}
@@ -104,20 +108,14 @@ ${c.bold(L.feedback.title)}
104
108
  ${url}/issues/new
105
109
  ${c.dim(` ${L.feedback.once}`)}
106
110
  `);
107
- // Пометка «уже показывали» — удобство, а не работа команды. Домашнего каталога может не быть
108
- // записываемым вовсе: в контейнере, запущенном `--user 1001:127`, у этого uid нет записи в
109
- // /etc/passwd, `homedir()` даёт «/», и запись падает с EACCES на `/.config`. До 2026-09-09
110
- // это роняло ВЕСЬ `init` — то есть любого, кто набрал команду из нашей же документации по
111
- // docker. Локально не воспроизводилось случайно: uid разработчика 1000 совпадает с
112
- // пользователем `node` в образе, у которого дом есть. Нашёл конвейер, где uid 1001.
111
+ // Пометка «уже показывали» — удобство, а не работа команды: `markAsked` не бросает, а
112
+ // возвращает, записалось ли. Дом бывает недоступен для записи контейнере с `--user
113
+ // 1001:127` у этого uid нет записи в /etc/passwd, homedir() даёт «/»), и до 2026-09-09 это
114
+ // роняло ВЕСЬ `init` — то есть любого, кто набрал команду из нашей же документации по docker.
113
115
  //
114
116
  // Молча глотать нельзя — это то, что красит наш же swallowed-error. Поэтому вслух: не
115
117
  // запомнили, покажем снова. Установка при этом доходит до конца.
116
- try {
117
- await writeIfAbsent(FEEDBACK_MARK, "shown\n", { force: false });
118
- } catch {
119
- console.log(c.dim(` ${L.feedback.notRemembered}`));
120
- }
118
+ if (!(await markAsked("install", dirs))) console.log(c.dim(` ${L.feedback.notRemembered}`));
121
119
  }
122
120
 
123
121
  function findJournal() {
@@ -0,0 +1,70 @@
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 { readAdvice, portableSelf } from "./context.mjs";
19
+ import { readRun } from "../lib/run.mjs";
20
+ import { probeStatus } from "./probe.mjs";
21
+ import { L } from "../i18n/index.mjs";
22
+
23
+ // Больше пяти пунктов за раз агент не удержит — как и человек; остальное называется числом.
24
+ const MAX_ITEMS = 5;
25
+
26
+ // Чистая функция: состояние → строки задания. Порядок — по тому, насколько пункт ФАКТ о проекте
27
+ // и насколько без него невыполнимы остальные: манифест → прогон → красное → гейт стоит, но
28
+ // пропустил брак из пробы → брак, для которого гейта нет → проверки, которые у проекта уже есть
29
+ // → свод, невидимый Claude Code → «начните с этих трёх» из каталога. Класс из пробы в последнем
30
+ // списке не повторяется.
31
+ function taskText(st, T = L.prompt) {
32
+ const self = st.self || "aqk";
33
+ const it = [];
34
+ if (!st.manifest) it.push(T.item.init(self));
35
+ if (!st.run) it.push(T.item.runNone(self));
36
+ else if (st.run.stale) it.push(T.item.runStale(self, st.run.when));
37
+ for (const name of st.run?.red || []) it.push(T.item.red(name, self));
38
+ for (const m of st.missed || []) it.push(T.item.missed(m, self));
39
+ for (const b of st.blind || []) it.push(T.item.blind(b, self));
40
+ if ((st.adopt || []).length) it.push(T.item.adopt(st.adopt, self));
41
+ if (st.shim) it.push(T.item.shim[st.shim](self));
42
+ const seen = new Set((st.blind || []).map((b) => b.slug));
43
+ for (const s of st.start || []) if (!seen.has(s.slug)) it.push(T.item.start(s, self));
44
+
45
+ const out = [T.title, "", T.intro, "", T.rulesTitle, ...T.rules.map((r) => `- ${r}`), "", T.tasksTitle];
46
+ if (!it.length) out.push(T.empty);
47
+ it.slice(0, MAX_ITEMS).forEach((line, i) => out.push(`${i + 1}. ${line}`));
48
+ if (it.length > MAX_ITEMS) out.push(T.more(it.length - MAX_ITEMS, self));
49
+ out.push("", T.verifyTitle, ...T.verify(self).map((r) => `- ${r}`));
50
+ return out;
51
+ }
52
+
53
+ async function cmdPrompt() {
54
+ const man = await readManifest();
55
+ let probe = null;
56
+ try { probe = await probeStatus(); } catch { /* пробы нет — пунктов из неё не будет */ }
57
+ let advice = { adopt: [], blind: [], start: [], missed: [] };
58
+ try { advice = await readAdvice(man, probe); } catch { /* не посчитали — выдумывать пункты нельзя */ }
59
+ // Команда уходит в чужой контекст и, возможно, в чужие руки: абсолютный путь к нашей
60
+ // программе там не сработает — тот же довод, что у хука `context --install`.
61
+ console.log(taskText({
62
+ self: portableSelf(SELF),
63
+ manifest: Boolean(man),
64
+ run: await readRun(),
65
+ shim: await claudeShimFor(CWD),
66
+ ...advice,
67
+ }).join("\n"));
68
+ }
69
+
70
+ export { cmdPrompt, taskText };
@@ -63,6 +63,7 @@ const enDocs = {
63
63
  "No run has been made — which checks are red is UNKNOWN. This is not \"clean\": `aqk doctor --run`.",
64
64
  runStale: (when) =>
65
65
  `The last run ${when} is OLDER than the last commit — it describes different code.`,
66
+ runCannot: (names) => `COULD NOT CHECK (the checks themselves failed — not findings about the code): ${names}`,
66
67
  runClean: (when) => `Last run ${when} — nothing red.`,
67
68
  runRed: (when, names) => `Last run ${when} — RED: ${names}.`,
68
69
  andMore: (n) => `and ${n} more`,
@@ -185,6 +185,33 @@ export const enGates = {
185
185
  issue: "Found a bug or it did not fit — open an issue; both are the most useful feedback there is.",
186
186
  notRemembered: "could not remember this (home is not writable) — the message will appear again",
187
187
  once: "This message is shown once: it will not appear again on this machine.",
188
+
189
+ ask: {
190
+ cannot: (names, cmd) =>
191
+ `AQK could not check: ${names}. That is the most valuable thing to tell the author — ${cmd}`,
192
+ blind: (names, cmd) =>
193
+ `The probe found defect classes nobody catches here: ${names}. Does that match your experience? — ${cmd}`,
194
+ red: (names, cmd) =>
195
+ `The kit caught: ${names}. One line to the author — did it help: ${cmd}`,
196
+ once: "Asked once per project; switch it off with AQK_FEEDBACK=0",
197
+ agent: "Say this to the human in one sentence, and do not insist if they do not answer.",
198
+ },
199
+ how: "Sending is one click (a prefilled issue opens; the text stays editable):",
200
+ issueTitle: "Feedback on the kit",
201
+ nothingSent: "Nothing was sent: the kit makes no outgoing requests other than the version check.",
202
+ orPaste: (cmd) => `No GitHub? Forward the text above as is — all of it comes from ${cmd}`,
203
+ report: {
204
+ title: "### Feedback on the kit",
205
+ unknown: "unknown",
206
+ none: "none",
207
+ env: (v, node, os) => `version: ${v} · node: ${node} · system: ${os}`,
208
+ level: (x) => `level: ${x}`,
209
+ stack: (x) => `stack: ${x}`,
210
+ gates: (n, red, cannot) => `gates declared: ${n} · red: ${red} · could not check: ${cannot}`,
211
+ blind: (x) => `classes nobody catches here: ${x}`,
212
+ say: "What you would say in your own words (one line — the most useful part of the whole message):",
213
+ mark: (v) => `<!-- collected by "aqk feedback" ${v}: no paths, no code, no repository name -->`,
214
+ },
188
215
  },
189
216
 
190
217
  note: {
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:
@@ -44,7 +50,9 @@ export const en = {
44
50
  blob: "assemble the guides into a single GOD_AI.md",
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",
53
+ feedback: "a report on how the kit worked plus a prefilled link — the only payment it asks",
47
54
  vitals: "is what the kit runs on wired up: gate tools, hooks, version freshness",
55
+ prompt: "one task for the agent: what to fix, in order, and how to prove it is done",
48
56
  contextInstall: "the same in full — the map and the rulebook — installed as a hook",
49
57
  report: "the mandatory report form: what is in place, what is not, what was not read; --since <ref> adds what proves the diff",
50
58
  badge: "a level badge for your README — and a check that it does not lie",
@@ -73,6 +81,14 @@ export const en = {
73
81
  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
82
  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
83
  selectSkipped: (names) => `not run (by --only/--skip): ${names} — their state is unknown, they are not "green"`,
84
+ claudeShim: {
85
+ 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\".",
86
+ 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).",
87
+ },
88
+ annotDropped: (n, more) => `pull request annotations: ${n}, ${more} more not shown — GitHub takes about ten per step; every finding is in the log above`,
89
+ heldQuiet: (n, cmd) => `held by the machine: ${n} — by name: ${cmd}`,
90
+ skipQuiet: (n, cmd) => `not applicable to this repository: ${n} — by name and why: ${cmd}`,
91
+ passedQuiet: (n) => `${n} more passed — by name: --verbose`,
76
92
  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
93
  rulesByHuman: (total, machine, human) =>
78
94
  `${human} of ${total} rules in the entry point are guarded by a HUMAN, ${machine} by a machine.`,
@@ -95,6 +111,17 @@ export const en = {
95
111
  toReach: (n) => `To reach AQK-${n}:`,
96
112
  gives: (what) => `What it buys you: ${what}`,
97
113
  allDone: "All levels reached.",
114
+ limitsTitle: "A level measures tooling, not reliability. What it does not prove:",
115
+ limitsProbe: {
116
+ never: (_, cmd) => `defects in your files: the probe has never run — ${cmd}`,
117
+ off: () => "defects in your files: the probe is off (probe: 0) — whether your checks catch them is unknown",
118
+ blind: ({ names, behind }) => `defects in your files: the probe${ago(behind)} did NOT catch — ${names.join(", ")}`,
119
+ partial: ({ caught, unknown, behind }, cmd) => `defects in your files: the probe${ago(behind)} caught ${caught} classes, ${unknown} unproven (${cmd})`,
120
+ caught: ({ caught, behind }) => `defects in your files: the probe${ago(behind)} caught all ${caught} planted classes — only those the catalog has`,
121
+ nothing: ({ behind }, cmd) => `defects in your files: the probe${ago(behind)} planted nothing — ${cmd}`,
122
+ old: ({ behind }, cmd) => `defects in your files: the probe ran${ago(behind)}; ${cmd} shows the result`,
123
+ },
124
+ limitsCi: "pipeline: whether it passed is not visible from here — we only check that gates are declared in it",
98
125
 
99
126
  gatesHeading: "Gates",
100
127
  langs: "languages",
@@ -142,6 +169,10 @@ export const en = {
142
169
  `either fix them and drop them from advisory, or admit the rule does not exist.`,
143
170
  runHeading: "Running the declared gates",
144
171
  timeout: "did not finish within 5 minutes",
172
+ cannotCheck: (why) => `could not check: ${why}`,
173
+ whySpawn: (code) => `failed to start${code ? ` (${code})` : ""}`,
174
+ whySignal: (sig) => `killed by signal ${sig || "?"}`,
175
+ whyExit: (code) => `exit ${code} — for this command that is a failure, not a finding`,
145
176
  running: (i, n) => `[${i}/${n}] running…`,
146
177
  proving: "checking that the gates catch defects on their own samples…",
147
178
  exitCode: (code) => `exit ${code}`,
@@ -216,7 +247,7 @@ export const en = {
216
247
  has_agent_entry: ["no entry point for an agent here", "an entry point for an agent exists"],
217
248
  has_ui: ["no stylesheets or UI components in sight", "a UI exists: stylesheets or components"],
218
249
  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"],
250
+ 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
251
  },
221
252
  },
222
253
 
@@ -242,5 +273,41 @@ export const en = {
242
273
  alreadyDeclared: "already declared",
243
274
  },
244
275
 
276
+ prompt: {
277
+ title: "# Task: get this project's checks actually working",
278
+ intro: "Written by AQK from the current state of the repository. Do the items in order.",
279
+ rulesTitle: "## Ground rules",
280
+ rules: [
281
+ "Use only commands from this task and from the repository. Do not invent any.",
282
+ "Run an item's check first and see it fail. Then fix. Done means the same command passes.",
283
+ "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.",
284
+ "A decision only the owner can make (what to add to the project, which rules to adopt) — ask, do not guess.",
285
+ "Change only what these items need.",
286
+ ],
287
+ tasksTitle: "## What to do",
288
+ empty: "Nothing to do: no gate is red, the probe found nothing, nothing to add. Still run the verification below.",
289
+ done: "Done —",
290
+ item: {
291
+ init: (s) => `Create the manifest: \`${s} init\` — without it the other commands refuse. Done — \`${s} doctor\` shows a level.`,
292
+ 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.`,
293
+ 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.`,
294
+ 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.`,
295
+ red: (name, s) => `Gate \`${name}\` is red. Run \`${s} doctor --run --only ${name}\`, read the findings and fix the code. Done — that command passes.`,
296
+ 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.`,
297
+ 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.`,
298
+ shim: {
299
+ 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.`,
300
+ 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.`,
301
+ },
302
+ 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.`,
303
+ },
304
+ more: (n, s) => `And ${n} more — the full list: \`${s} doctor\`. Finish these first.`,
305
+ verifyTitle: "## How to verify it is done",
306
+ verify: (s) => [
307
+ `\`${s} doctor --run\` — the run passes.`,
308
+ `\`${s} prove\` — no gate is broken: each one fails on its own red sample.`,
309
+ "Name these commands and their results in your report. \"Looks like it works\" is not done.",
310
+ ],
311
+ },
245
312
  ...enGates,
246
313
  };
@@ -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 };
@@ -65,6 +65,7 @@ const ruDocs = {
65
65
  "Прогон не делался — какие проверки красные, НЕИЗВЕСТНО. Это не «чисто»: `aqk doctor --run`.",
66
66
  runStale: (when) =>
67
67
  `Последний прогон ${when} СТАРЕЕ последнего коммита — он описывает не тот код, что здесь.`,
68
+ runCannot: (names) => `НЕ СМОГЛИ ПРОВЕРИТЬ (сбой самих проверок, а не находки о коде): ${names}`,
68
69
  runClean: (when) => `Последний прогон ${when} — красных нет.`,
69
70
  runRed: (when, names) => `Последний прогон ${when} — КРАСНЫЕ: ${names}.`,
70
71
  andMore: (n) => `и ещё ${n}`,
@@ -187,6 +187,34 @@ export const ruGates = {
187
187
  issue: "Нашёл баг или не подошло — заведи Issue, самая полезная обратная связь: и то и другое.",
188
188
  notRemembered: "запомнить не удалось (дом недоступен для записи) — сообщение появится снова",
189
189
  once: "Это разовое сообщение: больше не покажется на этой машине.",
190
+
191
+ // Просьба ПО ДЕЛУ: у неё всегда есть содержание, иначе её нет вовсе.
192
+ ask: {
193
+ cannot: (names, cmd) =>
194
+ `AQK не смог проверить: ${names}. Это самое ценное, что можно рассказать автору — ${cmd}`,
195
+ blind: (names, cmd) =>
196
+ `Проба нашла классы брака, которые здесь не ловит никто: ${names}. Совпало с вашим опытом? — ${cmd}`,
197
+ red: (names, cmd) =>
198
+ `Комплект поймал: ${names}. Одна строка автору — помогло или нет: ${cmd}`,
199
+ once: "Спрашиваем один раз на проект; выключить: AQK_FEEDBACK=0",
200
+ agent: "Скажи это человеку одной фразой и не настаивай, если он не ответил.",
201
+ },
202
+ how: "Отправить — одно нажатие (откроется готовая задача, текст можно править):",
203
+ issueTitle: "Отзыв о работе комплекта",
204
+ nothingSent: "Ничего не отправлено: комплект не делает исходящих запросов, кроме проверки версии.",
205
+ orPaste: (cmd) => `Не хочется GitHub — перешлите текст выше как есть: он весь собран ${cmd}`,
206
+ report: {
207
+ title: "### Отзыв о комплекте",
208
+ unknown: "неизвестно",
209
+ none: "нет",
210
+ env: (v, node, os) => `версия: ${v} · node: ${node} · система: ${os}`,
211
+ level: (x) => `уровень: ${x}`,
212
+ stack: (x) => `стек: ${x}`,
213
+ gates: (n, red, cannot) => `гейтов объявлено: ${n} · красных: ${red} · не смогли проверить: ${cannot}`,
214
+ blind: (x) => `классы, которые здесь не ловит никто: ${x}`,
215
+ say: "Что сказать своими словами (одна строка — самое полезное во всём письме):",
216
+ mark: (v) => `<!-- собрано «aqk feedback» ${v}: без путей, без кода, без имени репозитория -->`,
217
+ },
190
218
  },
191
219
 
192
220
  note: {