agent-quality-kit 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +43 -7
  2. package/README.ru.md +44 -7
  3. package/kit/gates/api-contract-has-arbiter/README.md +16 -1
  4. package/kit/gates/api-contract-has-arbiter/check.sh +66 -23
  5. package/kit/gates/complexity-limit/gate.yml +4 -0
  6. package/kit/gates/dead-code/gate.yml +4 -0
  7. package/kit/gates/entry-commands-exist/README.md +64 -0
  8. package/kit/gates/entry-commands-exist/check.sh +110 -0
  9. package/kit/gates/entry-commands-exist/gate.yml +19 -0
  10. package/kit/gates/entry-commands-exist/green/AGENTS.md +13 -0
  11. package/kit/gates/entry-commands-exist/green/Makefile +6 -0
  12. package/kit/gates/entry-commands-exist/green/justfile +2 -0
  13. package/kit/gates/entry-commands-exist/green/package.json +10 -0
  14. package/kit/gates/entry-commands-exist/red/AGENTS.md +9 -0
  15. package/kit/gates/entry-commands-exist/red/Makefile +2 -0
  16. package/kit/gates/entry-commands-exist/red/package.json +9 -0
  17. package/kit/gates/env-secrets-not-committed/README.md +73 -0
  18. package/kit/gates/env-secrets-not-committed/check.sh +139 -0
  19. package/kit/gates/env-secrets-not-committed/gate.yml +21 -0
  20. package/kit/gates/env-secrets-not-committed/green/.aqk-tracked +10 -0
  21. package/kit/gates/env-secrets-not-committed/green/.env +10 -0
  22. package/kit/gates/env-secrets-not-committed/green/.env.production +5 -0
  23. package/kit/gates/env-secrets-not-committed/green/.env.test +2 -0
  24. package/kit/gates/env-secrets-not-committed/red/.aqk-tracked +5 -0
  25. package/kit/gates/env-secrets-not-committed/red/.env +7 -0
  26. package/kit/gates/no-print-in-prod/gate.yml +4 -0
  27. package/kit/gates/swallowed-error/gate.yml +4 -0
  28. package/kit/gates/todo-without-task/gate.yml +4 -0
  29. package/llms.txt +8 -2
  30. package/package.json +1 -1
  31. package/tool/commands/badge.mjs +1 -1
  32. package/tool/commands/context.mjs +81 -9
  33. package/tool/commands/doctor-catalog.mjs +222 -0
  34. package/tool/commands/doctor.mjs +81 -209
  35. package/tool/commands/learn.mjs +119 -19
  36. package/tool/commands/probe.mjs +50 -68
  37. package/tool/commands/project.mjs +6 -1
  38. package/tool/commands/prompt.mjs +69 -0
  39. package/tool/commands/report.mjs +1 -1
  40. package/tool/commands/vitals.mjs +15 -11
  41. package/tool/i18n/en-docs.mjs +22 -1
  42. package/tool/i18n/en-gates.mjs +6 -1
  43. package/tool/i18n/en.mjs +78 -4
  44. package/tool/i18n/index.mjs +42 -3
  45. package/tool/i18n/ru-docs.mjs +24 -1
  46. package/tool/i18n/ru-gates.mjs +6 -1
  47. package/tool/i18n/ru.mjs +87 -4
  48. package/tool/lib/adopt.mjs +15 -1
  49. package/tool/lib/advice.mjs +115 -0
  50. package/tool/lib/annotate.mjs +66 -0
  51. package/tool/lib/brief.mjs +3 -1
  52. package/tool/lib/cadence.mjs +40 -1
  53. package/tool/lib/core.mjs +40 -1
  54. package/tool/lib/gate-worker.mjs +18 -0
  55. package/tool/lib/history.mjs +34 -5
  56. package/tool/lib/manifest.mjs +65 -21
  57. package/tool/lib/repo.mjs +47 -35
  58. package/tool/lib/run.mjs +98 -9
  59. package/tool/program.mjs +4 -0
  60. package/tool/selfcheck/smoke/_fixture.mjs +8 -3
  61. package/tool/selfcheck/smoke/api-contract.test.mjs +37 -0
  62. package/tool/selfcheck/smoke/corpus.test.mjs +151 -0
  63. package/tool/selfcheck/smoke/first-run.test.mjs +144 -0
  64. package/tool/selfcheck/smoke/verdict.test.mjs +91 -3
  65. package/tool/selfcheck/smoke.sh +10 -2
  66. package/tool/selfcheck/units-annotate.mjs +67 -0
  67. package/tool/selfcheck/units-cadence.mjs +40 -1
  68. package/tool/selfcheck/units-context.mjs +64 -1
  69. package/tool/selfcheck/units-learn.mjs +32 -0
  70. package/tool/selfcheck/units-level.mjs +97 -3
  71. package/tool/selfcheck/units-probe.mjs +2 -1
  72. package/tool/selfcheck/units-prompt.mjs +106 -0
  73. package/tool/selfcheck/units-repo.mjs +44 -1
  74. package/tool/selfcheck/units-verdict.mjs +76 -0
@@ -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,8 @@
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, startWith } from "../lib/repo.mjs";
11
+ import { triggerVerdict, recipeFor, EXT_LANG, whichSync, browserServerAdvice, MARKS, isApiSpec, claudeSeesRules } from "../lib/repo.mjs";
12
+ import { startWith } from "../lib/advice.mjs";
12
13
  import { proposeGates } from "../lib/adopt.mjs";
13
14
  import { CATALOGS, L } from "../i18n/index.mjs";
14
15
  import { dirname } from "node:path";
@@ -284,3 +285,45 @@ test("с чего начать: при равенстве признаков п
284
285
  ];
285
286
  assert.deepEqual(startWith(list, { langs: new Set() }, 2).map((e) => e.slug), ["a", "b"]);
286
287
  });
288
+
289
+ // Совет под линтер ПРОЕКТА, а не под язык. Отзыв с живого проекта 2026-09-11: «начни с этих
290
+ // трёх» советовал завести eslint проекту на Biome — комплект читал package.json и Makefile, но не
291
+ // biome.json. Команда для Biome — одно правило разово (`--only`), и `--error-on-warnings`
292
+ // обязателен: правило вне рекомендованных Biome ставит на «предупреждение», и без флага команда
293
+ // выходила с нулём, напечатав находку. Проверено на Biome 2.5.12.
294
+ test("совет проекту на Biome: команда Biome, а не eslint", async () => {
295
+ const { blindAdvice } = await import("../lib/advice.mjs");
296
+ const entry = {
297
+ slug: "no-print-in-prod", biome_rules: "suspicious/noConsole",
298
+ tool: "https://github.com/eslint/eslint",
299
+ recipes: { javascript: `eslint --no-config-lookup --rule '{"no-console":"error"}' {dir}` },
300
+ };
301
+ const biome = blindAdvice(entry, { langs: new Set(["typescript"]), has_biome: true });
302
+ assert.equal(biome.command, "npx @biomejs/biome lint --error-on-warnings --only=suspicious/noConsole .");
303
+ assert.equal(biome.tool, "https://github.com/biomejs/biome");
304
+ // Без biome.json — прежний совет под язык.
305
+ const plain = blindAdvice(entry, { langs: new Set(["javascript"]), has_biome: false });
306
+ assert.match(plain.command, /^eslint /);
307
+ // У Biome правила нет вовсе (`none`) — прежний совет, а не выдуманная команда Biome.
308
+ const none = blindAdvice({ ...entry, biome_rules: "none" }, { langs: new Set(["javascript"]), has_biome: true });
309
+ assert.match(none.command, /^eslint /);
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
+ });
@@ -0,0 +1,76 @@
1
+ // tool/selfcheck/units-verdict.mjs — вердикт пробы: «поймано» только ПО ДЕЛУ.
2
+ //
3
+ // ОТДЕЛЬНЫМ ФАЙЛОМ: units-probe.mjs у предела в 500 строк, и шов настоящий — там разбор
4
+ // истории и план пробы, здесь то, на чём держится главное обещание пробы.
5
+ //
6
+ // ЗАЧЕМ. Отзыв с живого проекта 2026-09-11 (Amplifie, TypeScript на Biome): класс «цвет из
7
+ // токена темы» проба отметила пойманным линтером, хотя Biome цвета не проверяет. Подсаженный
8
+ // кусок сломал форматирование — упал форматтер. Вердикт считал «покраснел» равным «поймал»:
9
+ // код возврата сравнивался, вывод выбрасывался. Итог — «every applicable class is caught by
10
+ // something», ложная уверенность ровно в сторону самоуспокоения. К себе комплект был мягче,
11
+ // чем к проекту: от чужого гейта мы требуем покраснеть НА БРАКЕ, а свой вердикт этого не
12
+ // проверял.
13
+ import test from "node:test";
14
+ import assert from "node:assert/strict";
15
+ import { namesPlant, probeVerdictPaired, catchVerdict } from "../lib/history.mjs";
16
+
17
+ test("назвал подсаженный файл: путь появился в выводе после подсадки", () => {
18
+ assert.equal(namesPlant("", "src/ui/card.tsx:12:5 lint/style/noColorLiteral", "src/ui/card.tsx"), true);
19
+ // Любая форма пути: относительная с ./, абсолютная из песочницы, с обратными слешами Windows.
20
+ assert.equal(namesPlant("", "./src/ui/card.tsx:3 print found", "src/ui/card.tsx"), true);
21
+ assert.equal(namesPlant("", "/tmp/aqk-probe-x/src/ui/card.tsx:3", "src/ui/card.tsx"), true);
22
+ assert.equal(namesPlant("", "src\\ui\\card.tsx(3,1): error", "src/ui/card.tsx"), true);
23
+ });
24
+
25
+ test("не назвал: упал по своей причине — форматтер, чужой файл, общий итог", () => {
26
+ assert.equal(namesPlant("", "Formatter would have printed the following content.\nFound 1 error.", "src/ui/card.tsx"), false);
27
+ assert.equal(namesPlant("", "src/other.tsx:1 error", "src/ui/card.tsx"), false);
28
+ // Файл упоминался и до подсадки столько же раз — нового ничего не сказано.
29
+ assert.equal(namesPlant("src/ui/card.tsx:1 warn", "src/ui/card.tsx:1 warn", "src/ui/card.tsx"), false);
30
+ });
31
+
32
+ const pairs = (before, after) => ({
33
+ before: before.map(([name, code]) => ({ name, code })),
34
+ after: after.map(([name, code, named]) => ({ name, code, named })),
35
+ });
36
+
37
+ test("покраснел, но подсаженного файла не назвал — «неизвестно», а не «поймано»", () => {
38
+ const { before, after } = pairs([["lint", 0], ["units", 0]], [["lint", 1, false], ["units", 0]]);
39
+ const r = probeVerdictPaired(before, after);
40
+ assert.equal(r.verdict, "unknown");
41
+ assert.equal(r.unattributed, 1);
42
+ assert.equal(r.caught, 0);
43
+ });
44
+
45
+ test("поймал по делу хоть один — «поймано»; безымянное падение соседа этого не отменяет", () => {
46
+ const { before, after } = pairs([["fmt", 0], ["lint", 0]], [["fmt", 1, false], ["lint", 1, true]]);
47
+ const r = probeVerdictPaired(before, after);
48
+ assert.equal(r.verdict, "caught");
49
+ assert.equal(r.caught, 1);
50
+ });
51
+
52
+ test("без сведений о выводе (старые вызовы) поведение прежнее", () => {
53
+ const { before, after } = pairs([["lint", 0]], [["lint", 1, undefined]]);
54
+ assert.equal(probeVerdictPaired(before, after).verdict, "caught");
55
+ });
56
+
57
+ // ИМЕНИ ФАЙЛА МАЛО. Biome, падая на форматировании, тоже называет файл: «src/card.tsx format».
58
+ // Отличить «упал на браке» от «упал на подсадке» можно только ПАРОЙ — ровно тем, чем мы проверяем
59
+ // чужие гейты: в то же место кладётся ЗЕЛЁНЫЙ образец той же записи. Краснеет и на нём — гейт
60
+ // падает от самой подсадки (форматирование, синтаксис), поимка не доказана.
61
+ test("пара: на красном назвал файл, на зелёном молчит — поймал", () => {
62
+ const red = { code: 1, out: "src/card.tsx:3 lint/noColorLiteral" };
63
+ const green = { code: 0, out: "" };
64
+ assert.equal(catchVerdict("", red, green, "src/card.tsx"), "caught");
65
+ });
66
+
67
+ test("пара: краснеет и на зелёном, называя тот же файл, — падает от подсадки, не поймал", () => {
68
+ const red = { code: 1, out: "src/card.tsx format ━━━ Formatter would have printed" };
69
+ const green = { code: 1, out: "src/card.tsx format ━━━ Formatter would have printed" };
70
+ assert.equal(catchVerdict("", red, green, "src/card.tsx"), "planting");
71
+ });
72
+
73
+ test("пара: файла не назвал — «безымянно»; зелёного образца под расширение нет — судим по имени", () => {
74
+ assert.equal(catchVerdict("", { code: 1, out: "Found 1 error." }, { code: 0, out: "" }, "src/a.py"), "nameless");
75
+ assert.equal(catchVerdict("", { code: 1, out: "src/a.py:1 T201" }, null, "src/a.py"), "caught");
76
+ });