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
@@ -86,3 +86,35 @@ test("typedFrom: реплика блоками собирается из тек
86
86
  });
87
87
  assert.equal(typedFrom(line)[0].text, "всегда так");
88
88
  });
89
+
90
+ // ПОВТОР — СИГНАЛ, КОТОРЫЙ ДАЁТ САМ ЧЕЛОВЕК. Разбор AgentLint 2026-09-11: их SS2 сопоставляет
91
+ // поправку с правилом свода по словам. Замер на логах владельца: из 45 поправок к записанным
92
+ // правилам относятся от силы две — такой приём дал бы шум. Зато человек сам помечает повтор:
93
+ // «я же говорил», «опять», «снова». Из 14 таких реплик в одном проекте настоящих норм 5–6, и
94
+ // прежний отбор по маркерам наставления не ловил НИ ОДНОЙ.
95
+ test("isRepeat: «я же говорил», «опять», «снова» — повтор; «опять же» и вопрос-недоумение — нет", async () => {
96
+ const { isRepeat } = await import("../commands/learn.mjs");
97
+ assert.equal(isRepeat("нет старые задачи не трогаем я же говорил только пусть новые так работают"), true);
98
+ assert.equal(isRepeat("стой зачем ты сумму убрал я же не просил этого ты опять не верно меня понял"), true);
99
+ assert.equal(isRepeat("ок давай запишем, только опять не хочу плодить файлы"), true);
100
+ assert.equal(isRepeat("I told you not to touch the lockfile"), true);
101
+ assert.equal(isRepeat("и сказать в чате, опять же, где что смотреть"), false, "«опять же» — связка, а не повтор");
102
+ assert.equal(isRepeat("ок го дальше"), false);
103
+ // Второй прогон на тех же логах: половина шума — вопросы-недоумения и вставленные цитаты агента.
104
+ assert.equal(isRepeat("не понял еще нужно что то опять в env добавить?"), false, "вопрос с «опять» — недоумение, а не норма");
105
+ assert.equal(isRepeat("⬜ Два мира ролей — всплывёт, если снова будем трогать роли. поясни"), false, "цитата ответа агента");
106
+ assert.equal(isRepeat("почему ты так долго тестируешь я же говорю не нужно столько тестов?"), true, "сильная пометка держится и с вопросом");
107
+ });
108
+
109
+ // Повтор, совпавший с правилом свода, — главное, что может сказать эта команда: правило
110
+ // записано, а человек всё равно поправляет. Текстом оно не держится — ему нужен сторож.
111
+ test("repeatedRules: повтор, совпавший с правилом свода, называет это правило", async () => {
112
+ const { repeatedRules } = await import("../commands/learn.mjs");
113
+ const entry = "# Правила\n\n- **Не плодить файлы.** Новый файл — только если без него нельзя. <!-- aqk: человек -->\n" +
114
+ "- **Секреты только в .env.** <!-- aqk: secrets-not-in-code -->\n- обычная строка списка без метки\n";
115
+ const hits = repeatedRules([{ text: "опять ты плодишь файлы, я же говорил", when: "2026-09-03" }], entry);
116
+ assert.equal(hits.length, 1);
117
+ assert.match(hits[0].rule, /Не плодить файлы/);
118
+ assert.equal(hits[0].arbiter, "человек");
119
+ assert.equal(repeatedRules([{ text: "опять не тот цвет кнопки", when: "x" }], entry).length, 0, "повтор не про правило — не сюда");
120
+ });
@@ -11,7 +11,7 @@ import test from "node:test";
11
11
  import assert from "node:assert/strict";
12
12
  import { commandFor, verdict } from "../lib/prove.mjs";
13
13
  import { assessLevel, layoutChecks, unknownKeys, KNOWN_KEYS, parseManifest, coversOf, coversUnproven, unparsedLines } from "../lib/manifest.mjs";
14
- import { pickLang, langFromText } from "../i18n/index.mjs";
14
+ import { pickLang, langFromText, langFromDocs } from "../i18n/index.mjs";
15
15
  import { progress, selectGates } from "../lib/run.mjs";
16
16
 
17
17
  // --- строка «идёт» во время прогона ---------------------------------------------------
@@ -214,6 +214,26 @@ test("мусор в поле lang не молчит, а просто не дей
214
214
  assert.equal(pickLang({}, { lang: "клингонский" }), "en");
215
215
  });
216
216
 
217
+ // Отзыв с живого проекта 2026-09-11: свод правил и методички на русском, машина на Windows без
218
+ // LANG — и весь вывод английский, пока руками не впишешь `lang:`. Язык текста, который проект
219
+ // сам о себе написал, — свойство проекта; поэтому он выше локали и ниже манифеста.
220
+ test("язык свода проекта сильнее локали и слабее манифеста", () => {
221
+ const ruDoc = "# Правила\n\nПеред коммитом запусти `npm run check` и `make gates`. Схемы живут в " +
222
+ "`packages/contract`, их проверяет сервер.\n\n```sh\nnpm run typecheck && npm run lint\n```\n" +
223
+ "Любое изменение договора проходит через ревью и фиксируется в журнале решений.\n";
224
+ const enDoc = "# Rules\n\nBefore committing run `npm run check`. Schemas live in packages/contract " +
225
+ "and the server validates them. Every contract change goes through review and is logged.\n";
226
+ assert.equal(langFromDocs(ruDoc.repeat(2)), "ru", "русский свод с кодом внутри не узнан");
227
+ assert.equal(langFromDocs(enDoc.repeat(2)), "en");
228
+ assert.equal(langFromDocs("# X\n\n`npm test`\n"), "", "по трём словам язык не решается");
229
+ assert.equal(pickLang({ LANG: "en_US.UTF-8" }, null, "ru"), "ru");
230
+ assert.equal(pickLang({}, null, "ru"), "ru", "Windows без LANG: свод решает");
231
+ assert.equal(pickLang({ LANG: "ru_RU.UTF-8" }, null, "en"), "en");
232
+ assert.equal(pickLang({}, { lang: "en" }, "ru"), "en", "манифест сильнее свода");
233
+ assert.equal(pickLang({ AQK_LANG: "en" }, null, "ru"), "en", "переменная сильнее свода");
234
+ assert.equal(pickLang({ LANG: "ru_RU.UTF-8" }, null, ""), "ru", "свод молчит — решает локаль");
235
+ });
236
+
217
237
  // Сокращённый разбор языка в i18n/index.mjs существует потому, что каталог строк нужен раньше,
218
238
  // чем кто-либо успеет прочитать манифест целиком. Два разбора одного файла — то же, что два
219
239
  // свода правил: через месяц они расходятся, и непонятно, какой настоящий. Сверяем ответы.
@@ -0,0 +1,106 @@
1
+ // tool/selfcheck/units-prompt.mjs — задание для агента одним текстом (`aqk prompt`).
2
+ //
3
+ // ЗАЧЕМ. Между диагнозом и действием не было моста: `doctor` пишет человеку, `context` — «как
4
+ // дела» агенту, а «почини вот это, это и это» человек пересказывал сам. Идея — из разбора
5
+ // agentlint (research/competitors/agentlint.md, «Задание для агента»): правила поведения
6
+ // сверху, исправления по весу, в конце — как проверить. Их слабость не берём: у них «проверь» —
7
+ // это «балл вырос», у нас — команда, которая краснеет и зеленеет.
8
+ //
9
+ // node --test tool/selfcheck/units-prompt.mjs
10
+ import test from "node:test";
11
+ import assert from "node:assert/strict";
12
+ import { taskText } from "../commands/prompt.mjs";
13
+ import { CATALOGS } from "../i18n/index.mjs";
14
+
15
+ const T = CATALOGS.ru.prompt;
16
+ const base = {
17
+ self: "aqk", manifest: true, run: { when: "2026-09-11 12:00", red: [], stale: false },
18
+ blind: [], adopt: [], shim: null, start: [],
19
+ };
20
+ const text = (s) => taskText({ ...base, ...s }, T).join("\n");
21
+ const items = (s) => text(s).split("\n").filter((l) => /^\d+\. /.test(l));
22
+
23
+ test("правила поведения сверху и проверка в конце — всегда, даже когда пунктов нет", () => {
24
+ const t = text({});
25
+ assert.ok(t.indexOf(T.rulesTitle) < t.indexOf(T.verifyTitle), "правила идут до проверки");
26
+ assert.match(t, /aqk doctor --run/, "без команды проверки «готово» ничем не доказано");
27
+ assert.match(t, new RegExp(T.empty.slice(0, 20)), "пустое задание сказано словами");
28
+ });
29
+
30
+ test("красный гейт — пункт с командой, которая его запускает одного", () => {
31
+ const it = items({ run: { when: "x", red: ["lint", "units"], stale: false } });
32
+ assert.equal(it.length, 2);
33
+ assert.match(it[0], /aqk doctor --run --only lint/);
34
+ });
35
+
36
+ // Прогона не было или он старше последнего коммита: список красных — про другой код. Первый
37
+ // пункт — прогнать; пустое задание при этом было бы утверждением «чисто», которого никто не делал.
38
+ test("нет прогона или он устарел — первый пункт «прогони», а не «всё чисто»", () => {
39
+ const none = text({ run: null });
40
+ assert.doesNotMatch(none, new RegExp(T.empty.slice(0, 20)), "без прогона задание не пустое");
41
+ assert.match(items({ run: null })[0], /aqk doctor --run/);
42
+ const stale = items({ run: { when: "x", red: ["lint"], stale: true } });
43
+ assert.match(stale[0], /aqk doctor --run/, "устаревший прогон — сначала прогнать заново");
44
+ });
45
+
46
+ test("порядок по весу: манифест → красное → брак из пробы → свои проверки → свод → начало", () => {
47
+ const it = items({
48
+ manifest: false,
49
+ run: { when: "x", red: ["lint"], stale: false },
50
+ blind: [{ slug: "swallowed-error", file: "src/a.py", command: "ruff check --select BLE ." }],
51
+ adopt: [{ name: "test", cmd: "npm test", source: "package.json" }],
52
+ shim: "missing",
53
+ start: [{ slug: "secrets-not-in-code", intent: "ключи не в коде", command: "gitleaks dir ." }],
54
+ });
55
+ assert.match(it[0], /aqk init/);
56
+ assert.match(it[1], /lint/);
57
+ assert.match(it[2], /swallowed-error.*src\/a\.py/);
58
+ assert.match(it[3], /npm test/);
59
+ assert.match(it[4], /@AGENTS\.md/);
60
+ assert.equal(it.length, 5, "больше пяти за раз агент не удержит");
61
+ assert.match(text({
62
+ manifest: false, run: { when: "x", red: ["lint"], stale: false },
63
+ blind: [{ slug: "s", file: "f", command: "" }], adopt: [{ name: "t", cmd: "c", source: "p" }],
64
+ shim: "missing", start: [{ slug: "x", intent: "y", command: "" }],
65
+ }), new RegExp(T.more(1).slice(0, 12)), "остаток назван числом, а не выброшен молча");
66
+ });
67
+
68
+ test("класс из пробы не повторяется в «начните с»; без готовой команды — aqk add", () => {
69
+ const it = items({
70
+ blind: [{ slug: "swallowed-error", file: "src/a.py", command: "" }],
71
+ start: [{ slug: "swallowed-error", intent: "i", command: "" }, { slug: "todo-without-task", intent: "i", command: "" }],
72
+ });
73
+ assert.equal(it.length, 2);
74
+ assert.match(it[0], /aqk add swallowed-error/);
75
+ assert.match(it[1], /todo-without-task/);
76
+ });
77
+
78
+ // Гейт стоит, проба его гоняла — и он пропустил. Не «поставь» (стоит), а «здесь он слеп».
79
+ test("гейт стоит, но пропустил брак из пробы — пункт «разберись», сразу после красных", () => {
80
+ const it = items({
81
+ run: { when: "x", red: ["lint"], stale: false },
82
+ missed: [{ slug: "swallowed-error", file: "src/a.py" }],
83
+ start: [{ slug: "todo-without-task", intent: "i", command: "" }],
84
+ });
85
+ assert.match(it[1], /swallowed-error.*src\/a\.py/);
86
+ assert.doesNotMatch(it[1], /aqk add/, "гейт уже стоит — ставить его второй раз бессмысленно");
87
+ });
88
+
89
+ test("у каждого пункта — чем доказать, что готово", () => {
90
+ const it = items({
91
+ run: { when: "x", red: ["lint"], stale: false },
92
+ blind: [{ slug: "swallowed-error", file: "src/a.py", command: "" }],
93
+ missed: [{ slug: "complexity-limit", file: "src/b.py" }],
94
+ start: [{ slug: "todo-without-task", intent: "i", command: "" }],
95
+ });
96
+ for (const line of it) assert.match(line, new RegExp(T.done.slice(0, 6)), `пункт без арбитра: ${line}`);
97
+ });
98
+
99
+ test("оба языка несут одни и те же ключи задания", () => {
100
+ const keys = (o) => Object.keys(o).sort().join(",");
101
+ assert.equal(keys(CATALOGS.ru.prompt), keys(CATALOGS.en.prompt));
102
+ // Пункты лежат глубже — пропущенный в одном языке пункт упал бы ошибкой только у того, кто
103
+ // на этом языке работает.
104
+ assert.equal(keys(CATALOGS.ru.prompt.item), keys(CATALOGS.en.prompt.item));
105
+ assert.equal(keys(CATALOGS.ru.prompt.item.shim), keys(CATALOGS.en.prompt.item.shim));
106
+ });
@@ -8,7 +8,7 @@
8
8
  // node --test tool/selfcheck/units-repo.mjs
9
9
  import test from "node:test";
10
10
  import assert from "node:assert/strict";
11
- import { triggerVerdict, recipeFor, EXT_LANG, whichSync, browserServerAdvice, MARKS, isApiSpec } from "../lib/repo.mjs";
11
+ import { triggerVerdict, recipeFor, EXT_LANG, whichSync, browserServerAdvice, MARKS, isApiSpec, claudeSeesRules } from "../lib/repo.mjs";
12
12
  import { startWith } from "../lib/advice.mjs";
13
13
  import { proposeGates } from "../lib/adopt.mjs";
14
14
  import { CATALOGS, L } from "../i18n/index.mjs";
@@ -308,3 +308,22 @@ test("совет проекту на Biome: команда Biome, а не eslint
308
308
  const none = blindAdvice({ ...entry, biome_rules: "none" }, { langs: new Set(["javascript"]), has_biome: true });
309
309
  assert.match(none.command, /^eslint /);
310
310
  });
311
+
312
+ // --- свод виден Claude Code --------------------------------------------------------
313
+ // Документация Claude Code (code.claude.com/docs/en/memory, раздел AGENTS.md, сверено
314
+ // 2026-09-11): «Claude Code reads CLAUDE.md, not AGENTS.md». Рекомендовано: CLAUDE.md с
315
+ // @AGENTS.md либо символическая ссылка. Проект, где Claude Code настроен, а свод лежит только
316
+ // в AGENTS.md, пишет правила агенту, который их не читает.
317
+ test("свод в AGENTS.md виден Claude Code только через CLAUDE.md с @AGENTS.md", () => {
318
+ const base = { agents: true, claude: null, claudeLink: false, dotClaude: false };
319
+ assert.equal(claudeSeesRules(base), null, "Claude Code не настроен — молчим: Codex и Cursor читают AGENTS.md сами");
320
+ assert.equal(claudeSeesRules({ ...base, dotClaude: true }), "missing");
321
+ assert.equal(claudeSeesRules({ ...base, claude: "# Rules\n\nSee AGENTS.md for everything.\n" }), "noImport",
322
+ "упоминание словами — не подключение: файл Claude Code в контекст не загрузит");
323
+ assert.equal(claudeSeesRules({ ...base, claude: "# CLAUDE.md\n\n@AGENTS.md\n" }), null);
324
+ assert.equal(claudeSeesRules({ ...base, claude: "@../AGENTS.md\n" }), null, "из .claude/CLAUDE.md свод подключают на уровень выше");
325
+ assert.equal(claudeSeesRules({ ...base, claude: "Правила — в `@AGENTS.md`.\n" }), "noImport",
326
+ "в обратных кавычках @ не подключает — так в документации");
327
+ assert.equal(claudeSeesRules({ ...base, claude: "x", claudeLink: true }), null, "ссылка на AGENTS.md — тот же файл");
328
+ assert.equal(claudeSeesRules({ ...base, agents: false, dotClaude: true }), null, "AGENTS.md нет — подключать нечего");
329
+ });