agent-quality-kit 0.7.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 +135 -9
- package/README.ru.md +167 -23
- package/kit/docs/ai/project-baseline.md +14 -0
- package/kit/docs/ready-made-rules.md +103 -0
- package/kit/gates/_skip.sh +61 -1
- package/kit/gates/ci-not-hijackable/README.md +56 -0
- package/kit/gates/ci-not-hijackable/check.sh +73 -0
- package/kit/gates/ci-not-hijackable/gate.yml +19 -0
- package/kit/gates/ci-not-hijackable/green/.github/workflows/triage.yml +19 -0
- package/kit/gates/ci-not-hijackable/red/.github/workflows/triage.yml +18 -0
- package/kit/gates/color-from-token/check.sh +10 -2
- package/kit/gates/color-from-token/green/Button.tsx +2 -0
- package/kit/gates/complexity-limit/check.sh +6 -7
- package/kit/gates/duplicate-code/check.sh +5 -1
- package/kit/gates/entry-links-exist/check.sh +4 -1
- package/kit/gates/entry-links-exist/green/AGENTS.md +2 -0
- package/kit/gates/file-size-limit/check.sh +1 -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/kit/gates/secrets-not-in-code/check.sh +16 -3
- package/kit/gates/secrets-not-in-code/green/testdata/certificate/key.pem +3 -0
- package/kit/gates/todo-without-task/check.sh +1 -1
- package/kit/gates/todo-without-task/green/app.py +1 -0
- package/llms.txt +38 -2
- package/package.json +2 -3
- package/tool/commands/context.mjs +264 -0
- package/tool/commands/doctor.mjs +104 -28
- package/tool/commands/learn.mjs +159 -0
- package/tool/commands/project.mjs +19 -2
- package/tool/commands/prove.mjs +1 -0
- package/tool/commands/report.mjs +33 -1
- package/tool/commands/vitals.mjs +159 -0
- package/tool/i18n/en-docs.mjs +125 -1
- package/tool/i18n/en.mjs +39 -36
- package/tool/i18n/index.mjs +36 -3
- package/tool/i18n/ru-docs.mjs +127 -1
- package/tool/i18n/ru.mjs +39 -36
- package/tool/lib/banner.mjs +59 -0
- package/tool/lib/brief.mjs +192 -0
- package/tool/lib/core.mjs +32 -1
- package/tool/lib/evidence.mjs +124 -0
- package/tool/lib/manifest.mjs +173 -15
- package/tool/lib/prove.mjs +24 -2
- package/tool/lib/repo.mjs +31 -1
- package/tool/lib/scope.mjs +10 -1
- package/tool/lib/templates.mjs +1 -0
- package/tool/program.mjs +45 -23
- package/tool/selfcheck/smoke.sh +592 -3
- package/tool/selfcheck/units-banner.mjs +65 -0
- package/tool/selfcheck/units-brief.mjs +97 -0
- package/tool/selfcheck/units-context.mjs +188 -0
- package/tool/selfcheck/units-evidence.mjs +83 -0
- package/tool/selfcheck/units-learn.mjs +88 -0
- package/tool/selfcheck/units-level.mjs +211 -3
- package/tool/selfcheck/units-repo.mjs +134 -0
- package/tool/selfcheck/units-vitals.mjs +62 -0
- package/tool/selfcheck/units.mjs +4 -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,130 @@
|
|
|
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
|
+
},
|
|
50
|
+
// THE STATE BLOCK — the one text of ours whose reader is a machine, not a person.
|
|
51
|
+
// It goes into the agent's context via a SessionStart hook, so it is written as claims of
|
|
52
|
+
// fact: no politeness, no preamble, every line either a fact or an honest "unknown".
|
|
53
|
+
context: {
|
|
54
|
+
title: "AQK — the state of this repository right now. Only what a machine computed; where it\ndoes not know, it says \"unknown\" — which is not the same as \"fine\".",
|
|
55
|
+
level: (r, top, missing) =>
|
|
56
|
+
`Level: AQK-${r} of ${top}.` + (missing ? ` AQK-${r + 1} is missing: ${missing}.` : ""),
|
|
57
|
+
levelUnknown: "Level: not computed — there is no .aqk.yml here. The standard is not set up.",
|
|
58
|
+
rules: (total, machine, human) =>
|
|
59
|
+
`Rules in the entry point: ${total}. Held by a machine ${machine}, by a human ${human}.`,
|
|
60
|
+
rulesNobody: "A rule held by a human is held by nobody: no machine checks it.",
|
|
61
|
+
runNone:
|
|
62
|
+
"No run has been made — which checks are red is UNKNOWN. This is not \"clean\": `aqk doctor --run`.",
|
|
63
|
+
runStale: (when) =>
|
|
64
|
+
`The last run ${when} is OLDER than the last commit — it describes different code.`,
|
|
65
|
+
runClean: (when) => `Last run ${when} — nothing red.`,
|
|
66
|
+
runRed: (when, names) => `Last run ${when} — RED: ${names}.`,
|
|
67
|
+
andMore: (n) => `and ${n} more`,
|
|
68
|
+
skipped: (n) => `Not run: ${n} — the tool is absent on this machine, their state is unknown.`,
|
|
69
|
+
ratchets: (list) => `Ratchets: ${list}. The list may only get shorter, never longer.`,
|
|
70
|
+
where: (entry) => `The rulebook: ${entry}. What proves a diff: \`aqk report --since main\`.`,
|
|
71
|
+
mapTitle: "WHAT THIS TOOL CAN DO. The full list of commands — not a retelling, the same list\nthe help is built from:",
|
|
72
|
+
rulesTitle: (e) => `THE RULEBOOK OF THIS PROJECT (${e}) — verbatim, in full. This is not an invitation\nto read it: it is already here.`,
|
|
73
|
+
hookAlready: (p) => `the hook is already in ${p} — changing nothing.`,
|
|
74
|
+
hookAdded: (p) => `SessionStart hook appended to ${p}:`,
|
|
75
|
+
hookCreated: (p) => `created ${p} with a SessionStart hook:`,
|
|
76
|
+
hookBadJson: (p) => `${p} does not parse as JSON — leaving it alone. Fix it and retry.`,
|
|
77
|
+
hookWhat:
|
|
78
|
+
"the project state now reaches the agent's context before its first action, not at its discretion.",
|
|
79
|
+
},
|
|
80
|
+
// Форма отчёта переехала сюда из терминального каталога 2026-09-08: это текст, который
|
|
81
|
+
// программа ПИШЕТ в .aqk/report.md, а не говорит в терминал. Повод — тот же гейт
|
|
82
|
+
// file-size-limit, что развёл эти файлы в первый раз: терминальный каталог снова перерос
|
|
83
|
+
// 500 строк. Шов по смыслу, а не пополам.
|
|
84
|
+
report2: {
|
|
85
|
+
evidenceNamed: (who) => `named by: ${who}`,
|
|
86
|
+
evidenceSilent: (n) => `${n} check${n === 1 ? "" : "s"} walked past it and said nothing`,
|
|
87
|
+
evidenceTitle: "What proves this diff",
|
|
88
|
+
evidenceUncovered: "no check named this file",
|
|
89
|
+
evidenceBase: "base of comparison",
|
|
90
|
+
evidenceHash: "evidence fingerprint",
|
|
91
|
+
evidenceBadRef: (r) => `ref "${r}" could not be resolved — coverage not computed`,
|
|
92
|
+
evidenceNoFiles: (r) => `no code files in the diff against "${r}" — nothing to prove`,
|
|
93
|
+
evidenceWarn:
|
|
94
|
+
"◻️ means a check walked the directory and said nothing. \"Scanned and clean\" and " +
|
|
95
|
+
"\"never looked\" are indistinguishable from the output, and neither may be passed off " +
|
|
96
|
+
"as the other. " +
|
|
97
|
+
"The fingerprint covers the base, the set of commands and the contents of the files. " +
|
|
98
|
+
"Change any of them and this report is stale, so \"ran it, then edited three more files\" " +
|
|
99
|
+
"stops being indistinguishable from \"ran it\". Mechanism taken from donecheck (MIT).",
|
|
100
|
+
title: "AQK report",
|
|
101
|
+
noManifest: (cmd) => `No .aqk.yml — nothing to report on. Start with ${cmd}`,
|
|
102
|
+
level: "Level",
|
|
103
|
+
holdsTitle: "What a machine holds (from a run, not from the manifest)",
|
|
104
|
+
nothingRuns: "⬜ no gate is declared",
|
|
105
|
+
native: (prog) => `native recipe: ${prog}`,
|
|
106
|
+
portable: "portable check",
|
|
107
|
+
weakerTitle: "Weaker than it could be",
|
|
108
|
+
weaker: (progs) => `${progs} is available on this system, but the gate uses the portable check — it catches less`,
|
|
109
|
+
missingTitle: "What is missing",
|
|
110
|
+
nothingMissing: "✅ every applicable entry is installed",
|
|
111
|
+
needsTool: (prog) => `needs ${prog} — not on this system`,
|
|
112
|
+
notInstalled: "applicable, but not installed",
|
|
113
|
+
hiddenTitle: "Not applicable to this repository",
|
|
114
|
+
readTitle: "What the kit told you to read",
|
|
115
|
+
readWarn:
|
|
116
|
+
"The mark only means the file is on disk. Whether it was read, the machine does not know " +
|
|
117
|
+
"and does not pretend to: that is answered by whoever is reporting.",
|
|
118
|
+
ignoreTitle: "What .aqkignore hides",
|
|
119
|
+
ignoreNone: "no .aqkignore file — nothing is hidden",
|
|
120
|
+
ignoreWarn:
|
|
121
|
+
"Hiding things silently is the same class as a silent gate: the gates do not look at these " +
|
|
122
|
+
"paths at all. A line here means there is no protection there, and will not be.",
|
|
123
|
+
whyTitle: "Why this matters — briefly",
|
|
124
|
+
whyNothing: "nothing to add: everything applicable is in place",
|
|
125
|
+
saved: (path) => `Saved: ${path}`,
|
|
126
|
+
docs: {
|
|
127
|
+
baseline: "the minimum a project needs, independent of language",
|
|
128
|
+
readyMade: "the map of off-the-shelf rules: look for a ready one before writing your own",
|
|
129
|
+
rulesGeneral: "general working rules",
|
|
130
|
+
rulesTesting: "rules about tests",
|
|
131
|
+
rulesSecurity: "rules about security",
|
|
132
|
+
},
|
|
133
|
+
},
|
|
10
134
|
manifestDoc: {
|
|
11
135
|
head: [
|
|
12
136
|
"# .aqk.yml — the Agent Quality Kit manifest",
|
|
@@ -15,7 +139,7 @@ const enDocs = {
|
|
|
15
139
|
"# and that is honest: filling it with placeholders is pointless, files are checked, not words.",
|
|
16
140
|
],
|
|
17
141
|
entry: "# AQK-0 — what the agent reads first.",
|
|
18
|
-
rules: "# AQK-1 — where the standards are and which checks are mandatory.",
|
|
142
|
+
rules: "# AQK-1 — where the standards are and which checks are mandatory. docs — where the\n# guides are: doctor reads both, so the kit may live anywhere you like.",
|
|
19
143
|
gates: [
|
|
20
144
|
" # name: a command returning 0 or non-zero. An empty declaration protects nothing and is",
|
|
21
145
|
' # rejected by the "a declared gate runs" check — hence examples here, not placeholders.',
|
package/tool/i18n/en.mjs
CHANGED
|
@@ -8,6 +8,18 @@ import { enDocs } from "./en-docs.mjs";
|
|
|
8
8
|
import { templates } from "./templates-en.mjs";
|
|
9
9
|
|
|
10
10
|
export const en = {
|
|
11
|
+
learn: {
|
|
12
|
+
title: "Said out loud, never written down",
|
|
13
|
+
noLogs: (p) => `no logs for this project: ${p}\n The command reads Claude Code transcripts on this machine. Empty means nobody worked here.`,
|
|
14
|
+
counted: (s, typed, said, fresh) =>
|
|
15
|
+
`sessions: ${s} · typed by a human: ${typed} · looks like an instruction: ${said} · not in the entry point: ${fresh}`,
|
|
16
|
+
nothing: "everything that looks like a rule is already in the entry point",
|
|
17
|
+
andMore: (n) => `… and ${n} more`,
|
|
18
|
+
warn:
|
|
19
|
+
"These are CANDIDATES, not findings: measured on 1619 messages, the markers returned 79, and " +
|
|
20
|
+
"about half of those are real rules. The human decides. Nothing was written to disk — the " +
|
|
21
|
+
"command reads transcripts and prints to the terminal only.",
|
|
22
|
+
},
|
|
11
23
|
...enDocs,
|
|
12
24
|
templates,
|
|
13
25
|
help: {
|
|
@@ -27,13 +39,21 @@ export const en = {
|
|
|
27
39
|
new: "scaffold your own gate for the catalogue",
|
|
28
40
|
note: "record a lesson in the shared bruise journal",
|
|
29
41
|
blob: "assemble the guides into a single GOD_AI.md",
|
|
30
|
-
|
|
42
|
+
learn: "rule candidates from local transcripts: said out loud, never written down",
|
|
43
|
+
context: "the project state in one block — for an agent's context, not for reading",
|
|
44
|
+
contextInstall: "the same in full — the map and the rulebook — installed as a hook",
|
|
45
|
+
report: "the mandatory report form: what is in place, what is not, what was not read; --since <ref> adds what proves the diff",
|
|
31
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",
|
|
32
48
|
noInstall: "Without installing: npx agent-quality-kit init",
|
|
33
49
|
language: "Output language: AQK_LANG=ru (or en), otherwise your system locale",
|
|
34
50
|
},
|
|
35
51
|
|
|
36
52
|
doctor: {
|
|
53
|
+
baselineClash: (f) =>
|
|
54
|
+
`"--baseline" is an inspection, not a run: it always exits 0, so together with "${f}" it\n` +
|
|
55
|
+
" gives you a pipeline that cannot go red.\n" +
|
|
56
|
+
" fix: split it in two — \"doctor --baseline\" and \"doctor --run --min 1\".",
|
|
37
57
|
docsKit: "guides — the originals live here, not a copy",
|
|
38
58
|
docs: "guides",
|
|
39
59
|
rulesKit: "standards — the originals live here, not a copy",
|
|
@@ -68,6 +88,16 @@ export const en = {
|
|
|
68
88
|
hasThings: "has",
|
|
69
89
|
install: (cmd) => `install: ${cmd}`,
|
|
70
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}`,
|
|
71
101
|
total: "Total:",
|
|
72
102
|
totalHeld: (n) => `held by a machine ${n}`,
|
|
73
103
|
totalTodo: (n) => `applicable but not installed ${n}`,
|
|
@@ -77,6 +107,7 @@ export const en = {
|
|
|
77
107
|
sinceBadRef: (ref) => `cannot compare against "${ref}": no such ref, or this is not a git repository`,
|
|
78
108
|
notScopable: "output carries no paths — cannot be narrowed by diff, left red",
|
|
79
109
|
outsideDiff: (n) => `findings exist, but outside the diff (${n})`,
|
|
110
|
+
advisoryQuiet: "(advisory — cannot fail the run)",
|
|
80
111
|
advisoryMark: "advisory — shown, the run was not failed",
|
|
81
112
|
advisorySummary: (names) =>
|
|
82
113
|
`advisory and red: ${names.join(", ")}. These are switched-off checks: ` +
|
|
@@ -91,6 +122,9 @@ export const en = {
|
|
|
91
122
|
manifestUnknown: (keys) =>
|
|
92
123
|
`The manifest has fields the standard does not know: ${keys.join(", ")}. Looks like a typo — ` +
|
|
93
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.",
|
|
94
128
|
manifestKnown: (keys) => `Manifest fields: ${keys.join(", ")}`,
|
|
95
129
|
thresholdPass: (min) => `Threshold AQK-${min} passed.`,
|
|
96
130
|
thresholdFail: (min, now) => `Threshold AQK-${min} NOT passed: currently AQK-${now}.`,
|
|
@@ -145,6 +179,7 @@ export const en = {
|
|
|
145
179
|
has_agent_config: ["the agent was never configured here", "agent settings exist"],
|
|
146
180
|
has_agent_entry: ["no entry point for an agent here", "an entry point for an agent exists"],
|
|
147
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"],
|
|
148
183
|
},
|
|
149
184
|
},
|
|
150
185
|
|
|
@@ -341,12 +376,14 @@ export const en = {
|
|
|
341
376
|
n4c: ". A gate guards an artefact that exists:",
|
|
342
377
|
n4d: "a check for code that is not written yet is a dead rule.",
|
|
343
378
|
burned: (cmd) => `Got burned by something — write it down: ${cmd}`,
|
|
379
|
+
hookHint: (cmd) => `Claude Code: ${cmd} — the project state will reach the agent's context\n by itself, before its first action. Other agents have no hook, and that is not an omission.`,
|
|
344
380
|
},
|
|
345
381
|
|
|
346
382
|
feedback: {
|
|
347
383
|
title: "If this was useful:",
|
|
348
384
|
star: (url) => `Star it — ${url}`,
|
|
349
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",
|
|
350
387
|
once: "This message is shown once: it will not appear again on this machine.",
|
|
351
388
|
},
|
|
352
389
|
|
|
@@ -428,6 +465,7 @@ export const en = {
|
|
|
428
465
|
noTarget: "no place to substitute the sample directory — the command was written by hand",
|
|
429
466
|
otherRecipe: (lang) => `the samples are written for the "${lang}" recipe, another one is installed — nothing to prove with`,
|
|
430
467
|
noGates: "no gates declared — nothing to prove",
|
|
468
|
+
needsProgram: (progs) => `NOT CHECKED here — needs "${progs}"`,
|
|
431
469
|
noSamplesDir: "the samples field in .aqk.yml is empty — nowhere to look for samples",
|
|
432
470
|
nothingProven:
|
|
433
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.",
|
|
@@ -446,41 +484,6 @@ export const en = {
|
|
|
446
484
|
checkOk: (level, where) => `Badge matches the run: AQK-${level} — ${where}`,
|
|
447
485
|
},
|
|
448
486
|
|
|
449
|
-
report2: {
|
|
450
|
-
title: "AQK report",
|
|
451
|
-
noManifest: (cmd) => `No .aqk.yml — nothing to report on. Start with ${cmd}`,
|
|
452
|
-
level: "Level",
|
|
453
|
-
holdsTitle: "What a machine holds (from a run, not from the manifest)",
|
|
454
|
-
nothingRuns: "⬜ no gate is declared",
|
|
455
|
-
native: (prog) => `native recipe: ${prog}`,
|
|
456
|
-
portable: "portable check",
|
|
457
|
-
weakerTitle: "Weaker than it could be",
|
|
458
|
-
weaker: (progs) => `${progs} is available on this system, but the gate uses the portable check — it catches less`,
|
|
459
|
-
missingTitle: "What is missing",
|
|
460
|
-
nothingMissing: "✅ every applicable entry is installed",
|
|
461
|
-
needsTool: (prog) => `needs ${prog} — not on this system`,
|
|
462
|
-
notInstalled: "applicable, but not installed",
|
|
463
|
-
hiddenTitle: "Not applicable to this repository",
|
|
464
|
-
readTitle: "What the kit told you to read",
|
|
465
|
-
readWarn:
|
|
466
|
-
"The mark only means the file is on disk. Whether it was read, the machine does not know " +
|
|
467
|
-
"and does not pretend to: that is answered by whoever is reporting.",
|
|
468
|
-
ignoreTitle: "What .aqkignore hides",
|
|
469
|
-
ignoreNone: "no .aqkignore file — nothing is hidden",
|
|
470
|
-
ignoreWarn:
|
|
471
|
-
"Hiding things silently is the same class as a silent gate: the gates do not look at these " +
|
|
472
|
-
"paths at all. A line here means there is no protection there, and will not be.",
|
|
473
|
-
whyTitle: "Why this matters — briefly",
|
|
474
|
-
whyNothing: "nothing to add: everything applicable is in place",
|
|
475
|
-
saved: (path) => `Saved: ${path}`,
|
|
476
|
-
docs: {
|
|
477
|
-
baseline: "the minimum a project needs, independent of language",
|
|
478
|
-
readyMade: "the map of off-the-shelf rules: look for a ready one before writing your own",
|
|
479
|
-
rulesGeneral: "general working rules",
|
|
480
|
-
rulesTesting: "rules about tests",
|
|
481
|
-
rulesSecurity: "rules about security",
|
|
482
|
-
},
|
|
483
|
-
},
|
|
484
487
|
|
|
485
488
|
|
|
486
489
|
};
|
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 };
|