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.
- package/README.md +92 -9
- package/README.ru.md +124 -23
- package/kit/docs/ai/project-baseline.md +14 -0
- package/kit/docs/ready-made-rules.md +103 -0
- package/kit/gates/color-from-token/check.sh +5 -1
- package/kit/gates/lesson-has-outcome/check.sh +5 -1
- package/kit/gates/mcp-server-resolves/README.md +62 -0
- package/kit/gates/mcp-server-resolves/check.sh +110 -0
- package/kit/gates/mcp-server-resolves/gate.yml +18 -0
- package/kit/gates/mcp-server-resolves/green/.mcp.json +20 -0
- package/kit/gates/mcp-server-resolves/red/.mcp.json +16 -0
- package/llms.txt +19 -4
- package/package.json +2 -6
- package/tool/commands/context.mjs +9 -5
- package/tool/commands/doctor.mjs +83 -14
- package/tool/commands/project.mjs +18 -2
- package/tool/commands/prove.mjs +1 -0
- package/tool/commands/vitals.mjs +159 -0
- package/tool/i18n/en-docs.mjs +40 -0
- package/tool/i18n/en.mjs +18 -0
- package/tool/i18n/index.mjs +36 -3
- package/tool/i18n/ru-docs.mjs +40 -0
- package/tool/i18n/ru.mjs +18 -0
- package/tool/lib/banner.mjs +59 -0
- package/tool/lib/brief.mjs +192 -0
- package/tool/lib/core.mjs +2 -0
- package/tool/lib/manifest.mjs +146 -15
- package/tool/lib/prove.mjs +11 -1
- package/tool/lib/repo.mjs +31 -1
- package/tool/program.mjs +26 -0
- package/tool/selfcheck/smoke.sh +350 -1
- package/tool/selfcheck/units-banner.mjs +65 -0
- package/tool/selfcheck/units-brief.mjs +97 -0
- package/tool/selfcheck/units-context.mjs +3 -1
- package/tool/selfcheck/units-level.mjs +147 -1
- package/tool/selfcheck/units-repo.mjs +134 -0
- package/tool/selfcheck/units-vitals.mjs +62 -0
- 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 };
|
package/tool/i18n/en-docs.mjs
CHANGED
|
@@ -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.",
|
package/tool/i18n/index.mjs
CHANGED
|
@@ -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
|
-
|
|
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 };
|
package/tool/i18n/ru-docs.mjs
CHANGED
|
@@ -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 };
|