agent-quality-kit 0.8.0 → 0.9.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 (38) hide show
  1. package/README.md +92 -9
  2. package/README.ru.md +124 -23
  3. package/kit/docs/ai/project-baseline.md +14 -0
  4. package/kit/docs/ready-made-rules.md +103 -0
  5. package/kit/gates/color-from-token/check.sh +5 -1
  6. package/kit/gates/lesson-has-outcome/check.sh +5 -1
  7. package/kit/gates/mcp-server-resolves/README.md +62 -0
  8. package/kit/gates/mcp-server-resolves/check.sh +110 -0
  9. package/kit/gates/mcp-server-resolves/gate.yml +18 -0
  10. package/kit/gates/mcp-server-resolves/green/.mcp.json +20 -0
  11. package/kit/gates/mcp-server-resolves/red/.mcp.json +16 -0
  12. package/llms.txt +19 -4
  13. package/package.json +2 -6
  14. package/tool/commands/context.mjs +9 -5
  15. package/tool/commands/doctor.mjs +83 -14
  16. package/tool/commands/project.mjs +18 -2
  17. package/tool/commands/prove.mjs +1 -0
  18. package/tool/commands/vitals.mjs +159 -0
  19. package/tool/i18n/en-docs.mjs +40 -0
  20. package/tool/i18n/en.mjs +18 -0
  21. package/tool/i18n/index.mjs +36 -3
  22. package/tool/i18n/ru-docs.mjs +40 -0
  23. package/tool/i18n/ru.mjs +18 -0
  24. package/tool/lib/banner.mjs +59 -0
  25. package/tool/lib/brief.mjs +192 -0
  26. package/tool/lib/core.mjs +2 -0
  27. package/tool/lib/manifest.mjs +146 -15
  28. package/tool/lib/prove.mjs +11 -1
  29. package/tool/lib/repo.mjs +31 -1
  30. package/tool/program.mjs +26 -0
  31. package/tool/selfcheck/smoke.sh +350 -1
  32. package/tool/selfcheck/units-banner.mjs +65 -0
  33. package/tool/selfcheck/units-brief.mjs +97 -0
  34. package/tool/selfcheck/units-context.mjs +3 -1
  35. package/tool/selfcheck/units-level.mjs +147 -1
  36. package/tool/selfcheck/units-repo.mjs +134 -0
  37. package/tool/selfcheck/units-vitals.mjs +62 -0
  38. package/tool/selfcheck/units.mjs +3 -75
@@ -0,0 +1,159 @@
1
+ // tool/commands/vitals.mjs — «всё ли у самого комплекта подключено».
2
+ //
3
+ // ЗАЧЕМ ОТДЕЛЬНАЯ КОМАНДА. `doctor` смотрит на РЕПОЗИТОРИЙ, `prove` — на гейты, `context` —
4
+ // на состояние. На саму обвязку не смотрит никто: стоят ли инструменты, которых требуют
5
+ // объявленные гейты; прописан ли хук в `.git/hooks` НА САМОМ ДЕЛЕ, а не только в конфиге;
6
+ // получает ли агент состояние. Сегодня это выясняется красным гейтом посреди коммита — в
7
+ // худший момент из возможных, когда человек занят другим и просто выключит проверку.
8
+ //
9
+ // ЧЕТЫРЕ СОСТОЯНИЯ, А НЕ ДВА, И КАЖДОЕ ЗАРАБОТАНО.
10
+ // ✔ подключено;
11
+ // ✘ СЛОМАНО — объявленный гейт не состоится: нет инструмента, потеряна строка манифеста;
12
+ // · не подключено, и это выбор — хука pre-commit нет, потому что гоняют в конвейере;
13
+ // ~ посмотреть не смогли.
14
+ //
15
+ // Первая версия ставила `✘` хуку, которого нет. На нашем же репозитории вышло два креста за
16
+ // сознательное решение: pre-commit локально мы не ставим, проверки идут в CI. Команда, которая
17
+ // кричит «сломано» про выбор, — ровно та, которую выключают в первый день, и вместе с ней
18
+ // перестают читать настоящие отказы. Кода возврата касается только `✘`.
19
+ import { readFile } from "node:fs/promises";
20
+ import { join } from "node:path";
21
+ import { CWD, MANIFEST, SELF, c, exists } from "../lib/core.mjs";
22
+ import { readManifest, unparsedLines, gateRequires } from "../lib/manifest.mjs";
23
+ import { whichSync } from "../lib/repo.mjs";
24
+ import { updateWanted } from "../lib/brief.mjs";
25
+ import { L } from "../i18n/index.mjs";
26
+
27
+ // Чистая функция: на входе факты, на выходе строки. Отделена от чтения диска намеренно —
28
+ // «неизвестно» проверяется перебором случаев, а не прогоном, потому что случай «не смогли
29
+ // посмотреть» на исправной машине не воспроизвести.
30
+ function vitalsRows(f) {
31
+ const t = L.vitals;
32
+ const missing = (f.tools || []).filter((x) => !x.found);
33
+ const rows = [
34
+ {
35
+ key: "tools",
36
+ ok: (f.tools || []).length === 0 ? null : missing.length === 0,
37
+ detail: missing.length
38
+ ? t.toolsMissing(missing.map((x) => `${x.prog} (${x.gate})`).join(", "))
39
+ : t.toolsOk((f.tools || []).length),
40
+ },
41
+ {
42
+ key: "manifest",
43
+ ok: f.unparsed > 0 ? false : true,
44
+ detail: f.unparsed > 0 ? t.manifestBad(f.unparsed) : t.manifestOk,
45
+ },
46
+ {
47
+ key: "preCommit",
48
+ ok: f.preCommit === null ? null : f.preCommit ? true : "no",
49
+ detail: f.preCommit === null ? t.unknownHook : f.preCommit ? t.preCommitOk : t.preCommitNo,
50
+ },
51
+ {
52
+ key: "sessionHook",
53
+ ok: f.sessionHook === null ? null : f.sessionHook ? true : "no",
54
+ detail: f.sessionHook === null ? t.unknownHook : f.sessionHook ? t.sessionOk : t.sessionNo(`${SELF} context --install`),
55
+ },
56
+ ];
57
+ // Устаревшая версия — не отказ: человек мог закрепить её сознательно, и ронять за это нельзя.
58
+ if (f.version) {
59
+ rows.push({
60
+ key: "version",
61
+ ok: null,
62
+ detail: f.version.latest && f.version.latest !== f.version.current
63
+ ? t.versionOld(f.version.latest, f.version.current)
64
+ : t.versionOk(f.version.current),
65
+ });
66
+ }
67
+ return rows;
68
+ }
69
+
70
+ function vitalsVerdict(rows) {
71
+ return rows.some((r) => r.ok === false) ? 1 : 0;
72
+ }
73
+
74
+ // Первое слово команды — та программа, без которой гейт не состоится. Обёртки снимаются:
75
+ // `bash x.sh` требует bash, а не x.sh; `npx --yes knip@6` требует npx.
76
+ function progOf(cmd) {
77
+ const parts = String(cmd || "").trim().split(/\s+/);
78
+ return parts[0] || "";
79
+ }
80
+
81
+ async function cmdVitals() {
82
+ const man = await readManifest();
83
+ const gates = man?.gates && typeof man.gates === "object" && !Array.isArray(man.gates) ? man.gates : {};
84
+
85
+ const seen = new Map();
86
+ for (const [gate, cmd] of Object.entries(gates)) {
87
+ const prog = progOf(cmd);
88
+ if (!prog || seen.has(prog)) continue;
89
+ seen.set(prog, { gate, prog, found: Boolean(whichSync(prog)) });
90
+ }
91
+
92
+ // Первого слова мало. Запись каталога бывает обёрткой: команда начинается с `bash`, который
93
+ // есть всегда, а работать без `slopcheck` или `zizmor` она не может — и `doctor --run`
94
+ // краснеет там, где `vitals` печатал «все инструменты на месте». Ровно тот разрыв, ради
95
+ // закрытия которого эта команда и заведена. Программа названа в `requires:` записи.
96
+ // Найдено 2026-09-09 сверкой вывода двух команд на одном репозитории.
97
+ const samplesDir = typeof man?.samples === "string" ? man.samples.trim() : "";
98
+ for (const gate of Object.keys(gates)) {
99
+ const missing = await gateRequires(samplesDir, gate, whichSync);
100
+ for (const prog of missing || []) {
101
+ if (seen.has(prog)) continue;
102
+ seen.set(prog, { gate, prog, found: false });
103
+ }
104
+ }
105
+
106
+ let unparsed = 0;
107
+ try { unparsed = unparsedLines(await readFile(join(CWD, MANIFEST), "utf8")).length; } catch { /* манифеста нет */ }
108
+
109
+ // Хук pre-commit проверяется в `.git/hooks`, а НЕ в `.pre-commit-config.yaml`. Запись в
110
+ // конфиге — это намерение; сработает только то, что лежит в самом гите. Ровно та разница,
111
+ // ради которой весь комплект: объявлено и работает — разные утверждения.
112
+ let preCommit = null;
113
+ const hook = join(CWD, ".git", "hooks", "pre-commit");
114
+ if (await exists(join(CWD, ".git"))) {
115
+ preCommit = false;
116
+ if (await exists(hook)) {
117
+ try { preCommit = /pre-commit|aqk/i.test(await readFile(hook, "utf8")); } catch { preCommit = null; }
118
+ }
119
+ }
120
+
121
+ let sessionHook = null;
122
+ const settings = join(CWD, ".claude", "settings.json");
123
+ if (await exists(join(CWD, ".claude"))) {
124
+ sessionHook = false;
125
+ if (await exists(settings)) {
126
+ try {
127
+ const s = JSON.parse(await readFile(settings, "utf8"));
128
+ sessionHook = JSON.stringify(s?.hooks?.SessionStart || []).includes("context");
129
+ } catch { sessionHook = null; }
130
+ }
131
+ }
132
+
133
+ let version = null;
134
+ if (updateWanted()) {
135
+ try {
136
+ const { PKG_ROOT } = await import("../lib/core.mjs");
137
+ const current = JSON.parse(await readFile(join(PKG_ROOT, "package.json"), "utf8")).version || "";
138
+ const r = await fetch("https://registry.npmjs.org/agent-quality-kit/latest", {
139
+ signal: AbortSignal.timeout(3000),
140
+ headers: { accept: "application/vnd.npm.install-v1+json" },
141
+ });
142
+ version = { current, latest: r.ok ? String((await r.json()).version || "") : "" };
143
+ } catch { /* сети нет — строку про версию просто не покажем */ }
144
+ }
145
+
146
+ const rows = vitalsRows({ tools: [...seen.values()], unparsed, preCommit, sessionHook, version });
147
+ console.log(c.bold(`\n ${L.vitals.title}\n`));
148
+ for (const r of rows) {
149
+ const mark = r.ok === true ? c.green("✔")
150
+ : r.ok === false ? c.red("✘")
151
+ : r.ok === "no" ? c.yellow("·")
152
+ : c.dim("~");
153
+ console.log(` ${mark} ${L.vitals.names[r.key].padEnd(22)} ${r.ok === false ? r.detail : c.dim(r.detail)}`);
154
+ }
155
+ console.log("");
156
+ process.exit(vitalsVerdict(rows));
157
+ }
158
+
159
+ export { cmdVitals, vitalsRows, vitalsVerdict };
@@ -7,6 +7,46 @@
7
7
  // the second for one second in a terminal.
8
8
 
9
9
  const enDocs = {
10
+ // Блок vitals переехал сюда 2026-09-09 по той же причине, что и brief: терминальный
11
+ // каталог снова перерос 500 строк, и поймал это наш же file-size-limit.
12
+ vitals: {
13
+ title: "Is what the kit runs on actually wired up",
14
+ names: {
15
+ tools: "gate tools",
16
+ manifest: "manifest parsed",
17
+ preCommit: "pre-commit hook",
18
+ sessionHook: "state to the agent",
19
+ version: "version",
20
+ vitals: "is what the kit runs on wired up: tools, hooks, freshness",
21
+ },
22
+ toolsOk: (n) => `all ${n} present`,
23
+ toolsMissing: (l) => `NOT FOUND: ${l} — those gates will not happen`,
24
+ manifestOk: "parsed in full",
25
+ manifestBad: (n) => `${n} lines were not parsed and HAVE NO EFFECT — details in doctor`,
26
+ unknownHook: "could not look — unknown, not \"no\"",
27
+ preCommitOk: "wired in .git/hooks",
28
+ preCommitNo: "not in .git/hooks: a config entry is an intent, not a guard",
29
+ sessionOk: "the SessionStart hook hands the state to the agent",
30
+ sessionNo: (cmd) => `the agent gets no state: ${cmd}`,
31
+ versionOk: (v) => `${v}, current`,
32
+ versionOld: (l, cur) => `${l} is out, you have ${cur}`,
33
+ },
34
+ // Блок краткого вывода переехал сюда 2026-09-09: терминальный каталог снова перерос
35
+ // 500 строк, и поймал это наш же file-size-limit. Шов по смыслу условен — это всё-таки
36
+ // терминал, — но предел настоящий, а делить пополам хуже, чем делить по соседству.
37
+ brief: {
38
+ name: "AQK",
39
+ held: (n) => `holds ${n}`,
40
+ todo: (n) => `not installed ${n}`,
41
+ levelUnknown: "level unknown",
42
+ red: (l) => ` RED: ${l}`,
43
+ advise: (slug, why) => ` ↑ install: ${slug} — ${why}`,
44
+ adviseOff: (cmd, env) => ` not needed: ${cmd}, or ${env}`,
45
+ update: (l, c, how) => `version ${l} is out, you have ${c} — update: ${how}`,
46
+ updateHow: "npx agent-quality-kit@latest, or npm i -g agent-quality-kit",
47
+ updateHookHow: "pre-commit autoupdate",
48
+ updateOff: (env) => ` not needed: ${env}`,
49
+ },
10
50
  // THE STATE BLOCK — the one text of ours whose reader is a machine, not a person.
11
51
  // It goes into the agent's context via a SessionStart hook, so it is written as claims of
12
52
  // fact: no politeness, no preamble, every line either a fact or an honest "unknown".
package/tool/i18n/en.mjs CHANGED
@@ -44,6 +44,7 @@ export const en = {
44
44
  contextInstall: "the same in full — the map and the rulebook — installed as a hook",
45
45
  report: "the mandatory report form: what is in place, what is not, what was not read; --since <ref> adds what proves the diff",
46
46
  badge: "a level badge for your README — and a check that it does not lie",
47
+ version: "the banner and the version number — same as --version and -v",
47
48
  noInstall: "Without installing: npx agent-quality-kit init",
48
49
  language: "Output language: AQK_LANG=ru (or en), otherwise your system locale",
49
50
  },
@@ -87,6 +88,16 @@ export const en = {
87
88
  hasThings: "has",
88
89
  install: (cmd) => `install: ${cmd}`,
89
90
  notApplicable: (n) => `Not applicable to this repository (${n}):`,
91
+ noBrowserServer:
92
+ "this project has a UI, and the agent has no browser: it cannot look at its own change\n and judges the work by what compiled. Those are different claims.",
93
+ noBrowserServerHow: (l) => `add an MCP server — works with any agent: ${l}`,
94
+ coveredBy: (n) => `held by another arbiter: ${n} — the portable entry is not needed`,
95
+ coveredByGate: (g) => `held by "${g}", declared in the manifest`,
96
+ coversUnknown: (l) => `covers names a gate absent from gates: ${l} — those entries are held by nothing`,
97
+ coversUnproven: (e, g, codes) =>
98
+ `claim unverified: "${e}" is declared held by gate "${g}", but neither its command nor the\n linter config names rules ${codes} — the entry may be held by nothing`,
99
+ coversUnprovenHow: (cmd) => `settle it: add those rules to the linter, or install the entry — ${cmd}`,
100
+ totalCovered: (n) => `held by another arbiter ${n}`,
90
101
  total: "Total:",
91
102
  totalHeld: (n) => `held by a machine ${n}`,
92
103
  totalTodo: (n) => `applicable but not installed ${n}`,
@@ -96,6 +107,7 @@ export const en = {
96
107
  sinceBadRef: (ref) => `cannot compare against "${ref}": no such ref, or this is not a git repository`,
97
108
  notScopable: "output carries no paths — cannot be narrowed by diff, left red",
98
109
  outsideDiff: (n) => `findings exist, but outside the diff (${n})`,
110
+ advisoryQuiet: "(advisory — cannot fail the run)",
99
111
  advisoryMark: "advisory — shown, the run was not failed",
100
112
  advisorySummary: (names) =>
101
113
  `advisory and red: ${names.join(", ")}. These are switched-off checks: ` +
@@ -110,6 +122,9 @@ export const en = {
110
122
  manifestUnknown: (keys) =>
111
123
  `The manifest has fields the standard does not know: ${keys.join(", ")}. Looks like a typo — ` +
112
124
  `such a field is silently read as absent, and the verdict comes out wrong.`,
125
+ manifestUnparsed: (n, t) => `manifest line ${n} was not parsed and HAS NO EFFECT: ${t}`,
126
+ manifestUnparsedWhy:
127
+ "field and gate names use latin letters, digits, dash and underscore. What is declared here\n does not run at all — while looking as if it does.",
113
128
  manifestKnown: (keys) => `Manifest fields: ${keys.join(", ")}`,
114
129
  thresholdPass: (min) => `Threshold AQK-${min} passed.`,
115
130
  thresholdFail: (min, now) => `Threshold AQK-${min} NOT passed: currently AQK-${now}.`,
@@ -164,6 +179,7 @@ export const en = {
164
179
  has_agent_config: ["the agent was never configured here", "agent settings exist"],
165
180
  has_agent_entry: ["no entry point for an agent here", "an entry point for an agent exists"],
166
181
  has_ui: ["no stylesheets or UI components in sight", "a UI exists: stylesheets or components"],
182
+ has_mcp: ["no MCP tools are wired up for the agent here", "MCP servers are declared"],
167
183
  },
168
184
  },
169
185
 
@@ -367,6 +383,7 @@ export const en = {
367
383
  title: "If this was useful:",
368
384
  star: (url) => `Star it — ${url}`,
369
385
  issue: "Found a bug or it did not fit — open an issue; both are the most useful feedback there is.",
386
+ notRemembered: "could not remember this (home is not writable) — the message will appear again",
370
387
  once: "This message is shown once: it will not appear again on this machine.",
371
388
  },
372
389
 
@@ -448,6 +465,7 @@ export const en = {
448
465
  noTarget: "no place to substitute the sample directory — the command was written by hand",
449
466
  otherRecipe: (lang) => `the samples are written for the "${lang}" recipe, another one is installed — nothing to prove with`,
450
467
  noGates: "no gates declared — nothing to prove",
468
+ needsProgram: (progs) => `NOT CHECKED here — needs "${progs}"`,
451
469
  noSamplesDir: "the samples field in .aqk.yml is empty — nowhere to look for samples",
452
470
  nothingProven:
453
471
  "not a single gate is proven. A level above AQK-1 would mean trust in the author, not a fact:\n a project whose gate is `true` would pass it exactly like a project with real protection.",
@@ -11,23 +11,56 @@
11
11
  // хуже и расходятся молча. Каталоги лежат отдельно, и модульная проверка сверяет, что в них
12
12
  // одни и те же ключи: «поддерживаем два языка» — утверждение, которое обязана держать машина.
13
13
 
14
+ import { readFileSync } from "node:fs";
15
+ import { join } from "node:path";
14
16
  import { ru } from "./ru.mjs";
15
17
  import { en } from "./en.mjs";
16
18
 
17
19
  // Порядок: явная переменная окружения → системная локаль → английский. Английский последним
18
20
  // потому, что незнакомый язык интерфейса — это препятствие, а незнакомый английский в 2026-м
19
21
  // препятствие меньшее, чем незнакомая кириллица.
20
- function pickLang(env = process.env) {
22
+ // Порядок: переменная окружения → ПОЛЕ МАНИФЕСТА → системная локаль → английский.
23
+ //
24
+ // Поле манифеста добавлено 2026-09-08 по просьбе первого чужого пользователя: «язык берётся из
25
+ // LC_ALL/LANG, а на Windows их просто нет: русский проект получает английский вывод. AQK_LANG=ru
26
+ // чинит, но у следующего человека будет своё». Он прав: язык репозитория — свойство репозитория,
27
+ // а локаль — свойство машины, на которой его сегодня открыли.
28
+ //
29
+ // Переменная окружения оставлена ВЫШЕ манифеста намеренно: человек, набравший AQK_LANG=en
30
+ // руками, хочет английский именно сейчас, и спорить с ним манифестом значит отнять у него
31
+ // последнее средство. Манифест выше локали — он про проект, локаль про машину.
32
+ function pickLang(env = process.env, man = null) {
21
33
  const forced = String(env.AQK_LANG || "").toLowerCase();
22
34
  if (forced.startsWith("ru")) return "ru";
23
35
  if (forced.startsWith("en")) return "en";
36
+ const declared = String(man?.lang || "").toLowerCase();
37
+ if (declared.startsWith("ru")) return "ru";
38
+ if (declared.startsWith("en")) return "en";
24
39
  const locale = String(env.LC_ALL || env.LC_MESSAGES || env.LANG || "").toLowerCase();
25
40
  if (locale.startsWith("ru")) return "ru";
26
41
  return "en";
27
42
  }
28
43
 
44
+ // Язык читается СИНХРОННО и одним полем: каталог строк нужен раньше, чем что-либо успеет
45
+ // прочитать манифест целиком, а полный разбор отсюда звать нельзя — manifest.mjs сам берёт
46
+ // строки здесь, и вышло бы кольцо. Чтобы этот сокращённый разбор не разошёлся с настоящим,
47
+ // модульная проверка сверяет их ответы на одном и том же тексте.
48
+ function langFromText(text) {
49
+ const m = String(text).match(/^lang:[ \t]*["']?([A-Za-z][A-Za-z-]*)/m);
50
+ return m ? m[1] : "";
51
+ }
52
+
53
+ function manifestLang(cwd = process.cwd()) {
54
+ try {
55
+ return { lang: langFromText(readFileSync(join(cwd, ".aqk.yml"), "utf8")) };
56
+ } catch {
57
+ // Манифеста нет или он нечитаем — не повод падать: язык просто выберется дальше по порядку.
58
+ return null;
59
+ }
60
+ }
61
+
29
62
  const CATALOGS = { ru, en };
30
- const LANG = pickLang();
63
+ const LANG = pickLang(process.env, manifestLang());
31
64
  const L = CATALOGS[LANG];
32
65
 
33
- export { L, LANG, pickLang, CATALOGS };
66
+ export { L, LANG, pickLang, langFromText, CATALOGS };
@@ -7,6 +7,46 @@
7
7
  // вторые — он же в терминале, одну секунду.
8
8
 
9
9
  const ruDocs = {
10
+ // Блок vitals переехал сюда 2026-09-09 по той же причине, что и brief: терминальный
11
+ // каталог снова перерос 500 строк, и поймал это наш же file-size-limit.
12
+ vitals: {
13
+ title: "Подключено ли то, чем комплект работает",
14
+ names: {
15
+ tools: "инструменты гейтов",
16
+ manifest: "манифест разобран",
17
+ preCommit: "хук pre-commit",
18
+ sessionHook: "состояние агенту",
19
+ version: "версия",
20
+ vitals: "подключено ли то, чем комплект работает: инструменты, хуки, свежесть",
21
+ },
22
+ toolsOk: (n) => `все ${n} на месте`,
23
+ toolsMissing: (l) => `НЕ НАЙДЕНЫ: ${l} — эти гейты не состоятся`,
24
+ manifestOk: "разобран целиком",
25
+ manifestBad: (n) => `${n} строк не разобраны и НЕ ДЕЙСТВУЮТ — подробности в doctor`,
26
+ unknownHook: "посмотреть не смогли — неизвестно, а не «нет»",
27
+ preCommitOk: "прописан в .git/hooks",
28
+ preCommitNo: "в .git/hooks его нет: запись в конфиге — намерение, а не защита",
29
+ sessionOk: "хук SessionStart отдаёт состояние агенту",
30
+ sessionNo: (cmd) => `агент не получает состояние: ${cmd}`,
31
+ versionOk: (v) => `${v}, свежая`,
32
+ versionOld: (l, cur) => `есть ${l}, у вас ${cur}`,
33
+ },
34
+ // Блок краткого вывода переехал сюда 2026-09-09: терминальный каталог снова перерос
35
+ // 500 строк, и поймал это наш же file-size-limit. Шов по смыслу условен — это всё-таки
36
+ // терминал, — но предел настоящий, а делить пополам хуже, чем делить по соседству.
37
+ brief: {
38
+ name: "AQK",
39
+ held: (n) => `держит ${n}`,
40
+ todo: (n) => `не поставлено ${n}`,
41
+ levelUnknown: "уровень неизвестен",
42
+ red: (l) => ` КРАСНЫЕ: ${l}`,
43
+ advise: (slug, why) => ` ↑ поставить: ${slug} — ${why}`,
44
+ adviseOff: (cmd, env) => ` не нужно: ${cmd}, либо ${env}`,
45
+ update: (l, c, how) => `новая версия ${l}, у вас ${c} — обновить: ${how}`,
46
+ updateHow: "npx agent-quality-kit@latest, либо npm i -g agent-quality-kit",
47
+ updateHookHow: "pre-commit autoupdate",
48
+ updateOff: (env) => ` не нужно: ${env}`,
49
+ },
10
50
  // БЛОК СОСТОЯНИЯ — единственный наш текст, у которого читатель машина, а не человек.
11
51
  // Уходит в контекст агента хуком SessionStart, поэтому пишется как утверждения о факте:
12
52
  // без вежливости, без вводных, каждая строка либо факт, либо честное «неизвестно».
package/tool/i18n/ru.mjs CHANGED
@@ -45,6 +45,7 @@ export const ru = {
45
45
  contextInstall: "то же самое, но целиком — карта и свод правил — и хуком в контекст",
46
46
  report: "обязательная форма отчёта: что стоит, что нет, что не прочитано; --since <ссылка> — ещё и чем доказан диф",
47
47
  badge: "значок уровня для README — и проверка, что он не врёт",
48
+ version: "заставка и номер версии — то же по --version и -v",
48
49
  noInstall: "Без установки: npx agent-quality-kit init",
49
50
  language: "Язык вывода: AQK_LANG=en (или ru), иначе по системной локали",
50
51
  },
@@ -88,6 +89,16 @@ export const ru = {
88
89
  hasThings: "есть",
89
90
  install: (cmd) => `поставить: ${cmd}`,
90
91
  notApplicable: (n) => `Не применимо к этому репозиторию (${n}):`,
92
+ noBrowserServer:
93
+ "у проекта есть интерфейс, а у агента нет браузера: посмотреть на своё изменение глазами\n он не может и судит о работе по тому, что собралось. Это разные утверждения.",
94
+ noBrowserServerHow: (l) => `подключается MCP-сервером, любым агентом: ${l}`,
95
+ coveredBy: (n) => `закрыто другим арбитром: ${n} — переносимую запись ставить незачем`,
96
+ coveredByGate: (g) => `держит «${g}», объявленный в манифесте`,
97
+ coversUnknown: (l) => `covers называет гейт, которого нет в gates: ${l} — эти записи не закрыты ничем`,
98
+ coversUnproven: (e, g, codes) =>
99
+ `заявка не подтверждена: «${e}» объявлена закрытой гейтом «${g}», но ни его команда, ни\n конфиг линтера не называют правил ${codes} — запись может быть не закрыта ничем`,
100
+ coversUnprovenHow: (cmd) => `подтверди: добавь эти правила в линтер, либо поставь запись — ${cmd}`,
101
+ totalCovered: (n) => `закрыто другим арбитром ${n}`,
91
102
  total: "Итого:",
92
103
  totalHeld: (n) => `держит машина ${n}`,
93
104
  totalTodo: (n) => `применимо но не поставлено ${n}`,
@@ -97,6 +108,7 @@ export const ru = {
97
108
  sinceBadRef: (ref) => `не могу сравнить с «${ref}»: такой ссылки нет или это не репозиторий git`,
98
109
  notScopable: "вывод без путей — дифом не сужается, оставлен красным",
99
110
  outsideDiff: (n) => `находки есть, но вне дифа (${n})`,
111
+ advisoryQuiet: "(совещательный — уронить прогон не может)",
100
112
  advisoryMark: "совещательный — показано, прогон не уронен",
101
113
  advisorySummary: (names) =>
102
114
  `совещательные и красные: ${names.join(", ")}. Это выключенные проверки: ` +
@@ -111,6 +123,9 @@ export const ru = {
111
123
  manifestUnknown: (keys) =>
112
124
  `В манифесте поля, которых стандарт не знает: ${keys.join(", ")}. Похоже на опечатку — ` +
113
125
  `такое поле молча читается как отсутствующее, и вердикт выходит неверным.`,
126
+ manifestUnparsed: (n, t) => `строка ${n} манифеста не разобрана и НЕ ДЕЙСТВУЕТ: ${t}`,
127
+ manifestUnparsedWhy:
128
+ "имена полей и гейтов пишутся латиницей, цифрами, дефисом и подчёркиванием. Объявленное\n здесь не выполняется вовсе — а выглядит так, будто выполняется.",
114
129
  manifestKnown: (keys) => `Поля манифеста: ${keys.join(", ")}`,
115
130
  thresholdPass: (min) => `Порог AQK-${min} пройден.`,
116
131
  thresholdFail: (min, now) => `Порог AQK-${min} НЕ пройден: сейчас AQK-${now}.`,
@@ -170,6 +185,7 @@ export const ru = {
170
185
  has_agent_config: ["агента здесь не настраивали", "настройки агента есть"],
171
186
  has_agent_entry: ["свода для агента здесь нет", "свод для агента есть"],
172
187
  has_ui: ["не видно стилей и компонентов интерфейса", "интерфейс есть: стили или компоненты"],
188
+ has_mcp: ["агенту здесь не подключали внешних инструментов через MCP", "MCP-серверы объявлены"],
173
189
  },
174
190
  },
175
191
 
@@ -373,6 +389,7 @@ export const ru = {
373
389
  title: "Если пригодилось:",
374
390
  star: (url) => `Поставь звезду — ${url}`,
375
391
  issue: "Нашёл баг или не подошло — заведи Issue, самая полезная обратная связь: и то и другое.",
392
+ notRemembered: "запомнить не удалось (дом недоступен для записи) — сообщение появится снова",
376
393
  once: "Это разовое сообщение: больше не покажется на этой машине.",
377
394
  },
378
395
 
@@ -454,6 +471,7 @@ export const ru = {
454
471
  noTarget: "не видно, куда подставить каталог образца — команда написана руками",
455
472
  otherRecipe: (lang) => `образцы написаны под рецепт «${lang}», а стоит другой — доказать нечем`,
456
473
  noGates: "гейтов не объявлено — доказывать нечего",
474
+ needsProgram: (progs) => `НЕ ПРОВЕРЕНА здесь — нужен «${progs}»`,
457
475
  noSamplesDir: "в .aqk.yml не заполнено поле samples — образцы искать негде",
458
476
  nothingProven:
459
477
  "ни один гейт не доказан. Уровень выше AQK-1 означал бы доверие к автору, а не факт:\n проект с гейтом «true» прошёл бы его так же, как проект с настоящей защитой.",
@@ -0,0 +1,59 @@
1
+ // tool/lib/banner.mjs — заставка. Первая встреча человека с комплектом.
2
+ //
3
+ // ЗАЧЕМ ОНА ЕСТЬ. Жалоба владельца: «скачал и че дальше, не понятно, работает он вообще или
4
+ // нет». Строка присутствия в прогоне отвечает на «работает ли», а заставка — на «что это
5
+ // вообще такое»: она показывается там, где человек встречается с инструментом впервые.
6
+ //
7
+ // ПОКАЗЫВАЕТСЯ ДВАЖДЫ ЗА ВСЮ ЖИЗНЬ: при `init` и по `--version`. На каждой команде — нет:
8
+ // то, что видишь тридцатый раз, перестаёт читаться, а рядом стоят находки, которые читать надо.
9
+ //
10
+ // РИСУНОК — СВОЙ. Готовые работы из чужих подборок красивы, но у них нет ни автора, ни
11
+ // лицензии, а пакет уезжает в npm под MIT. Вставить чужое молча — сделать ровно то, за что мы
12
+ // ругаем других. Этот прислан владельцем; надпись собрана шрифтом ANSI Shadow, к шрифтам
13
+ // вопросов о правах нет.
14
+ // Отступ у кошки ОДИН пробел, у букв — два, и это не опечатка. Пустой Брайль `⠀` занимает
15
+ // место, но ничего не рисует: у рисунка видимая часть начинается со второго знака, и при
16
+ // одинаковом отступе кошка выглядела сдвинутой вправо на целый знак. Центры сведены по
17
+ // ВИДИМОЙ части, а не по началу строки.
18
+ const ART = [
19
+ " ⠀⠀⠀⠀⢀⣾⢷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⢧⡀",
20
+ " ⠀⠀⠀⠀⠀⢀⣾⠃⢘⣷⣀⣀⣤⣤⣤⣄⣀⢸⡏⠈⢷⡀",
21
+ " ⠀⠀⠀⠀⠀⣼⣷⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣬⣷",
22
+ " ⠀⠀⠀⠀⢰⣿⣿⣿⣿⠁⠸⠗⢹⣿⡟⠰⠷⠈⢻⣿⣿⣿⡇",
23
+ " ⠀⠀⢐⣒⣺⣿⣿⣿⣿⣀⠀⣀⣼⠛⢷⣄⠀⢀⣼⣿⣿⣿⣿⣉⡁",
24
+ " ⠀⠀⠀⠤⢺⣿⣿⣿⣿⣿⣿⣿⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⠢",
25
+ "",
26
+ " █████╗ ██████╗ ██╗ ██╗",
27
+ " ██╔══██╗██╔═══██╗██║ ██╔╝",
28
+ " ███████║██║ ██║█████╔╝",
29
+ " ██╔══██║██║▄▄ ██║██╔═██╗",
30
+ " ██║ ██║╚██████╔╝██║ ██╗",
31
+ " ╚═╝ ╚═╝ ╚══▀▀═╝ ╚═╝ ╚═╝",
32
+ ];
33
+
34
+ // Объявленная ширина сверяется с настоящей модульной проверкой: правка рисунка, разъехавшаяся
35
+ // с этим числом, роняет её. Иначе число здесь через месяц врёт, как всякая цифра в комментарии.
36
+ const BANNER_WIDTH = 27;
37
+
38
+ // Терминал без UTF-8 превратит Брайль в вопросительные знаки, и человек решит, что инструмент
39
+ // сломан. Тогда честнее короткая строка: она несёт то же самое, только буквами.
40
+ function canDrawArt(env) {
41
+ if (String(env.AQK_NO_ART || "") === "1") return false;
42
+ const loc = `${env.LC_ALL || ""}${env.LC_CTYPE || ""}${env.LANG || ""}`.toLowerCase();
43
+ // Windows-терминалы локаль обычно не выставляют вовсе, а UTF-8 там с 2019 года есть.
44
+ // Поэтому пусто — рисуем; явно не-UTF-8 — не рисуем.
45
+ if (!loc) return true;
46
+ return loc.includes("utf-8") || loc.includes("utf8");
47
+ }
48
+
49
+ // ВЕРСИИ В ЗАСТАВКЕ НЕТ — решение владельца. Выпуски идут часто, и номер в картинке устаревает
50
+ // быстрее всего остального; за версией есть `--version`, где она и печатается отдельной строкой.
51
+ function banner(env = process.env) {
52
+ if (!canDrawArt(env)) return " aqk — agent quality kit";
53
+ // Отступ подписи — шесть, а не три: он посчитан от ВИДИМОГО центра букв (со 2-го знака по
54
+ // 26-й, центр 14), а не подобран на глаз. Подобранный на глаз держался до первой правки
55
+ // рисунка; посчитанный переживёт её, а проверка ниже покажет, если разъедется.
56
+ return `${ART.join("\n")}\n agent quality kit`;
57
+ }
58
+
59
+ export { banner, BANNER_WIDTH, canDrawArt };