agent-quality-kit 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -7
- package/README.ru.md +44 -7
- package/kit/gates/api-contract-has-arbiter/README.md +16 -1
- package/kit/gates/api-contract-has-arbiter/check.sh +66 -23
- package/kit/gates/complexity-limit/gate.yml +4 -0
- package/kit/gates/dead-code/gate.yml +4 -0
- package/kit/gates/entry-commands-exist/README.md +64 -0
- package/kit/gates/entry-commands-exist/check.sh +110 -0
- package/kit/gates/entry-commands-exist/gate.yml +19 -0
- package/kit/gates/entry-commands-exist/green/AGENTS.md +13 -0
- package/kit/gates/entry-commands-exist/green/Makefile +6 -0
- package/kit/gates/entry-commands-exist/green/justfile +2 -0
- package/kit/gates/entry-commands-exist/green/package.json +10 -0
- package/kit/gates/entry-commands-exist/red/AGENTS.md +9 -0
- package/kit/gates/entry-commands-exist/red/Makefile +2 -0
- package/kit/gates/entry-commands-exist/red/package.json +9 -0
- package/kit/gates/env-secrets-not-committed/README.md +73 -0
- package/kit/gates/env-secrets-not-committed/check.sh +139 -0
- package/kit/gates/env-secrets-not-committed/gate.yml +21 -0
- package/kit/gates/env-secrets-not-committed/green/.aqk-tracked +10 -0
- package/kit/gates/env-secrets-not-committed/green/.env +10 -0
- package/kit/gates/env-secrets-not-committed/green/.env.production +5 -0
- package/kit/gates/env-secrets-not-committed/green/.env.test +2 -0
- package/kit/gates/env-secrets-not-committed/red/.aqk-tracked +5 -0
- package/kit/gates/env-secrets-not-committed/red/.env +7 -0
- package/kit/gates/no-print-in-prod/gate.yml +4 -0
- package/kit/gates/swallowed-error/gate.yml +4 -0
- package/kit/gates/todo-without-task/gate.yml +4 -0
- package/llms.txt +8 -2
- package/package.json +1 -1
- package/tool/commands/badge.mjs +1 -1
- package/tool/commands/context.mjs +81 -9
- package/tool/commands/doctor-catalog.mjs +222 -0
- package/tool/commands/doctor.mjs +81 -209
- package/tool/commands/learn.mjs +119 -19
- package/tool/commands/probe.mjs +50 -68
- package/tool/commands/project.mjs +6 -1
- package/tool/commands/prompt.mjs +69 -0
- package/tool/commands/report.mjs +1 -1
- package/tool/commands/vitals.mjs +15 -11
- package/tool/i18n/en-docs.mjs +22 -1
- package/tool/i18n/en-gates.mjs +6 -1
- package/tool/i18n/en.mjs +78 -4
- package/tool/i18n/index.mjs +42 -3
- package/tool/i18n/ru-docs.mjs +24 -1
- package/tool/i18n/ru-gates.mjs +6 -1
- package/tool/i18n/ru.mjs +87 -4
- package/tool/lib/adopt.mjs +15 -1
- package/tool/lib/advice.mjs +115 -0
- package/tool/lib/annotate.mjs +66 -0
- package/tool/lib/brief.mjs +3 -1
- package/tool/lib/cadence.mjs +40 -1
- package/tool/lib/core.mjs +40 -1
- package/tool/lib/gate-worker.mjs +18 -0
- package/tool/lib/history.mjs +34 -5
- package/tool/lib/manifest.mjs +65 -21
- package/tool/lib/repo.mjs +47 -35
- package/tool/lib/run.mjs +98 -9
- package/tool/program.mjs +4 -0
- package/tool/selfcheck/smoke/_fixture.mjs +8 -3
- package/tool/selfcheck/smoke/api-contract.test.mjs +37 -0
- package/tool/selfcheck/smoke/corpus.test.mjs +151 -0
- package/tool/selfcheck/smoke/first-run.test.mjs +144 -0
- package/tool/selfcheck/smoke/verdict.test.mjs +91 -3
- package/tool/selfcheck/smoke.sh +10 -2
- package/tool/selfcheck/units-annotate.mjs +67 -0
- package/tool/selfcheck/units-cadence.mjs +40 -1
- package/tool/selfcheck/units-context.mjs +64 -1
- package/tool/selfcheck/units-learn.mjs +32 -0
- package/tool/selfcheck/units-level.mjs +97 -3
- package/tool/selfcheck/units-probe.mjs +2 -1
- package/tool/selfcheck/units-prompt.mjs +106 -0
- package/tool/selfcheck/units-repo.mjs +44 -1
- package/tool/selfcheck/units-verdict.mjs +76 -0
package/tool/commands/learn.mjs
CHANGED
|
@@ -13,10 +13,16 @@
|
|
|
13
13
|
// настоящие правила («файл не трогай», «делай прогон с базой обязательно», «никаких
|
|
14
14
|
// обходных временных путей») и разговорная шелуха примерно поровну.
|
|
15
15
|
//
|
|
16
|
-
// ЧЕГО ЗДЕСЬ НАМЕРЕННО НЕТ. Поиска
|
|
17
|
-
// agent-lint. Замер его не подтвердил: на 67 сессиях владелец не повторяет
|
|
18
|
-
// говорит его один раз и каждый раз иначе. Те «повторы», что нашлись,
|
|
19
|
-
// одной реплики в самом логе.
|
|
16
|
+
// ЧЕГО ЗДЕСЬ НАМЕРЕННО НЕТ. Поиска повторов ПО СХОДСТВУ ТЕКСТА — приёма, на котором построен
|
|
17
|
+
// session-analyzer у agent-lint. Замер его не подтвердил: на 67 сессиях владелец не повторяет
|
|
18
|
+
// правило дословно, он говорит его один раз и каждый раз иначе. Те «повторы», что нашлись,
|
|
19
|
+
// оказались задвоением одной реплики в самом логе.
|
|
20
|
+
//
|
|
21
|
+
// ЧТО ЕСТЬ ВМЕСТО НЕГО (2026-09-11). Повтор, который помечает САМ человек: «я же говорил»,
|
|
22
|
+
// «опять», «снова». Не угадывание, что две реплики об одном, а слова «это уже было». На логах
|
|
23
|
+
// двух проектов — 7 и 1 такая реплика, настоящих норм среди них 6 и 1; отбор по маркерам
|
|
24
|
+
// наставления не ловил ни одной. Такой повтор, совпавший с правилом свода, — «записано, а
|
|
25
|
+
// поправлять всё равно приходится»: правилу нужен сторож, текстом оно не держится.
|
|
20
26
|
//
|
|
21
27
|
// ПРИВАТНОСТЬ. Команда читает переписку. Поэтому: только логи ТЕКУЩЕГО проекта (или явно
|
|
22
28
|
// названного), только в терминал, ни строки на диск, код возврата всегда 0. Отчёт, который
|
|
@@ -47,14 +53,86 @@ const MARKERS = new RegExp(
|
|
|
47
53
|
|
|
48
54
|
// Признаки вставки, а не реплики: длина, код в тройных кавычках, много переносов, пути, ссылки.
|
|
49
55
|
// Каждый добавлен по итогу прогона, а не на всякий случай.
|
|
56
|
+
function isPaste(text) {
|
|
57
|
+
const t = String(text || "").trim();
|
|
58
|
+
if (!t || t.length > 400) return true;
|
|
59
|
+
if (t.includes("```")) return true;
|
|
60
|
+
if ((t.match(/\n/g) || []).length > 6) return true;
|
|
61
|
+
if (/https?:\/\//.test(t)) return true;
|
|
62
|
+
return (t.match(/\S+\/\S+/g) || []).length >= 3;
|
|
63
|
+
}
|
|
64
|
+
|
|
50
65
|
function looksLikeRule(text) {
|
|
66
|
+
return !isPaste(text) && MARKERS.test(String(text));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ПОВТОР — сигнал, который даёт сам человек: «я же говорил», «опять», «снова». Разбор AgentLint
|
|
70
|
+
// 2026-09-11 (research/competitors/agentlint-0xmariowu.md): их SS2 сопоставляет поправку с
|
|
71
|
+
// правилом свода по словам. Замер на логах владельца: из 45 поправок к записанным правилам
|
|
72
|
+
// относятся от силы две — такой приём дал бы шум. А реплик с пометкой повтора в том же проекте
|
|
73
|
+
// 14, настоящих норм среди них 5–6 («опять не хочу плодить файлы», «я же не просил, ты опять не
|
|
74
|
+
// так понял») — и отбор по маркерам наставления выше не ловил НИ ОДНОЙ: слов «всегда»/«никогда»
|
|
75
|
+
// в них нет. `\b` здесь не годится — в JavaScript он не видит границ кириллических слов.
|
|
76
|
+
// Две силы пометки. Сильная — «я же говорил», «сколько раз» — повтор при любой форме реплики.
|
|
77
|
+
// Слабая — «опять», «снова» — только в утверждении: второй прогон на тех же логах показал, что
|
|
78
|
+
// вопрос с ней — недоумение («че опять rust?», «опять в env добавить?»), а не норма. Реплика со
|
|
79
|
+
// значка статуса (⬜ ✅ ❌) — вставленная цитата ответа агента, а не слова человека.
|
|
80
|
+
const STRONG = new RegExp(
|
|
81
|
+
"(^|[^а-яёa-z])(я же (говорил|говорю|просил|сказал|писал)|говорил же|сколько (раз|можно)|" +
|
|
82
|
+
"в который раз|(ещё|еще) раз говорю|i (already )?told you)([^а-яёa-z]|$)",
|
|
83
|
+
"i",
|
|
84
|
+
);
|
|
85
|
+
const WEAK = /(^|[^а-яёa-z])(опять(?! же)|снова|again)([^а-яёa-z]|$)/i;
|
|
86
|
+
|
|
87
|
+
function isRepeat(text) {
|
|
51
88
|
const t = String(text || "").trim();
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
89
|
+
if (isPaste(t) || /^[⬜✅❌☐☑]/u.test(t)) return false;
|
|
90
|
+
return STRONG.test(t) || (WEAK.test(t) && !t.includes("?"));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Правила свода: пункты списка с меткой сторожа `<!-- aqk: … -->` — так их размечает комплект.
|
|
94
|
+
// Свод без меток — пункты под заголовком про правила. Заголовок правила — жирное начало или
|
|
95
|
+
// часть до двоеточия: по нему и сверяем, хвост пояснения совпал бы с чем угодно.
|
|
96
|
+
function entryRules(entryText) {
|
|
97
|
+
const items = [];
|
|
98
|
+
let cur = null;
|
|
99
|
+
let section = "";
|
|
100
|
+
for (const line of String(entryText).split(/\r?\n/)) {
|
|
101
|
+
const h = /^#{1,4}\s+(.+)$/.exec(line);
|
|
102
|
+
if (h) { section = h[1]; cur = null; continue; }
|
|
103
|
+
const b = /^[-*]\s+(.+)$/.exec(line);
|
|
104
|
+
if (b) { cur = { text: b[1], section }; items.push(cur); continue; }
|
|
105
|
+
if (cur && /^\s{2,}\S/.test(line)) cur.text += ` ${line.trim()}`;
|
|
106
|
+
else cur = null;
|
|
107
|
+
}
|
|
108
|
+
const marked = items.filter((r) => /<!--\s*aqk:/.test(r.text));
|
|
109
|
+
const pool = marked.length ? marked : items.filter((r) => /правил|rules|constraints|запрет/i.test(r.section));
|
|
110
|
+
return pool.map((r) => {
|
|
111
|
+
const body = r.text.replace(/<!--[\s\S]*?-->/g, "").trim();
|
|
112
|
+
const title = (/^\*\*([^*]+)\*\*/.exec(body) || /^([^:]{3,60}):/.exec(body) || [, body.slice(0, 100)])[1].trim();
|
|
113
|
+
return { title, arbiter: (/<!--\s*aqk:\s*(\S+?)\s*-->/.exec(r.text) || [])[1] || null };
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Слова-пометки повтора и служебные слова по правилу не сверяются: иначе «опять» совпало бы с
|
|
118
|
+
// любым правилом, где оно встретилось.
|
|
119
|
+
const NOT_TOPIC = new Set(["опять", "снова", "говор", "проси", "сказа", "писал", "тольк", "всегд",
|
|
120
|
+
"никог", "нужно", "можно", "котор", "когда", "чтобы", "этого", "again", "told", "always", "never"]);
|
|
121
|
+
const topic = (t) => new Set(keyWords(t).map(stem).filter((s) => !NOT_TOPIC.has(s)));
|
|
122
|
+
|
|
123
|
+
// Записано, а поправлять всё равно приходится: повтор, у которого с заголовком правила совпали
|
|
124
|
+
// две основы — или все, если заголовок короче трёх слов.
|
|
125
|
+
function repeatedRules(repeats, entryText) {
|
|
126
|
+
const out = [];
|
|
127
|
+
for (const r of entryRules(entryText)) {
|
|
128
|
+
const t = topic(r.title);
|
|
129
|
+
if (!t.size) continue;
|
|
130
|
+
for (const m of repeats) {
|
|
131
|
+
const common = [...topic(m.text)].filter((s) => t.has(s)).length;
|
|
132
|
+
if (common >= 2 || (t.size <= 2 && common === t.size)) out.push({ rule: r.title, arbiter: r.arbiter, ...m });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return out;
|
|
58
136
|
}
|
|
59
137
|
|
|
60
138
|
// Слова, по которым сверяем сказанное с записанным. Короткие отброшены: на них совпадёт что
|
|
@@ -127,6 +205,7 @@ async function cmdLearn(argv = process.argv) {
|
|
|
127
205
|
try { files = (await readdir(root)).filter((f) => f.endsWith(".jsonl")); } catch { files = []; }
|
|
128
206
|
const seen = new Set();
|
|
129
207
|
const said = [];
|
|
208
|
+
const repeats = [];
|
|
130
209
|
let typedTotal = 0;
|
|
131
210
|
for (const f of files) {
|
|
132
211
|
let raw = "";
|
|
@@ -136,24 +215,45 @@ async function cmdLearn(argv = process.argv) {
|
|
|
136
215
|
const key = m.text.toLowerCase().slice(0, 200);
|
|
137
216
|
if (seen.has(key)) continue;
|
|
138
217
|
seen.add(key);
|
|
139
|
-
if (
|
|
218
|
+
if (isRepeat(m.text)) repeats.push(m);
|
|
219
|
+
else if (looksLikeRule(m.text)) said.push(m);
|
|
140
220
|
}
|
|
141
221
|
}
|
|
142
222
|
|
|
143
223
|
const entry = await readEntry(await readManifest());
|
|
144
|
-
const
|
|
145
|
-
fresh.
|
|
224
|
+
const byDate = (a, b) => String(b.when).localeCompare(String(a.when));
|
|
225
|
+
const fresh = said.filter((m) => saidNotWritten(m.text, entry)).sort(byDate);
|
|
226
|
+
const ruleHits = repeatedRules(repeats, entry).sort(byDate);
|
|
227
|
+
const onRule = new Set(ruleHits.map((h) => h.text));
|
|
228
|
+
const again = repeats.filter((m) => !onRule.has(m.text)).sort(byDate);
|
|
146
229
|
|
|
147
|
-
console.log(` ${c.dim(L.learn.counted(files.length, typedTotal, said.length, fresh.length))}\n`);
|
|
148
|
-
if (!fresh.length) {
|
|
230
|
+
console.log(` ${c.dim(L.learn.counted(files.length, typedTotal, said.length, fresh.length, repeats.length))}\n`);
|
|
231
|
+
if (!fresh.length && !repeats.length) {
|
|
149
232
|
console.log(` ${L.learn.nothing}\n`);
|
|
150
233
|
return;
|
|
151
234
|
}
|
|
152
|
-
|
|
153
|
-
|
|
235
|
+
const show = (m) => console.log(` ${c.dim(m.when)} ${m.text.slice(0, 150)}`);
|
|
236
|
+
// Первым — правило, которое ЗАПИСАНО, а человек всё равно поправляет: текстом оно не держится.
|
|
237
|
+
if (ruleHits.length) {
|
|
238
|
+
console.log(` ${c.bold(L.learn.ruleTitle)}`);
|
|
239
|
+
for (const h of ruleHits.slice(0, limit)) {
|
|
240
|
+
console.log(` ${c.yellow("!")} ${h.rule}${h.arbiter ? c.dim(` · aqk: ${h.arbiter}`) : ""}`);
|
|
241
|
+
console.log(` ${c.dim(h.when)} ${h.text.slice(0, 140)}`);
|
|
242
|
+
}
|
|
243
|
+
console.log(c.dim(` ${L.learn.ruleHow}\n`));
|
|
244
|
+
}
|
|
245
|
+
if (again.length) {
|
|
246
|
+
console.log(` ${c.bold(L.learn.repeatTitle)}`);
|
|
247
|
+
again.slice(0, limit).forEach(show);
|
|
248
|
+
if (again.length > limit) console.log(c.dim(` ${L.learn.andMore(again.length - limit)}`));
|
|
249
|
+
console.log("");
|
|
250
|
+
}
|
|
251
|
+
if (fresh.length) {
|
|
252
|
+
if (repeats.length) console.log(` ${c.bold(L.learn.restTitle)}`);
|
|
253
|
+
fresh.slice(0, limit).forEach(show);
|
|
254
|
+
if (fresh.length > limit) console.log(c.dim(`\n ${L.learn.andMore(fresh.length - limit)}`));
|
|
154
255
|
}
|
|
155
|
-
if (fresh.length > limit) console.log(c.dim(`\n ${L.learn.andMore(fresh.length - limit)}`));
|
|
156
256
|
console.log(`\n ${c.yellow(L.learn.warn)}\n`);
|
|
157
257
|
}
|
|
158
258
|
|
|
159
|
-
export { cmdLearn, logSlug, looksLikeRule, saidNotWritten, typedFrom };
|
|
259
|
+
export { cmdLearn, logSlug, looksLikeRule, saidNotWritten, typedFrom, isRepeat, repeatedRules };
|
package/tool/commands/probe.mjs
CHANGED
|
@@ -26,10 +26,11 @@ import { readFileSync } from "node:fs";
|
|
|
26
26
|
import { tmpdir } from "node:os";
|
|
27
27
|
import { join, dirname, extname } from "node:path";
|
|
28
28
|
import { readManifest } from "../lib/manifest.mjs";
|
|
29
|
-
import { fixHotspots, probeSummary, probeVerdictPaired, countProbe } from "../lib/history.mjs";
|
|
29
|
+
import { fixHotspots, probeSummary, probeVerdictPaired, countProbe, namesPlant, catchVerdict } from "../lib/history.mjs";
|
|
30
30
|
import { detectFacts, readCatalog, triggerVerdict } from "../lib/repo.mjs";
|
|
31
|
+
import { blindAdvice } from "../lib/advice.mjs";
|
|
31
32
|
import { CWD, GATES_SRC, TARGET_DIR, c, SELF, exists } from "../lib/core.mjs";
|
|
32
|
-
import { probeState, probeEvery, PROBE_EVERY, blindLines, parseBlind, parseRan } from "../lib/cadence.mjs";
|
|
33
|
+
import { probeState, probeEvery, PROBE_EVERY, blindLines, parseBlind, parseRan, parseCounts } from "../lib/cadence.mjs";
|
|
33
34
|
import { L } from "../i18n/index.mjs";
|
|
34
35
|
import { gateCommand } from "../lib/execution.mjs";
|
|
35
36
|
|
|
@@ -95,7 +96,7 @@ async function writeMark(now, blind, lines) {
|
|
|
95
96
|
"",
|
|
96
97
|
...lines,
|
|
97
98
|
"",
|
|
98
|
-
"Файл эфемерный: его переписывает каждая проба.
|
|
99
|
+
"Файл эфемерный: его переписывает каждая проба. `aqk init` кладёт его в .gitignore.",
|
|
99
100
|
].join("\n");
|
|
100
101
|
await writeFile(MARK(), body + "\n", "utf8");
|
|
101
102
|
}
|
|
@@ -156,58 +157,11 @@ function sampleLines(path, max = 4) {
|
|
|
156
157
|
.map((l) => l.slice(0, 88));
|
|
157
158
|
}
|
|
158
159
|
|
|
159
|
-
// Совет по НЕПОКРЫТОМУ классу: команда, которую можно вставить прямо сейчас.
|
|
160
|
-
//
|
|
161
|
-
// Проба находит настоящие дыры и печатала про них «close it: aqk add <имя>» — то есть «поставь
|
|
162
|
-
// нашу штуку». Человек, впервые увидевший комплект, закрывает окно. А готовая однострочная
|
|
163
|
-
// команда под его стек У НАС УЖЕ ЛЕЖИТ в `recipes` записи каталога; мы её не показывали.
|
|
164
|
-
//
|
|
165
|
-
// Замер руками на `requests` (самый скачиваемый python-пакет) 2026-09-10: в
|
|
166
|
-
// `src/requests/utils.py` — 75 коммитов-починок; дописана функция с `except Exception: pass`;
|
|
167
|
-
// их собственные `ruff` и `pytest` дали 0 и на чистой копии, и на подсаженной. Строка, которая
|
|
168
|
-
// поймала бы это, лежала в нашем каталоге всё это время.
|
|
169
|
-
//
|
|
170
|
-
// Переносимый рецепт (`any`) в совет НЕ идёт: он зовёт файл из комплекта, и человеку без
|
|
171
|
-
// комплекта вставить его некуда. Нет родного рецепта под стек — команды нет, и это честнее
|
|
172
|
-
// выдуманной.
|
|
173
|
-
function blindAdvice(entry, facts, hot = {}) {
|
|
174
|
-
const recipes = entry?.recipes && typeof entry.recipes === "object" ? entry.recipes : {};
|
|
175
|
-
// `langs` приходит МНОЖЕСТВОМ, а не массивом — `Array.isArray` тихо давал пустой список, и
|
|
176
|
-
// совет не печатался вовсе. Поймано на живом `requests`: langs = Set(1) { python }.
|
|
177
|
-
const langs = facts?.langs ? [...facts.langs] : [];
|
|
178
|
-
// Тот же порядок, что у `pickRecipe`: свой язык → безъязыковой родной → ничего. Переносимый
|
|
179
|
-
// (`any`) сюда не идёт никогда: он зовёт файл из комплекта, и человеку без комплекта вставить
|
|
180
|
-
// его некуда.
|
|
181
|
-
let cmd = null;
|
|
182
|
-
for (const key of [...langs, "native"]) {
|
|
183
|
-
const r = recipes[key];
|
|
184
|
-
if (!r || /\{gate\}/.test(r)) continue;
|
|
185
|
-
cmd = String(r).replace(/\{dir\}/g, ".")
|
|
186
|
-
// Вычистить то, что относится к НАМ, а не к его проекту. Исключение наших красных
|
|
187
|
-
// образцов нужно УСТАНОВЛЕННОМУ гейту — рядом с ним лежат образцы. Человеку, который
|
|
188
|
-
// команду только копирует, этих каталогов не существует, и флаги про них подрывают
|
|
189
|
-
// доверие: инструмент говорит про чужое хозяйство вместо его кода.
|
|
190
|
-
.replace(/\s--ignore-pattern\s+'[^']*gates\/[^']*'/g, "")
|
|
191
|
-
.replace(/\s--ignore-paths=?\s*'[^']*gates\/[^']*'/g, "")
|
|
192
|
-
.replace(/\s+/g, " ")
|
|
193
|
-
.trim();
|
|
194
|
-
break;
|
|
195
|
-
}
|
|
196
|
-
// Адрес — того инструмента, которым команда начинается: поле `tool` общее на все языки, и
|
|
197
|
-
// python-проекту показывалась ссылка на eslint. Первые три слова, а не одно: `npx knip`,
|
|
198
|
-
// `python -m vulture`. Не совпало — весь список: лишняя ссылка лучше, чем ни одной.
|
|
199
|
-
const urls = entry?.tool ? String(entry.tool).split(/\s+·\s+/) : [];
|
|
200
|
-
const head = cmd ? cmd.split(" ").slice(0, 3) : [];
|
|
201
|
-
const own = urls.find((u) => head.includes(u.replace(/\/+$/, "").split("/").pop()));
|
|
202
|
-
const tool = own ?? (entry?.tool ? String(entry.tool) : null);
|
|
203
|
-
return { command: cmd, tool, file: hot.file ?? null, fixes: hot.fixes ?? null, slug: entry?.slug ?? null };
|
|
204
|
-
}
|
|
205
|
-
|
|
206
160
|
// Красный образец записи, подходящий по расширению горячего файла. Расширение обязано
|
|
207
161
|
// совпадать: питоновский образец в проекте на TypeScript не проверит ничего, а покажет
|
|
208
162
|
// «не прикрыто» — ложная тревога того же класса, что молчащий гейт, только наоборот.
|
|
209
|
-
async function
|
|
210
|
-
const dir = join(GATES_SRC, entry,
|
|
163
|
+
async function sampleFor(entry, ext, kind = "red") {
|
|
164
|
+
const dir = join(GATES_SRC, entry, kind);
|
|
211
165
|
if (!(await exists(dir))) return null;
|
|
212
166
|
let names = [];
|
|
213
167
|
try { names = await readdir(dir); } catch { return null; }
|
|
@@ -226,7 +180,8 @@ async function redSampleFor(entry, ext) {
|
|
|
226
180
|
//
|
|
227
181
|
// Способ взят из мутационного тестирования, где та же задача решена двадцать лет назад: Stryker
|
|
228
182
|
// копирует проект во временный каталог, СИМЛИНКУЕТ `node_modules` и гоняет там родную команду.
|
|
229
|
-
// Копируются
|
|
183
|
+
// Копируются отслеживаемые и неигнорируемые файлы (`git ls-files --cached --others
|
|
184
|
+
// --exclude-standard`) — файлы проекта не меняются (итог пишется в .aqk/last-probe.md), а
|
|
230
185
|
// мусор сборки не тащится; тяжёлые каталоги зависимостей симлинкуются, иначе `npm test` в
|
|
231
186
|
// песочнице падал бы с «модуль не найден», и это читалось бы как сбой инструмента.
|
|
232
187
|
const DEP_DIRS = ["node_modules", ".venv", "venv", "vendor", "target", ".tox", ".bundle"];
|
|
@@ -284,20 +239,44 @@ async function plant(root, relPath, sample) {
|
|
|
284
239
|
// Гейт запускается В ПЕСОЧНИЦЕ и командой КАК ЕСТЬ — ничего в неё не подставляется. Именно это
|
|
285
240
|
// и делает пробу независимой от формы команды.
|
|
286
241
|
//
|
|
287
|
-
//
|
|
288
|
-
|
|
289
|
-
function runGates(gates, sandbox, { stopOnRed = false } = {}) {
|
|
242
|
+
// Вывод гейта сохраняется: по нему видно, ИЗ-ЗА ЧЕГО он покраснел (см. catchVerdict).
|
|
243
|
+
function runGates(gates, sandbox) {
|
|
290
244
|
const out = [];
|
|
291
245
|
for (const [name, cmd] of gates) {
|
|
292
246
|
const t0 = Date.now();
|
|
293
247
|
const r = spawnSync(gateCommand(cmd), { shell: true, cwd: sandbox, encoding: "utf8", timeout: 120000 });
|
|
294
248
|
const code = r.status === null ? 2 : r.status;
|
|
295
|
-
out.push({ name, code, ms: Date.now() - t0 });
|
|
296
|
-
if (stopOnRed && code === 1) break;
|
|
249
|
+
out.push({ name, code, ms: Date.now() - t0, out: `${r.stdout || ""}${r.stderr || ""}`.slice(0, 200000) });
|
|
297
250
|
}
|
|
298
251
|
return out;
|
|
299
252
|
}
|
|
300
253
|
|
|
254
|
+
// Один класс брака на одном файле: гейты по одному, в порядке плана. Покраснел и назвал файл —
|
|
255
|
+
// контроль ЗЕЛЁНЫМ образцом той же записи в том же месте (catchVerdict). Ранний выход — только на
|
|
256
|
+
// ПОДТВЕРЖДЁННОЙ поимке: первым мог покраснеть форматтер, и остановка на нём скрыла бы линтер,
|
|
257
|
+
// который брак действительно поймал.
|
|
258
|
+
async function probeOne({ sandbox, rel, red, green, gates, baseOut }) {
|
|
259
|
+
const after = [];
|
|
260
|
+
let restore = await plant(sandbox, rel, red);
|
|
261
|
+
try {
|
|
262
|
+
for (const g of gates) {
|
|
263
|
+
const [res] = runGates([g], sandbox);
|
|
264
|
+
after.push(res);
|
|
265
|
+
if (res.code !== 1) continue;
|
|
266
|
+
let ctl = null;
|
|
267
|
+
if (green && namesPlant(baseOut(res.name), res.out, rel)) {
|
|
268
|
+
await restore(); restore = await plant(sandbox, rel, green);
|
|
269
|
+
[ctl] = runGates([g], sandbox);
|
|
270
|
+
await restore(); restore = await plant(sandbox, rel, red);
|
|
271
|
+
}
|
|
272
|
+
res.why = catchVerdict(baseOut(res.name), res, ctl, rel);
|
|
273
|
+
res.named = res.why === "caught";
|
|
274
|
+
if (res.named) break;
|
|
275
|
+
}
|
|
276
|
+
} finally { await restore(); }
|
|
277
|
+
return after;
|
|
278
|
+
}
|
|
279
|
+
|
|
301
280
|
// Каким гейтом пробовать и в каком порядке.
|
|
302
281
|
//
|
|
303
282
|
// Цена пробы = (файлы × записи) × сумма длительностей гейтов. На самом комплекте после перехода
|
|
@@ -368,7 +347,8 @@ async function cmdProbe(args, { auto = false } = {}) {
|
|
|
368
347
|
// Пробуем только запланированными, в порядке плана.
|
|
369
348
|
const byName = new Map(gates);
|
|
370
349
|
const probeGates = plan.use.map((g) => [g.name, byName.get(g.name)]);
|
|
371
|
-
const baseline = plan.use.map((g) => ({ name: g.name, code: g.code }));
|
|
350
|
+
const baseline = plan.use.map((g) => ({ name: g.name, code: g.code, out: g.out }));
|
|
351
|
+
const baseOut = (name) => baseline.find((b) => b.name === name)?.out || "";
|
|
372
352
|
|
|
373
353
|
console.log(c.dim(` ${P.method(hot.length, entries.length, plan.use.length)}\n`));
|
|
374
354
|
if (plan.tooSlow.length) {
|
|
@@ -385,20 +365,20 @@ async function cmdProbe(args, { auto = false } = {}) {
|
|
|
385
365
|
let probed = 0;
|
|
386
366
|
|
|
387
367
|
for (const e of entries) {
|
|
388
|
-
const sample = await
|
|
368
|
+
const sample = await sampleFor(e.slug, ext, "red");
|
|
389
369
|
if (!sample) continue;
|
|
390
370
|
probed++;
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
try { after = runGates(probeGates, sandbox, { stopOnRed: true }); } finally { await restore(); }
|
|
371
|
+
const green = await sampleFor(e.slug, ext, "green");
|
|
372
|
+
const after = await probeOne({ sandbox, rel, red: sample, green, gates: probeGates, baseOut });
|
|
394
373
|
// Ранний выход обрывает список: гейты, до которых не дошли, считаются такими же, как на
|
|
395
374
|
// сухом прогоне. Иначе их отсутствие прочиталось бы как сбой запуска.
|
|
396
375
|
const seen = new Set(after.map((a) => a.name));
|
|
397
376
|
const full = after.concat(baseline.filter((b) => !seen.has(b.name)));
|
|
398
377
|
const r = probeVerdictPaired(baseline, full);
|
|
399
378
|
const verdict = r.verdict;
|
|
400
|
-
const
|
|
401
|
-
|
|
379
|
+
const redNow = full.filter((a) => a.code === 1 && baseline.find((b) => b.name === a.name)?.code === 0);
|
|
380
|
+
const caught = redNow.filter((a) => a.named !== false).map((a) => a.name);
|
|
381
|
+
const nameless = redNow.filter((a) => a.named === false).map((a) => `${a.name} (${a.why === "planting" ? P.planting : P.nameless})`);
|
|
402
382
|
records.push({ entry: e.slug, file: rel, verdict });
|
|
403
383
|
if (verdict === "caught") {
|
|
404
384
|
console.log(` ${c.green("✔")} ${e.intent.padEnd(48)} ${c.dim(P.caught(caught.join(", ")))}`);
|
|
@@ -419,7 +399,7 @@ async function cmdProbe(args, { auto = false } = {}) {
|
|
|
419
399
|
if (adv.tool) console.log(c.dim(` ${P.blindTool(adv.tool)}`));
|
|
420
400
|
console.log(c.dim(` ${P.install(`${SELF} add ${e.slug}`)}`));
|
|
421
401
|
} else {
|
|
422
|
-
console.log(` ${c.dim("~")} ${c.dim(e.intent.padEnd(48))} ${c.dim(P.unknown)}`);
|
|
402
|
+
console.log(` ${c.dim("~")} ${c.dim(e.intent.padEnd(48))} ${c.dim(nameless.length ? P.unattributed(nameless.join(", ")) : P.unknown)}`);
|
|
423
403
|
}
|
|
424
404
|
}
|
|
425
405
|
if (!probed) { unprobedN++; console.log(c.dim(` ${P.noSampleFor(ext || "—")}`)); }
|
|
@@ -442,6 +422,7 @@ async function cmdProbe(args, { auto = false } = {}) {
|
|
|
442
422
|
// Отметка нужна не для отчёта, а для КАДЕНЦИИ: по ней следующий прогон поймёт, что пора.
|
|
443
423
|
// Без неё команда снова становится тем, о чём надо вспомнить.
|
|
444
424
|
await writeMark(commitCount(), blind, [
|
|
425
|
+
`caught: ${n.caughtClasses}`, `unknown: ${n.unknownClasses}`,
|
|
445
426
|
`ran: ${probeGates.map(([name]) => name).join(" ")}`, "",
|
|
446
427
|
...blindLines(records), "",
|
|
447
428
|
...hot.map(({ path: p2, fixes }) => `- ${p2} (${P.fixes(fixes)})`),
|
|
@@ -462,7 +443,8 @@ async function probeStatus() {
|
|
|
462
443
|
if (every === null) return { state: "unknown", behind: null, badEvery: String(man?.probe) };
|
|
463
444
|
if (every === 0) return { state: "off", behind: null };
|
|
464
445
|
const mark = await readMark();
|
|
465
|
-
|
|
446
|
+
const text = mark?.text;
|
|
447
|
+
return { ...probeState(mark, commitCount(), every), classes: parseBlind(text), ran: parseRan(text), counts: parseCounts(text) };
|
|
466
448
|
}
|
|
467
449
|
|
|
468
|
-
export { cmdProbe, probeStatus, probeableGates, gatesState, extAlternatives, planProbeGates,
|
|
450
|
+
export { cmdProbe, probeStatus, probeableGates, gatesState, extAlternatives, planProbeGates, isCode };
|
|
@@ -6,7 +6,7 @@ import { spawnSync } from "node:child_process";
|
|
|
6
6
|
import { join, dirname, relative } from "node:path";
|
|
7
7
|
import {
|
|
8
8
|
CWD, PKG_ROOT, DOCS_SRC, RULES_SRC, TARGET_DIR, MANIFEST, SELF, REPO_URL, c, exists, die,
|
|
9
|
-
copyDir, writeIfAbsent, FEEDBACK_MARK, docPath } from "../lib/core.mjs";
|
|
9
|
+
copyDir, writeIfAbsent, FEEDBACK_MARK, docPath, ensureIgnored } from "../lib/core.mjs";
|
|
10
10
|
import { AGENTS_MD, CLAUDE_MD, MANIFEST_YML } from "../lib/templates.mjs";
|
|
11
11
|
import { banner } from "../lib/banner.mjs";
|
|
12
12
|
import { readManifest } from "../lib/manifest.mjs";
|
|
@@ -41,6 +41,10 @@ async function cmdInit(args) {
|
|
|
41
41
|
const claude = join(CWD, "CLAUDE.md");
|
|
42
42
|
track(await writeIfAbsent(claude, CLAUDE_MD, { force }), claude);
|
|
43
43
|
|
|
44
|
+
// Служебные файлы — в .gitignore сразу, до первого прогона: иначе первый же `doctor --run`
|
|
45
|
+
// оставит в дереве файл, который попадёт в коммит (так и случилось на живом проекте).
|
|
46
|
+
const ignored = await ensureIgnored(CWD);
|
|
47
|
+
|
|
44
48
|
// Заставка в начале init — первая встреча человека с комплектом. Второй раз он увидит её
|
|
45
49
|
// только если сам спросит `--version`: то, что видишь тридцатый раз, перестаёт читаться.
|
|
46
50
|
console.log(`\n${banner()}\n`);
|
|
@@ -56,6 +60,7 @@ async function cmdInit(args) {
|
|
|
56
60
|
if (LANG === "en" && created.some((f) => f.includes(`${TARGET_DIR}/docs/`) || f.includes(`${TARGET_DIR}\\docs\\`))) {
|
|
57
61
|
console.log(c.dim(`\n ${L.init.docsRu}`));
|
|
58
62
|
}
|
|
63
|
+
if (ignored.length) console.log(c.dim(`\n ${L.init.ignored(ignored.join(", "))}`));
|
|
59
64
|
if (skipped.length) {
|
|
60
65
|
console.log(c.yellow(`\n ${L.init.kept(skipped.length)}`));
|
|
61
66
|
for (const f of skipped) console.log(` ${f}`);
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// tool/commands/prompt.mjs — `aqk prompt`: одно задание для агента — «почини вот это, это и это».
|
|
2
|
+
//
|
|
3
|
+
// ЗАЧЕМ. Между диагнозом и действием не было моста. `doctor` пишет человеку, `context` говорит
|
|
4
|
+
// агенту «как дела», а задание «сделай вот это» человек пересказывал сам — и пересказ терял
|
|
5
|
+
// команды. Идея — из разбора agentlint (research/competitors/agentlint.md, «Задание для агента»):
|
|
6
|
+
// правила поведения сверху, исправления по весу, в конце — как проверить.
|
|
7
|
+
//
|
|
8
|
+
// ЧЕГО У НИХ НЕ БЕРЁМ. У них «проверь» — это «балл вырос». У нас у КАЖДОГО пункта свой арбитр:
|
|
9
|
+
// команда, которая сейчас красная и должна стать зелёной. Иначе агент доложит «сделал» про
|
|
10
|
+
// пункт, который ничем не доказан, — тот самый отказ, против которого весь комплект.
|
|
11
|
+
//
|
|
12
|
+
// НИЧЕГО НЕ ЗАПУСКАЕТ. Читает то же, что `context`: манифест, `.aqk/last-run.md`, отметку пробы,
|
|
13
|
+
// каталог. Задание пишется за секунду, а прогон — работа агента, и она в задании первой строкой,
|
|
14
|
+
// если прогона нет или он устарел: иначе пустое задание прочиталось бы как «всё чисто».
|
|
15
|
+
import { readManifest } from "../lib/manifest.mjs";
|
|
16
|
+
import { claudeShimFor } from "../lib/repo.mjs";
|
|
17
|
+
import { CWD, SELF } from "../lib/core.mjs";
|
|
18
|
+
import { readRun, readAdvice, portableSelf } from "./context.mjs";
|
|
19
|
+
import { probeStatus } from "./probe.mjs";
|
|
20
|
+
import { L } from "../i18n/index.mjs";
|
|
21
|
+
|
|
22
|
+
// Больше пяти пунктов за раз агент не удержит — как и человек; остальное называется числом.
|
|
23
|
+
const MAX_ITEMS = 5;
|
|
24
|
+
|
|
25
|
+
// Чистая функция: состояние → строки задания. Порядок — по тому, насколько пункт ФАКТ о проекте
|
|
26
|
+
// и насколько без него невыполнимы остальные: манифест → прогон → красное → гейт стоит, но
|
|
27
|
+
// пропустил брак из пробы → брак, для которого гейта нет → проверки, которые у проекта уже есть
|
|
28
|
+
// → свод, невидимый Claude Code → «начните с этих трёх» из каталога. Класс из пробы в последнем
|
|
29
|
+
// списке не повторяется.
|
|
30
|
+
function taskText(st, T = L.prompt) {
|
|
31
|
+
const self = st.self || "aqk";
|
|
32
|
+
const it = [];
|
|
33
|
+
if (!st.manifest) it.push(T.item.init(self));
|
|
34
|
+
if (!st.run) it.push(T.item.runNone(self));
|
|
35
|
+
else if (st.run.stale) it.push(T.item.runStale(self, st.run.when));
|
|
36
|
+
for (const name of st.run?.red || []) it.push(T.item.red(name, self));
|
|
37
|
+
for (const m of st.missed || []) it.push(T.item.missed(m, self));
|
|
38
|
+
for (const b of st.blind || []) it.push(T.item.blind(b, self));
|
|
39
|
+
if ((st.adopt || []).length) it.push(T.item.adopt(st.adopt, self));
|
|
40
|
+
if (st.shim) it.push(T.item.shim[st.shim](self));
|
|
41
|
+
const seen = new Set((st.blind || []).map((b) => b.slug));
|
|
42
|
+
for (const s of st.start || []) if (!seen.has(s.slug)) it.push(T.item.start(s, self));
|
|
43
|
+
|
|
44
|
+
const out = [T.title, "", T.intro, "", T.rulesTitle, ...T.rules.map((r) => `- ${r}`), "", T.tasksTitle];
|
|
45
|
+
if (!it.length) out.push(T.empty);
|
|
46
|
+
it.slice(0, MAX_ITEMS).forEach((line, i) => out.push(`${i + 1}. ${line}`));
|
|
47
|
+
if (it.length > MAX_ITEMS) out.push(T.more(it.length - MAX_ITEMS, self));
|
|
48
|
+
out.push("", T.verifyTitle, ...T.verify(self).map((r) => `- ${r}`));
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function cmdPrompt() {
|
|
53
|
+
const man = await readManifest();
|
|
54
|
+
let probe = null;
|
|
55
|
+
try { probe = await probeStatus(); } catch { /* пробы нет — пунктов из неё не будет */ }
|
|
56
|
+
let advice = { adopt: [], blind: [], start: [], missed: [] };
|
|
57
|
+
try { advice = await readAdvice(man, probe); } catch { /* не посчитали — выдумывать пункты нельзя */ }
|
|
58
|
+
// Команда уходит в чужой контекст и, возможно, в чужие руки: абсолютный путь к нашей
|
|
59
|
+
// программе там не сработает — тот же довод, что у хука `context --install`.
|
|
60
|
+
console.log(taskText({
|
|
61
|
+
self: portableSelf(SELF),
|
|
62
|
+
manifest: Boolean(man),
|
|
63
|
+
run: await readRun(),
|
|
64
|
+
shim: await claudeShimFor(CWD),
|
|
65
|
+
...advice,
|
|
66
|
+
}).join("\n"));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export { cmdPrompt, taskText };
|
package/tool/commands/report.mjs
CHANGED
|
@@ -86,7 +86,7 @@ async function cmdReport() {
|
|
|
86
86
|
|
|
87
87
|
// Прогон, а не чтение манифеста: «объявлен» и «работает» — разные утверждения, и весь
|
|
88
88
|
// смысл этой команды в том, чтобы в отчёт попало второе.
|
|
89
|
-
const run = declared.length ? runGates(man) : { results: [], failed: 0 };
|
|
89
|
+
const run = declared.length ? await runGates(man) : { results: [], failed: 0 };
|
|
90
90
|
|
|
91
91
|
const held = [], broken = [], todo = [], skip = [];
|
|
92
92
|
for (const res of run.results) {
|
package/tool/commands/vitals.mjs
CHANGED
|
@@ -16,9 +16,10 @@
|
|
|
16
16
|
// сознательное решение: pre-commit локально мы не ставим, проверки идут в CI. Команда, которая
|
|
17
17
|
// кричит «сломано» про выбор, — ровно та, которую выключают в первый день, и вместе с ней
|
|
18
18
|
// перестают читать настоящие отказы. Кода возврата касается только `✘`.
|
|
19
|
+
import { spawnSync } from "node:child_process";
|
|
19
20
|
import { readFile } from "node:fs/promises";
|
|
20
21
|
import { join } from "node:path";
|
|
21
|
-
import { CWD, MANIFEST, SELF, c, exists } from "../lib/core.mjs";
|
|
22
|
+
import { CWD, MANIFEST, SELF, c, exists, preCommitHook } from "../lib/core.mjs";
|
|
22
23
|
import { readManifest, unparsedLines, gateRequires } from "../lib/manifest.mjs";
|
|
23
24
|
import { whichSync } from "../lib/repo.mjs";
|
|
24
25
|
import { gitBash } from "../lib/execution.mjs";
|
|
@@ -122,14 +123,7 @@ async function cmdVitals() {
|
|
|
122
123
|
// Хук pre-commit проверяется в `.git/hooks`, а НЕ в `.pre-commit-config.yaml`. Запись в
|
|
123
124
|
// конфиге — это намерение; сработает только то, что лежит в самом гите. Ровно та разница,
|
|
124
125
|
// ради которой весь комплект: объявлено и работает — разные утверждения.
|
|
125
|
-
|
|
126
|
-
const hook = join(CWD, ".git", "hooks", "pre-commit");
|
|
127
|
-
if (await exists(join(CWD, ".git"))) {
|
|
128
|
-
preCommit = false;
|
|
129
|
-
if (await exists(hook)) {
|
|
130
|
-
try { preCommit = /pre-commit|aqk/i.test(await readFile(hook, "utf8")); } catch { preCommit = null; }
|
|
131
|
-
}
|
|
132
|
-
}
|
|
126
|
+
const preCommit = await preCommitHook(CWD);
|
|
133
127
|
|
|
134
128
|
let sessionHook = null;
|
|
135
129
|
const settings = join(CWD, ".claude", "settings.json");
|
|
@@ -145,15 +139,25 @@ async function cmdVitals() {
|
|
|
145
139
|
|
|
146
140
|
let version = null;
|
|
147
141
|
if (updateWanted()) {
|
|
142
|
+
let current = "";
|
|
148
143
|
try {
|
|
149
144
|
const { PKG_ROOT } = await import("../lib/core.mjs");
|
|
150
|
-
|
|
145
|
+
current = JSON.parse(await readFile(join(PKG_ROOT, "package.json"), "utf8")).version || "";
|
|
151
146
|
const r = await fetch("https://registry.npmjs.org/agent-quality-kit/latest", {
|
|
152
147
|
signal: AbortSignal.timeout(3000),
|
|
153
148
|
headers: { accept: "application/vnd.npm.install-v1+json" },
|
|
154
149
|
});
|
|
155
150
|
version = { current, latest: r.ok ? String((await r.json()).version || "") : "" };
|
|
156
|
-
} catch { /*
|
|
151
|
+
} catch { /* прямой запрос не прошёл — ниже спросим npm */ }
|
|
152
|
+
// ПРЯМОЙ ЗАПРОС НЕ ПРОШЁЛ — СПРОСИТЬ NPM. Отзыв с живого проекта 2026-09-11: vitals писал «не
|
|
153
|
+
// достучался до реестра», а `npm view` на той же машине работал. `fetch` в Node не знает
|
|
154
|
+
// прокси и зеркала из .npmrc, а npm знает. Только здесь, не в хуке: там секунды дороже.
|
|
155
|
+
if (current && !version?.latest) {
|
|
156
|
+
const r = spawnSync("npm", ["view", "agent-quality-kit", "version"],
|
|
157
|
+
{ encoding: "utf8", timeout: 10000, shell: process.platform === "win32" });
|
|
158
|
+
const latest = r.status === 0 ? String(r.stdout || "").trim().split("\n").pop() : "";
|
|
159
|
+
version = { current, latest };
|
|
160
|
+
}
|
|
157
161
|
}
|
|
158
162
|
|
|
159
163
|
const rows = vitalsRows({ tools: [...seen.values()], unparsed, preCommit, sessionHook, version });
|
package/tool/i18n/en-docs.mjs
CHANGED
|
@@ -66,7 +66,7 @@ const enDocs = {
|
|
|
66
66
|
runClean: (when) => `Last run ${when} — nothing red.`,
|
|
67
67
|
runRed: (when, names) => `Last run ${when} — RED: ${names}.`,
|
|
68
68
|
andMore: (n) => `and ${n} more`,
|
|
69
|
-
skipped: (n) => `Not run: ${n} — the tool is absent on this machine
|
|
69
|
+
skipped: (n) => `Not run: ${n} — skipped by --only/--skip or the tool is absent on this machine; their state is unknown.`,
|
|
70
70
|
probeNever: "No coverage probe has run — what is covered by nothing here is UNKNOWN. That is not \"covered\": `aqk probe`.",
|
|
71
71
|
probeOff: "The coverage probe is switched off in the manifest (`probe: 0`) — nobody counts what is covered by nothing here.",
|
|
72
72
|
probeUnknown: "The coverage probe could not run — what is covered by nothing here is UNKNOWN. That is not \"covered\".",
|
|
@@ -79,6 +79,26 @@ const enDocs = {
|
|
|
79
79
|
(behind ? ` (${behind} commits behind)` : "") + ".",
|
|
80
80
|
ratchets: (list) => `Ratchets: ${list}. The list may only get shorter, never longer.`,
|
|
81
81
|
where: (entry) => `The rulebook: ${entry}. What proves a diff: \`aqk report --since main\`.`,
|
|
82
|
+
nextTitle: "Next, in order — computed from this repository, not generic advice:",
|
|
83
|
+
nextStep: {
|
|
84
|
+
init: () => "Set up the standard: `aqk init` — without .aqk.yml `aqk add` refuses.",
|
|
85
|
+
adopt: (s) => `Declare the checks this project already has: in .aqk.yml, under gates: ${s.gates.map((g) => `${g.name}: "${g.cmd}"`).join(", ")}. Then \`aqk doctor --run\`.`,
|
|
86
|
+
blind: (s) => `The "${s.slug}" defect the probe planted in ${s.file} was NOT caught by your checks.` +
|
|
87
|
+
(s.command ? ` Catch it now: \`${s.command}\`.` : "") + ` Keep it caught: \`aqk add ${s.slug}\`.`,
|
|
88
|
+
start: (s) => `Install ${s.slug}: \`aqk add ${s.slug}\`` + (s.command ? ` (one line, no kit needed: \`${s.command}\`)` : "") +
|
|
89
|
+
", then `aqk prove` — the gate must go red on its own red sample.",
|
|
90
|
+
},
|
|
91
|
+
nextMore: (n) => `And ${n} more — the full list: \`aqk doctor\`.`,
|
|
92
|
+
whenTitle: "When to do what:",
|
|
93
|
+
whenCommit: (hook) => hook === true
|
|
94
|
+
? "Before a commit → the `.git/hooks/pre-commit` hook runs the checks itself; do not bypass it (`--no-verify`)."
|
|
95
|
+
: "Before a commit → `aqk doctor --run --since main`: there is no pre-commit hook, it will not happen by itself.",
|
|
96
|
+
whenRules: [
|
|
97
|
+
"Added or changed a check → `aqk prove`: the gate must go red on its own red sample, otherwise it checks nothing.",
|
|
98
|
+
"Writing a rule into the rulebook → put the arbiter mark next to it, `<!-- aqk: gate-name -->`; no gate — `<!-- aqk: human -->`, which is an admission, not a check.",
|
|
99
|
+
"A check is in your way → do not weaken it (`|| true`, `--exit-zero`, a suppression without a rule code): stop and ask the owner.",
|
|
100
|
+
"You think it is done → `aqk report --since main`: every changed file must be named by a check.",
|
|
101
|
+
],
|
|
82
102
|
mapTitle: "WHAT THIS TOOL CAN DO. The full list of commands — not a retelling, the same list\nthe help is built from:",
|
|
83
103
|
rulesTitle: (e) => `THE RULEBOOK OF THIS PROJECT (${e}) — verbatim, in full. This is not an invitation\nto read it: it is already here.`,
|
|
84
104
|
hookAlready: (p) => `the hook is already in ${p} — changing nothing.`,
|
|
@@ -196,6 +216,7 @@ const enDocs = {
|
|
|
196
216
|
},
|
|
197
217
|
],
|
|
198
218
|
report: {
|
|
219
|
+
skippedBySelect: "not run (by --only/--skip), state unknown",
|
|
199
220
|
title: "aqk doctor --run",
|
|
200
221
|
version: "version",
|
|
201
222
|
level: "level",
|
package/tool/i18n/en-gates.mjs
CHANGED
|
@@ -158,6 +158,7 @@ export const enGates = {
|
|
|
158
158
|
"the guides in .aqk/docs/ are in Russian — a deliberate decision, not a broken install.\n The rules in .aqk/rules/ are in English; the guides are prose an agent may ignore anyway,\n and what a machine holds lives in .aqk.yml and the gates. Translation waits for someone who needs it.",
|
|
159
159
|
created: (n) => `created (${n}):`,
|
|
160
160
|
andMore: (n) => `… and ${n} more`,
|
|
161
|
+
ignored: (list) => `the kit's runtime files were added to .gitignore — machine state, it does not belong in git: ${list}`,
|
|
161
162
|
kept: (n) => `already there, left untouched (${n}):`,
|
|
162
163
|
overwrite: (cmd) => `overwrite: ${cmd}`,
|
|
163
164
|
nextTitle: "What to do next, in order:",
|
|
@@ -255,16 +256,20 @@ export const enGates = {
|
|
|
255
256
|
badEvery: (v) => `the manifest says probe: "${v}", which is not a commit count. The probe does NOT run: silently using the default would mean doing something other than what is written.`,
|
|
256
257
|
autoFirst: "no coverage probe has ever run here — running it myself. Turn off: AQK_PROBE=0",
|
|
257
258
|
auto: (n) => `${n} commits since the last probe — running it myself. Turn off: AQK_PROBE=0`,
|
|
259
|
+
autoNotInCi: (cmd) => `in CI the probe does not run by itself — it would be minutes of surprise in a fast check; run it as a separate job: ${cmd} (or AQK_PROBE=1)`,
|
|
258
260
|
title: "aqk probe — what the declared checks cannot see",
|
|
259
261
|
method: (files, entries, gates) =>
|
|
260
262
|
`method: a red sample from a catalogue entry is planted into a COPY of the project, then the ` +
|
|
261
263
|
`DECLARED gates are run there — the command is used as written, nothing is substituted into it. ` +
|
|
262
264
|
`Files: ${files}, applicable entries: ${entries}, gates green on a clean checkout: ${gates}. ` +
|
|
263
|
-
`
|
|
265
|
+
`Project files are not changed; the probe result goes to .aqk/last-probe.md (in .gitignore after init).`,
|
|
264
266
|
fixes: (n) => `fixes in history: ${n}`,
|
|
265
267
|
caught: (names) => `caught by: ${names}`,
|
|
266
268
|
blind: "NOTHING CATCHES IT",
|
|
267
269
|
unknown: "nothing to check with — the gate did not run (delegated tool missing)",
|
|
270
|
+
nameless: "did not name the planted file",
|
|
271
|
+
planting: "goes red on the green sample too — fails from the planting itself",
|
|
272
|
+
unattributed: (g) => `went red: ${g} — the catch is not proven`,
|
|
268
273
|
blindWhere: (file, fixes) => `where: ${file} — ${fixes} fix commits in its history`,
|
|
269
274
|
blindWhat: "what we planted into your file:",
|
|
270
275
|
blindFix: (cmd) => `catch it right now, no kit needed: ${cmd}`,
|