agent-quality-kit 0.13.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 +19 -5
- package/README.ru.md +20 -5
- 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/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/llms.txt +5 -1
- package/package.json +1 -1
- package/tool/commands/context.mjs +34 -15
- package/tool/commands/doctor-catalog.mjs +222 -0
- package/tool/commands/doctor.mjs +28 -211
- package/tool/commands/learn.mjs +119 -19
- package/tool/commands/probe.mjs +4 -2
- package/tool/commands/prompt.mjs +69 -0
- package/tool/i18n/en.mjs +65 -3
- package/tool/i18n/index.mjs +42 -3
- package/tool/i18n/ru.mjs +74 -3
- package/tool/lib/annotate.mjs +66 -0
- package/tool/lib/cadence.mjs +30 -1
- package/tool/lib/core.mjs +1 -0
- package/tool/lib/repo.mjs +45 -4
- package/tool/lib/run.mjs +20 -2
- 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 +39 -0
- package/tool/selfcheck/smoke/verdict.test.mjs +41 -2
- package/tool/selfcheck/smoke.sh +4 -0
- package/tool/selfcheck/units-annotate.mjs +67 -0
- package/tool/selfcheck/units-cadence.mjs +26 -1
- package/tool/selfcheck/units-learn.mjs +32 -0
- package/tool/selfcheck/units-level.mjs +21 -1
- package/tool/selfcheck/units-prompt.mjs +106 -0
- package/tool/selfcheck/units-repo.mjs +20 -1
|
@@ -260,6 +260,37 @@ async function installHook(full = false) {
|
|
|
260
260
|
console.log(c.dim(` ${T.hookWhat}`));
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
+
// Прошлый прогон — из отчёта, который кладёт `doctor --run`. Отдельной функцией: его читают и
|
|
264
|
+
// `context`, и `prompt`, и два разбора одного файла разошлись бы.
|
|
265
|
+
async function readRun() {
|
|
266
|
+
const lastRun = join(CWD, TARGET_DIR, "last-run.md");
|
|
267
|
+
if (!(await exists(lastRun))) return null;
|
|
268
|
+
const run = parseLastRun(await readFile(lastRun, "utf8"));
|
|
269
|
+
if (run) run.stale = runIsStale(run.when);
|
|
270
|
+
return run;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Что советовать — теми же функциями, что у `doctor`: корзины каталога, «начните с трёх», совет
|
|
274
|
+
// под язык, чужие проверки проекта. Одно место на `context` и `prompt`: второй расчёт того же
|
|
275
|
+
// самого разошёлся бы с первым. Класс из пробы, чей гейт уже стоит, в совет не идёт — ставить
|
|
276
|
+
// его второй раз бессмысленно.
|
|
277
|
+
async function readAdvice(man, probe) {
|
|
278
|
+
const facts = await detectFacts(man);
|
|
279
|
+
const catalog = await readCatalog();
|
|
280
|
+
const { todo } = catalogBuckets(catalog, facts, coversOf(man).covered);
|
|
281
|
+
const adopt = declaredGates(man).length ? [] : proposeGates(await readAdoptFiles(CWD));
|
|
282
|
+
const blind = (probe?.classes || [])
|
|
283
|
+
.filter((b) => !facts.gateKeys.includes(b.slug))
|
|
284
|
+
.map((b) => ({ ...b, command: blindAdvice(catalog.find((r) => r.slug === b.slug), facts, {}).command }));
|
|
285
|
+
const start = startWith(todo, facts, 3)
|
|
286
|
+
.map((rec) => ({ slug: rec.slug, intent: rec.intent || "", command: blindAdvice(rec, facts, {}).command }));
|
|
287
|
+
// Гейт стоит, проба его ГОНЯЛА — и брак он пропустил. Самое ценное, что проба знает: не
|
|
288
|
+
// «поставь», а «твоя проверка здесь слепа». Гейт, поставленный после пробы, сюда не идёт —
|
|
289
|
+
// поймает ли, покажет следующая.
|
|
290
|
+
const missed = (probe?.classes || []).filter((b) => facts.gateKeys.includes(b.slug) && probe?.ran?.has(b.slug));
|
|
291
|
+
return { adopt, blind, start, missed };
|
|
292
|
+
}
|
|
293
|
+
|
|
263
294
|
async function cmdContext(args = []) {
|
|
264
295
|
const full = args.includes("--full");
|
|
265
296
|
if (args.includes("--install")) return installHook(full);
|
|
@@ -286,12 +317,7 @@ async function cmdContext(args = []) {
|
|
|
286
317
|
rules = countArbiters(await readFile(join(CWD, entry), "utf8"), ["человек", "human", "nobody"]);
|
|
287
318
|
}
|
|
288
319
|
|
|
289
|
-
|
|
290
|
-
const lastRun = join(CWD, TARGET_DIR, "last-run.md");
|
|
291
|
-
if (await exists(lastRun)) {
|
|
292
|
-
run = parseLastRun(await readFile(lastRun, "utf8"));
|
|
293
|
-
if (run) run.stale = runIsStale(run.when);
|
|
294
|
-
}
|
|
320
|
+
const run = await readRun();
|
|
295
321
|
|
|
296
322
|
// Проба: сколько классов не ловит никто и насколько отметка отстала. Читается из файла,
|
|
297
323
|
// ничего не запускает — блок обязан укладываться в секунду.
|
|
@@ -334,14 +360,7 @@ async function cmdContext(args = []) {
|
|
|
334
360
|
// язык, чужие проверки проекта. Второй расчёт того же самого разошёлся бы с первым.
|
|
335
361
|
let next = null;
|
|
336
362
|
try {
|
|
337
|
-
const
|
|
338
|
-
const catalog = await readCatalog();
|
|
339
|
-
const { todo } = catalogBuckets(catalog, facts, coversOf(man).covered);
|
|
340
|
-
const adopt = declaredGates(man).length ? [] : proposeGates(await readAdoptFiles(CWD));
|
|
341
|
-
const blind = (probe?.classes || [])
|
|
342
|
-
.filter((b) => !facts.gateKeys.includes(b.slug))
|
|
343
|
-
.map((b) => ({ ...b, command: blindAdvice(catalog.find((r) => r.slug === b.slug), facts, {}).command }));
|
|
344
|
-
const start = startWith(todo, facts, 3).map((rec) => ({ slug: rec.slug, command: blindAdvice(rec, facts, {}).command }));
|
|
363
|
+
const { adopt, blind, start } = await readAdvice(man, probe);
|
|
345
364
|
next = nextSteps({ init: !man, adopt, blind, start });
|
|
346
365
|
} catch { /* не посчитали — блок скажет остальное; выдумывать шаги нельзя */ }
|
|
347
366
|
|
|
@@ -351,4 +370,4 @@ async function cmdContext(args = []) {
|
|
|
351
370
|
}).join("\n"));
|
|
352
371
|
}
|
|
353
372
|
|
|
354
|
-
export { cmdContext, contextBlock, nextSteps, parseLastRun, countArbiters, withHook, hasOurHook, portableSelf };
|
|
373
|
+
export { cmdContext, contextBlock, nextSteps, parseLastRun, countArbiters, withHook, hasOurHook, portableSelf, readRun, readAdvice };
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// tool/commands/doctor-catalog.mjs — что каталог говорит об ЭТОМ репозитории: какие записи
|
|
2
|
+
// держит машина, что у проекта уже есть, что пропустила проба, с чего начать, что неприменимо.
|
|
3
|
+
//
|
|
4
|
+
// Вынесено из doctor.mjs, когда тот дорос до 496 строк при пределе 500 (наш же
|
|
5
|
+
// `file-size-limit`). Шов настоящий: прогон гейтов и уровень — про то, что ОБЪЯВЛЕНО и как оно
|
|
6
|
+
// отработало; здесь — про каталог против фактов репозитория, и меняется это в другие дни.
|
|
7
|
+
|
|
8
|
+
import { readFile } from "node:fs/promises";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { CWD, SELF, c } from "../lib/core.mjs";
|
|
11
|
+
import { coversOf, coversUnproven } from "../lib/manifest.mjs";
|
|
12
|
+
import { readCatalog, browserServerAdvice } from "../lib/repo.mjs";
|
|
13
|
+
import { startWith, catalogBuckets, blindAdvice } from "../lib/advice.mjs";
|
|
14
|
+
import { proposeGates, readAdoptFiles } from "../lib/adopt.mjs";
|
|
15
|
+
import { assessBaseline, DEP_FILES, BASELINE_TOTAL } from "../lib/baseline.mjs";
|
|
16
|
+
import { declaredGates } from "../lib/run.mjs";
|
|
17
|
+
import { L } from "../i18n/index.mjs";
|
|
18
|
+
|
|
19
|
+
// Обязательный минимум проекта — прогоном, а не по памяти. До сих пор это было единственное
|
|
20
|
+
// место, где комплект просил верить на слово, что человек прочитал методичку и сверился.
|
|
21
|
+
async function reportBaseline(man, facts) {
|
|
22
|
+
const { readdir, readFile } = await import("node:fs/promises");
|
|
23
|
+
let files = [];
|
|
24
|
+
try {
|
|
25
|
+
files = (await readdir(CWD, { withFileTypes: true })).map((d) => d.name);
|
|
26
|
+
} catch { /* пустой список честнее выдуманного: ни один пункт не подтвердится */ }
|
|
27
|
+
|
|
28
|
+
// Файлы зависимостей читаются целиком и склеиваются: трекер ошибок объявляют по-разному в
|
|
29
|
+
// каждой экосистеме, а искать его надо одинаково.
|
|
30
|
+
let depsText = "";
|
|
31
|
+
for (const f of DEP_FILES) {
|
|
32
|
+
if (!files.some((n) => n.toLowerCase() === f)) continue;
|
|
33
|
+
try { depsText += (await readFile(join(CWD, f), "utf8")).toLowerCase() + "\n"; } catch { /* нечитаемый файл — просто не признак */ }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const rows = assessBaseline({ files, gateKeys: facts.gateKeys, facts, manifest: man || {}, depsText });
|
|
37
|
+
const okCount = rows.filter((r) => r.ok).length;
|
|
38
|
+
|
|
39
|
+
console.log(c.bold(`\n ${L.baseline.heading}\n`));
|
|
40
|
+
console.log(c.dim(` ${L.baseline.intro(rows.length, BASELINE_TOTAL)}`));
|
|
41
|
+
console.log(c.dim(` ${L.baseline.caveat}\n`));
|
|
42
|
+
for (const r of rows) {
|
|
43
|
+
const mark = r.ok ? c.green("✔") : c.yellow("✘");
|
|
44
|
+
const title = L.baseline.titles[r.key] || r.key;
|
|
45
|
+
console.log(` ${mark} ${String(r.n).padStart(2)}. ${title}`);
|
|
46
|
+
console.log(c.dim(` ${r.ok ? L.baseline.by(r.by) : L.baseline.none}`));
|
|
47
|
+
}
|
|
48
|
+
console.log(
|
|
49
|
+
"\n " + (okCount === rows.length ? c.green(`${okCount}/${rows.length}`) : c.yellow(`${okCount}/${rows.length}`)) +
|
|
50
|
+
c.dim(` · ${L.baseline.eyes(BASELINE_TOTAL - rows.length, "kit/docs/ai/project-baseline.md")}\n`)
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function reportCatalog(man, facts, probe = null, verbose = true) {
|
|
55
|
+
const catalog = await readCatalog();
|
|
56
|
+
if (!catalog.length) return;
|
|
57
|
+
|
|
58
|
+
// Четвёртая корзина, а не третья: «закрыто другим арбитром» — это НЕ «не поставлено».
|
|
59
|
+
// Пока их считали вместе, вывод каждый прогон называл долгом то, что уже держит biome или
|
|
60
|
+
// ruff. Просьба первого чужого пользователя; она же — наша собственная норма про вывод.
|
|
61
|
+
const { covered, unknownGates } = coversOf(man);
|
|
62
|
+
const { held, todo, skip, byOther } = catalogBuckets(catalog, facts, covered);
|
|
63
|
+
|
|
64
|
+
console.log(c.bold(`\n ${L.doctor.gatesHeading}\n`));
|
|
65
|
+
const marks = ["has_ci", "has_db", "has_docker", "has_tests", "has_deps"]
|
|
66
|
+
.filter((k) => facts[k])
|
|
67
|
+
.map((k) => k.replace("has_", ""));
|
|
68
|
+
console.log(
|
|
69
|
+
c.dim(` ${L.doctor.langs}: ${[...facts.langs].join(", ") || L.doctor.langsUnknown} · ${L.doctor.files}: ${facts.files}` +
|
|
70
|
+
(marks.length ? ` · ${L.doctor.hasThings}: ${marks.join(", ")}` : "") + "\n")
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
if (verbose) for (const rec of held) console.log(` ${c.green("✔")} ${rec.slug.padEnd(22)} ${c.dim(rec.intent || "")}`);
|
|
74
|
+
else if (held.length) console.log(` ${c.green("✔")} ${L.doctor.heldQuiet(held.length, `${SELF} doctor --verbose`)}`);
|
|
75
|
+
// ЧТО У ВАС УЖЕ ЕСТЬ — до итога и до списка крестов. Комплект, поставленный в проект с
|
|
76
|
+
// eslint, mocha и конвейером, показывал двадцать крестов и «держит машина 0»: мы считали
|
|
77
|
+
// только СВОИ записи, а чужие проверки не читали вовсе. С точки зрения владельца это
|
|
78
|
+
// неправда, и первое, что он видел, было обвинением. Предлагаем, а не вписываем: гейт в
|
|
79
|
+
// чужом манифесте без спроса — наше решение в чужом файле.
|
|
80
|
+
if (!declaredGates(man).length) {
|
|
81
|
+
const found = proposeGates(await readAdoptFiles(CWD));
|
|
82
|
+
if (found.length) {
|
|
83
|
+
console.log(`\n ${c.bold(L.doctor.haveAlready(found.length))}`);
|
|
84
|
+
for (const g of found) {
|
|
85
|
+
console.log(` ${c.green("✔")} ${g.name.padEnd(12)} ${c.dim(`${g.cmd} ← ${g.source}`)}`);
|
|
86
|
+
}
|
|
87
|
+
console.log(c.dim(` ${L.doctor.haveAlreadyHow(found.map((g) => `${g.name}: "${g.cmd}"`).join(" "))}`));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ЧТО ВАШИ ПРОВЕРКИ ПРОПУСТИЛИ. Проба знала имена непойманных классов и писала в отметку одно
|
|
92
|
+
// число; человек в `doctor` не видел ничего. Это самое конкретное, что мы знаем о проекте, —
|
|
93
|
+
// не «хорошая практика», а брак, подсаженный в ЕГО файл и ЕГО проверками не замеченный, —
|
|
94
|
+
// поэтому стоит выше списка «с чего начать». Читается из файла: ничего не запускает.
|
|
95
|
+
const blindOnes = (probe?.classes || []).map((b) => [b, catalog.find((r) => r.slug === b.slug)]).filter(([, r]) => r);
|
|
96
|
+
if (blindOnes.length) {
|
|
97
|
+
console.log(`\n ${c.yellow("⚠")} ${c.bold(L.doctor.blindHeading(probe.behind))}`);
|
|
98
|
+
for (const [b, rec] of blindOnes) {
|
|
99
|
+
// Три случая, и сливать их нельзя. Гейт стоял и проба его ГОНЯЛА — «стоит, но здесь не
|
|
100
|
+
// ловит», самое ценное. Гейт объявлен, но проба его не гоняла (поставлен позже или
|
|
101
|
+
// медленный) — «поймает ли, покажет следующая», а не «пойман». Гейта нет — совет.
|
|
102
|
+
const ranIt = probe.ran?.has(rec.slug);
|
|
103
|
+
const now = facts.gateKeys.includes(rec.slug);
|
|
104
|
+
console.log(` ${now && !ranIt ? c.dim("~") : c.red("✘")} ${rec.slug.padEnd(22)} ${c.dim(`${rec.intent || ""} ← ${b.file}`)}`);
|
|
105
|
+
if (ranIt) { console.log(c.dim(` ${L.doctor.blindRan(rec.slug)}`)); continue; }
|
|
106
|
+
if (now) { console.log(c.dim(` ${L.doctor.blindInstalled}`)); continue; }
|
|
107
|
+
const adv = blindAdvice(rec, facts, {});
|
|
108
|
+
if (adv.command) console.log(c.dim(` ${L.doctor.startCmd(adv.command)}`));
|
|
109
|
+
else console.log(c.dim(` ${L.doctor.install(`${SELF} add ${rec.slug}`)}`));
|
|
110
|
+
}
|
|
111
|
+
console.log(c.dim(` ${L.doctor.blindMore(`${SELF} probe`)}`));
|
|
112
|
+
} else if (probe?.state === "never" && declaredGates(man).length) {
|
|
113
|
+
console.log(c.dim(`\n ${L.doctor.probeNever(`${SELF} probe`)}`));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// С ЧЕГО НАЧАТЬ. Двадцать одинаковых крестов — это ноль требований: закрывают первое
|
|
117
|
+
// попавшееся или не закрывают ничего. Порядок не по нашему вкусу: сперва то, что родилось из
|
|
118
|
+
// настоящего отказа И закрывается одной готовой командой.
|
|
119
|
+
const first = todo.length > 3 ? startWith(todo, facts, 3) : [];
|
|
120
|
+
if (first.length) {
|
|
121
|
+
console.log(`\n ${c.bold(L.doctor.startWith)}`);
|
|
122
|
+
for (const rec of first) {
|
|
123
|
+
const adv = blindAdvice(rec, facts, {});
|
|
124
|
+
console.log(` ${c.yellow("→")} ${rec.slug.padEnd(22)} ${c.dim(rec.intent || "")}`);
|
|
125
|
+
if (adv.command) console.log(c.dim(` ${L.doctor.startCmd(adv.command)}`));
|
|
126
|
+
if (adv.tool) console.log(c.dim(` ${L.doctor.startTool(adv.tool)}`));
|
|
127
|
+
}
|
|
128
|
+
// Одна проверка руками — это разовый героизм. Сказать про хук здесь, а не в конце: человек
|
|
129
|
+
// читает первые строки и закрывает, а именно сейчас у него в руках список того, что стоит
|
|
130
|
+
// повесить перед пушем.
|
|
131
|
+
console.log(c.dim(`\n ${L.doctor.startHook}`));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ОСТАЛЬНОЕ — ПОСЛЕ ГЛАВНОГО И СЖАТО. Список шёл первым, по две строки на запись (вторая —
|
|
135
|
+
// «поставить: aqk add …»), и на requests главное начиналось со строки 84 из 102: человек
|
|
136
|
+
// читает сверху и закрывает раньше. Разбор соседа 2026-09-11 (research/competitors/agentlint.md):
|
|
137
|
+
// там первыми идут пять главных исправлений. Записи не теряются — теряется повтор подсказки.
|
|
138
|
+
const rest = todo.filter((r) => !first.includes(r));
|
|
139
|
+
if (rest.length) {
|
|
140
|
+
if (first.length) console.log(`\n ${c.bold(L.doctor.todoRest(rest.length))}`);
|
|
141
|
+
else console.log("");
|
|
142
|
+
// ○, а не ✘: запись не установлена — это не падение. Крест в зелёном прогоне глаз читает
|
|
143
|
+
// как провал, и через неделю человек перестаёт смотреть на красное вообще (отзыв с живого
|
|
144
|
+
// проекта 2026-09-11). ✘ остаётся за тем, что упало или пропустило брак.
|
|
145
|
+
for (const rec of rest) console.log(` ${c.dim("○")} ${rec.slug.padEnd(22)} ${rec.intent || ""}`);
|
|
146
|
+
console.log(c.dim(` ${L.doctor.todoRestHow(SELF)}`));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Второстепенное — в конце: что закрыто чужим арбитром, что неприменимо, советы без вердикта.
|
|
150
|
+
if (byOther.length) {
|
|
151
|
+
console.log(c.dim(`\n ${L.doctor.coveredBy(byOther.length)}`));
|
|
152
|
+
for (const [rec, gate] of byOther) console.log(c.dim(` ~ ${rec.slug.padEnd(22)} ${L.doctor.coveredByGate(gate)}`));
|
|
153
|
+
}
|
|
154
|
+
// Гейт, которого нет в gates:, не закрывает ничего — и молчать об этом нельзя: человек
|
|
155
|
+
// считает запись закрытой, а её не держит никто. Называется поимённо, жёлтым.
|
|
156
|
+
if (unknownGates.length) {
|
|
157
|
+
console.log(c.yellow(`\n ${L.doctor.coversUnknown(unknownGates.join(", "))}`));
|
|
158
|
+
}
|
|
159
|
+
// Заявка «эту запись держит наш линтер» сверяется с кодами правил из рецепта записи.
|
|
160
|
+
// Замерено на живом ruff.toml: девятнадцать групп правил, а print() не ловится — и заявка
|
|
161
|
+
// сняла бы запись с долга, не закрыв её ничем.
|
|
162
|
+
// Конфиги — ПО ЛИНТЕРАМ, а не одной склейкой: заявка сверяется правилами того линтера,
|
|
163
|
+
// которым закрыт гейт (отзыв с живого проекта 2026-09-11 — коды ruff искались в biome.json).
|
|
164
|
+
const readAll = async (names) => {
|
|
165
|
+
let t = "";
|
|
166
|
+
for (const f of names) { try { t += await readFile(join(CWD, f), "utf8") + "\n"; } catch { /* нет файла */ } }
|
|
167
|
+
return t;
|
|
168
|
+
};
|
|
169
|
+
let scripts = {}, pkgText = "";
|
|
170
|
+
try { pkgText = await readFile(join(CWD, "package.json"), "utf8"); scripts = JSON.parse(pkgText)?.scripts || {}; } catch { /* нет или не JSON */ }
|
|
171
|
+
const configs = {
|
|
172
|
+
// ruff.toml и .ruff.toml — конфиг ruff целиком, слово «ruff» в них писать незачем (поймал наш же
|
|
173
|
+
// smoke: `extend-select = [..., "T20"]` выбрасывался). pyproject.toml — только если в нём есть
|
|
174
|
+
// раздел ruff: он есть почти у каждого python-проекта и без ruff.
|
|
175
|
+
ruff: (await readAll(["ruff.toml", ".ruff.toml"])) +
|
|
176
|
+
((await readAll(["pyproject.toml"])).match(/^\[tool\.ruff[\s\S]*/m)?.[0] || ""),
|
|
177
|
+
eslint: (await readAll([".eslintrc", ".eslintrc.json", ".eslintrc.js", ".eslintrc.cjs", ".eslintrc.yml", "eslint.config.js", "eslint.config.mjs", "eslint.config.cjs", "eslint.config.ts"])) +
|
|
178
|
+
(/"eslintConfig"/.test(pkgText) ? pkgText : ""),
|
|
179
|
+
biome: await readAll(["biome.json", "biome.jsonc"]),
|
|
180
|
+
scripts,
|
|
181
|
+
};
|
|
182
|
+
for (const u of coversUnproven(man, catalog, configs)) {
|
|
183
|
+
if (u.kind === "unproven") {
|
|
184
|
+
console.log(c.yellow(`\n ${L.doctor.coversUnproven(u.entry, u.gate, u.codes.join(", "))}`));
|
|
185
|
+
console.log(c.dim(` ${L.doctor.coversUnprovenHow(`${SELF} add ${u.entry}`)}`));
|
|
186
|
+
} else if (u.kind === "impossible") {
|
|
187
|
+
console.log(c.yellow(`\n ${L.doctor.coversImpossible(u.entry, u.gate, u.linter)}`));
|
|
188
|
+
console.log(c.dim(` ${L.doctor.coversUnprovenHow(`${SELF} add ${u.entry}`)}`));
|
|
189
|
+
} else {
|
|
190
|
+
console.log(c.dim(`\n ${L.doctor.coversCantCheck(u.entry, u.gate)}`));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// Не вердикт, а совет: отсутствие браузерного сервера — незанятая возможность, а не дефект.
|
|
194
|
+
// Поэтому строка тусклая и без значка, и её нет у проекта без интерфейса.
|
|
195
|
+
let mcpText = "";
|
|
196
|
+
for (const f of [".mcp.json", ".cursor/mcp.json", ".vscode/mcp.json", ".claude/mcp.json"]) {
|
|
197
|
+
try { mcpText += await readFile(join(CWD, f), "utf8"); } catch { /* нет файла — нечего читать */ }
|
|
198
|
+
}
|
|
199
|
+
const browser = browserServerAdvice(facts, mcpText);
|
|
200
|
+
if (browser) {
|
|
201
|
+
console.log(c.dim(`\n ${L.doctor.noBrowserServer}`));
|
|
202
|
+
console.log(c.dim(` ${L.doctor.noBrowserServerHow(browser.servers.join(" · "))}`));
|
|
203
|
+
}
|
|
204
|
+
if (skip.length) {
|
|
205
|
+
if (verbose) {
|
|
206
|
+
console.log(c.dim(`\n ${L.doctor.notApplicable(skip.length)}`));
|
|
207
|
+
for (const [rec, why] of skip) console.log(c.dim(` · ${rec.slug.padEnd(22)} ${why}`));
|
|
208
|
+
} else {
|
|
209
|
+
console.log(c.dim(`\n ${L.doctor.skipQuiet(skip.length, `${SELF} doctor --verbose`)}`));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
console.log(
|
|
213
|
+
`\n ${c.bold(L.doctor.total)} ${L.doctor.totalHeld(held.length)}, ${L.doctor.totalTodo(c.yellow(todo.length))}, ` +
|
|
214
|
+
(byOther.length ? `${L.doctor.totalCovered(byOther.length)}, ` : "") +
|
|
215
|
+
c.dim(L.doctor.totalSkip(skip.length)) + "\n"
|
|
216
|
+
);
|
|
217
|
+
// Числа отдаются наружу, а не пересчитываются второй раз: два счёта одного и того же
|
|
218
|
+
// расходятся ровно так же, как два списка команд.
|
|
219
|
+
return { held: held.length, todo: todo.length, todoRecs: todo };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export { reportBaseline, reportCatalog };
|
package/tool/commands/doctor.mjs
CHANGED
|
@@ -3,218 +3,17 @@
|
|
|
3
3
|
import { readFile, mkdir, writeFile } from "node:fs/promises";
|
|
4
4
|
import { join, resolve } from "node:path";
|
|
5
5
|
import { spawnSync } from "node:child_process";
|
|
6
|
-
import { scopeOutput, splitAdvice, changedFiles } from "../lib/scope.mjs";
|
|
7
6
|
import { CWD, PKG_ROOT, TARGET_DIR, MANIFEST, SELF, c, exists, die, RUNTIME_FILES } from "../lib/core.mjs";
|
|
8
7
|
import { cmdProbe, probeStatus } from "./probe.mjs";
|
|
9
|
-
import { readManifest, assessLevel, unknownKeys, KNOWN_KEYS,
|
|
8
|
+
import { readManifest, assessLevel, unknownKeys, KNOWN_KEYS, layoutChecks, unparsedLines } from "../lib/manifest.mjs";
|
|
10
9
|
import { proveGates } from "../lib/prove.mjs";
|
|
11
|
-
import { detectFacts,
|
|
12
|
-
import {
|
|
13
|
-
import { proposeGates, readAdoptFiles } from "../lib/adopt.mjs";
|
|
14
|
-
import { assessBaseline, DEP_FILES, BASELINE_TOTAL } from "../lib/baseline.mjs";
|
|
10
|
+
import { detectFacts, claudeShimFor } from "../lib/repo.mjs";
|
|
11
|
+
import { reportBaseline, reportCatalog } from "./doctor-catalog.mjs";
|
|
15
12
|
import { L } from "../i18n/index.mjs";
|
|
16
13
|
import { countArbiters } from "./context.mjs";
|
|
17
14
|
import { beginBrief, finishBrief } from "../lib/brief.mjs";
|
|
18
15
|
import { declaredGates, sinceRef, runGates, progress, listArg } from "../lib/run.mjs";
|
|
19
|
-
import { autoProbeAllowed } from "../lib/cadence.mjs";
|
|
20
|
-
|
|
21
|
-
// Обязательный минимум проекта — прогоном, а не по памяти. До сих пор это было единственное
|
|
22
|
-
// место, где комплект просил верить на слово, что человек прочитал методичку и сверился.
|
|
23
|
-
async function reportBaseline(man, facts) {
|
|
24
|
-
const { readdir, readFile } = await import("node:fs/promises");
|
|
25
|
-
let files = [];
|
|
26
|
-
try {
|
|
27
|
-
files = (await readdir(CWD, { withFileTypes: true })).map((d) => d.name);
|
|
28
|
-
} catch { /* пустой список честнее выдуманного: ни один пункт не подтвердится */ }
|
|
29
|
-
|
|
30
|
-
// Файлы зависимостей читаются целиком и склеиваются: трекер ошибок объявляют по-разному в
|
|
31
|
-
// каждой экосистеме, а искать его надо одинаково.
|
|
32
|
-
let depsText = "";
|
|
33
|
-
for (const f of DEP_FILES) {
|
|
34
|
-
if (!files.some((n) => n.toLowerCase() === f)) continue;
|
|
35
|
-
try { depsText += (await readFile(join(CWD, f), "utf8")).toLowerCase() + "\n"; } catch { /* нечитаемый файл — просто не признак */ }
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const rows = assessBaseline({ files, gateKeys: facts.gateKeys, facts, manifest: man || {}, depsText });
|
|
39
|
-
const okCount = rows.filter((r) => r.ok).length;
|
|
40
|
-
|
|
41
|
-
console.log(c.bold(`\n ${L.baseline.heading}\n`));
|
|
42
|
-
console.log(c.dim(` ${L.baseline.intro(rows.length, BASELINE_TOTAL)}`));
|
|
43
|
-
console.log(c.dim(` ${L.baseline.caveat}\n`));
|
|
44
|
-
for (const r of rows) {
|
|
45
|
-
const mark = r.ok ? c.green("✔") : c.yellow("✘");
|
|
46
|
-
const title = L.baseline.titles[r.key] || r.key;
|
|
47
|
-
console.log(` ${mark} ${String(r.n).padStart(2)}. ${title}`);
|
|
48
|
-
console.log(c.dim(` ${r.ok ? L.baseline.by(r.by) : L.baseline.none}`));
|
|
49
|
-
}
|
|
50
|
-
console.log(
|
|
51
|
-
"\n " + (okCount === rows.length ? c.green(`${okCount}/${rows.length}`) : c.yellow(`${okCount}/${rows.length}`)) +
|
|
52
|
-
c.dim(` · ${L.baseline.eyes(BASELINE_TOTAL - rows.length, "kit/docs/ai/project-baseline.md")}\n`)
|
|
53
|
-
);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
async function reportCatalog(man, facts, probe = null) {
|
|
57
|
-
const catalog = await readCatalog();
|
|
58
|
-
if (!catalog.length) return;
|
|
59
|
-
|
|
60
|
-
// Четвёртая корзина, а не третья: «закрыто другим арбитром» — это НЕ «не поставлено».
|
|
61
|
-
// Пока их считали вместе, вывод каждый прогон называл долгом то, что уже держит biome или
|
|
62
|
-
// ruff. Просьба первого чужого пользователя; она же — наша собственная норма про вывод.
|
|
63
|
-
const { covered, unknownGates } = coversOf(man);
|
|
64
|
-
const { held, todo, skip, byOther } = catalogBuckets(catalog, facts, covered);
|
|
65
|
-
|
|
66
|
-
console.log(c.bold(`\n ${L.doctor.gatesHeading}\n`));
|
|
67
|
-
const marks = ["has_ci", "has_db", "has_docker", "has_tests", "has_deps"]
|
|
68
|
-
.filter((k) => facts[k])
|
|
69
|
-
.map((k) => k.replace("has_", ""));
|
|
70
|
-
console.log(
|
|
71
|
-
c.dim(` ${L.doctor.langs}: ${[...facts.langs].join(", ") || L.doctor.langsUnknown} · ${L.doctor.files}: ${facts.files}` +
|
|
72
|
-
(marks.length ? ` · ${L.doctor.hasThings}: ${marks.join(", ")}` : "") + "\n")
|
|
73
|
-
);
|
|
74
|
-
|
|
75
|
-
for (const rec of held) console.log(` ${c.green("✔")} ${rec.slug.padEnd(22)} ${c.dim(rec.intent || "")}`);
|
|
76
|
-
// ЧТО У ВАС УЖЕ ЕСТЬ — до итога и до списка крестов. Комплект, поставленный в проект с
|
|
77
|
-
// eslint, mocha и конвейером, показывал двадцать крестов и «держит машина 0»: мы считали
|
|
78
|
-
// только СВОИ записи, а чужие проверки не читали вовсе. С точки зрения владельца это
|
|
79
|
-
// неправда, и первое, что он видел, было обвинением. Предлагаем, а не вписываем: гейт в
|
|
80
|
-
// чужом манифесте без спроса — наше решение в чужом файле.
|
|
81
|
-
if (!declaredGates(man).length) {
|
|
82
|
-
const found = proposeGates(await readAdoptFiles(CWD));
|
|
83
|
-
if (found.length) {
|
|
84
|
-
console.log(`\n ${c.bold(L.doctor.haveAlready(found.length))}`);
|
|
85
|
-
for (const g of found) {
|
|
86
|
-
console.log(` ${c.green("✔")} ${g.name.padEnd(12)} ${c.dim(`${g.cmd} ← ${g.source}`)}`);
|
|
87
|
-
}
|
|
88
|
-
console.log(c.dim(` ${L.doctor.haveAlreadyHow(found.map((g) => `${g.name}: "${g.cmd}"`).join(" "))}`));
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// ЧТО ВАШИ ПРОВЕРКИ ПРОПУСТИЛИ. Проба знала имена непойманных классов и писала в отметку одно
|
|
93
|
-
// число; человек в `doctor` не видел ничего. Это самое конкретное, что мы знаем о проекте, —
|
|
94
|
-
// не «хорошая практика», а брак, подсаженный в ЕГО файл и ЕГО проверками не замеченный, —
|
|
95
|
-
// поэтому стоит выше списка «с чего начать». Читается из файла: ничего не запускает.
|
|
96
|
-
const blindOnes = (probe?.classes || []).map((b) => [b, catalog.find((r) => r.slug === b.slug)]).filter(([, r]) => r);
|
|
97
|
-
if (blindOnes.length) {
|
|
98
|
-
console.log(`\n ${c.yellow("⚠")} ${c.bold(L.doctor.blindHeading(probe.behind))}`);
|
|
99
|
-
for (const [b, rec] of blindOnes) {
|
|
100
|
-
// Три случая, и сливать их нельзя. Гейт стоял и проба его ГОНЯЛА — «стоит, но здесь не
|
|
101
|
-
// ловит», самое ценное. Гейт объявлен, но проба его не гоняла (поставлен позже или
|
|
102
|
-
// медленный) — «поймает ли, покажет следующая», а не «пойман». Гейта нет — совет.
|
|
103
|
-
const ranIt = probe.ran?.has(rec.slug);
|
|
104
|
-
const now = facts.gateKeys.includes(rec.slug);
|
|
105
|
-
console.log(` ${now && !ranIt ? c.dim("~") : c.red("✘")} ${rec.slug.padEnd(22)} ${c.dim(`${rec.intent || ""} ← ${b.file}`)}`);
|
|
106
|
-
if (ranIt) { console.log(c.dim(` ${L.doctor.blindRan(rec.slug)}`)); continue; }
|
|
107
|
-
if (now) { console.log(c.dim(` ${L.doctor.blindInstalled}`)); continue; }
|
|
108
|
-
const adv = blindAdvice(rec, facts, {});
|
|
109
|
-
if (adv.command) console.log(c.dim(` ${L.doctor.startCmd(adv.command)}`));
|
|
110
|
-
else console.log(c.dim(` ${L.doctor.install(`${SELF} add ${rec.slug}`)}`));
|
|
111
|
-
}
|
|
112
|
-
console.log(c.dim(` ${L.doctor.blindMore(`${SELF} probe`)}`));
|
|
113
|
-
} else if (probe?.state === "never" && declaredGates(man).length) {
|
|
114
|
-
console.log(c.dim(`\n ${L.doctor.probeNever(`${SELF} probe`)}`));
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// С ЧЕГО НАЧАТЬ. Двадцать одинаковых крестов — это ноль требований: закрывают первое
|
|
118
|
-
// попавшееся или не закрывают ничего. Порядок не по нашему вкусу: сперва то, что родилось из
|
|
119
|
-
// настоящего отказа И закрывается одной готовой командой.
|
|
120
|
-
const first = todo.length > 3 ? startWith(todo, facts, 3) : [];
|
|
121
|
-
if (first.length) {
|
|
122
|
-
console.log(`\n ${c.bold(L.doctor.startWith)}`);
|
|
123
|
-
for (const rec of first) {
|
|
124
|
-
const adv = blindAdvice(rec, facts, {});
|
|
125
|
-
console.log(` ${c.yellow("→")} ${rec.slug.padEnd(22)} ${c.dim(rec.intent || "")}`);
|
|
126
|
-
if (adv.command) console.log(c.dim(` ${L.doctor.startCmd(adv.command)}`));
|
|
127
|
-
if (adv.tool) console.log(c.dim(` ${L.doctor.startTool(adv.tool)}`));
|
|
128
|
-
}
|
|
129
|
-
// Одна проверка руками — это разовый героизм. Сказать про хук здесь, а не в конце: человек
|
|
130
|
-
// читает первые строки и закрывает, а именно сейчас у него в руках список того, что стоит
|
|
131
|
-
// повесить перед пушем.
|
|
132
|
-
console.log(c.dim(`\n ${L.doctor.startHook}`));
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// ОСТАЛЬНОЕ — ПОСЛЕ ГЛАВНОГО И СЖАТО. Список шёл первым, по две строки на запись (вторая —
|
|
136
|
-
// «поставить: aqk add …»), и на requests главное начиналось со строки 84 из 102: человек
|
|
137
|
-
// читает сверху и закрывает раньше. Разбор соседа 2026-09-11 (research/competitors/agentlint.md):
|
|
138
|
-
// там первыми идут пять главных исправлений. Записи не теряются — теряется повтор подсказки.
|
|
139
|
-
const rest = todo.filter((r) => !first.includes(r));
|
|
140
|
-
if (rest.length) {
|
|
141
|
-
if (first.length) console.log(`\n ${c.bold(L.doctor.todoRest(rest.length))}`);
|
|
142
|
-
else console.log("");
|
|
143
|
-
// ○, а не ✘: запись не установлена — это не падение. Крест в зелёном прогоне глаз читает
|
|
144
|
-
// как провал, и через неделю человек перестаёт смотреть на красное вообще (отзыв с живого
|
|
145
|
-
// проекта 2026-09-11). ✘ остаётся за тем, что упало или пропустило брак.
|
|
146
|
-
for (const rec of rest) console.log(` ${c.dim("○")} ${rec.slug.padEnd(22)} ${rec.intent || ""}`);
|
|
147
|
-
console.log(c.dim(` ${L.doctor.todoRestHow(SELF)}`));
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Второстепенное — в конце: что закрыто чужим арбитром, что неприменимо, советы без вердикта.
|
|
151
|
-
if (byOther.length) {
|
|
152
|
-
console.log(c.dim(`\n ${L.doctor.coveredBy(byOther.length)}`));
|
|
153
|
-
for (const [rec, gate] of byOther) console.log(c.dim(` ~ ${rec.slug.padEnd(22)} ${L.doctor.coveredByGate(gate)}`));
|
|
154
|
-
}
|
|
155
|
-
// Гейт, которого нет в gates:, не закрывает ничего — и молчать об этом нельзя: человек
|
|
156
|
-
// считает запись закрытой, а её не держит никто. Называется поимённо, жёлтым.
|
|
157
|
-
if (unknownGates.length) {
|
|
158
|
-
console.log(c.yellow(`\n ${L.doctor.coversUnknown(unknownGates.join(", "))}`));
|
|
159
|
-
}
|
|
160
|
-
// Заявка «эту запись держит наш линтер» сверяется с кодами правил из рецепта записи.
|
|
161
|
-
// Замерено на живом ruff.toml: девятнадцать групп правил, а print() не ловится — и заявка
|
|
162
|
-
// сняла бы запись с долга, не закрыв её ничем.
|
|
163
|
-
// Конфиги — ПО ЛИНТЕРАМ, а не одной склейкой: заявка сверяется правилами того линтера,
|
|
164
|
-
// которым закрыт гейт (отзыв с живого проекта 2026-09-11 — коды ruff искались в biome.json).
|
|
165
|
-
const readAll = async (names) => {
|
|
166
|
-
let t = "";
|
|
167
|
-
for (const f of names) { try { t += await readFile(join(CWD, f), "utf8") + "\n"; } catch { /* нет файла */ } }
|
|
168
|
-
return t;
|
|
169
|
-
};
|
|
170
|
-
let scripts = {}, pkgText = "";
|
|
171
|
-
try { pkgText = await readFile(join(CWD, "package.json"), "utf8"); scripts = JSON.parse(pkgText)?.scripts || {}; } catch { /* нет или не JSON */ }
|
|
172
|
-
const configs = {
|
|
173
|
-
// ruff.toml и .ruff.toml — конфиг ruff целиком, слово «ruff» в них писать незачем (поймал наш же
|
|
174
|
-
// smoke: `extend-select = [..., "T20"]` выбрасывался). pyproject.toml — только если в нём есть
|
|
175
|
-
// раздел ruff: он есть почти у каждого python-проекта и без ruff.
|
|
176
|
-
ruff: (await readAll(["ruff.toml", ".ruff.toml"])) +
|
|
177
|
-
((await readAll(["pyproject.toml"])).match(/^\[tool\.ruff[\s\S]*/m)?.[0] || ""),
|
|
178
|
-
eslint: (await readAll([".eslintrc", ".eslintrc.json", ".eslintrc.js", ".eslintrc.cjs", ".eslintrc.yml", "eslint.config.js", "eslint.config.mjs", "eslint.config.cjs", "eslint.config.ts"])) +
|
|
179
|
-
(/"eslintConfig"/.test(pkgText) ? pkgText : ""),
|
|
180
|
-
biome: await readAll(["biome.json", "biome.jsonc"]),
|
|
181
|
-
scripts,
|
|
182
|
-
};
|
|
183
|
-
for (const u of coversUnproven(man, catalog, configs)) {
|
|
184
|
-
if (u.kind === "unproven") {
|
|
185
|
-
console.log(c.yellow(`\n ${L.doctor.coversUnproven(u.entry, u.gate, u.codes.join(", "))}`));
|
|
186
|
-
console.log(c.dim(` ${L.doctor.coversUnprovenHow(`${SELF} add ${u.entry}`)}`));
|
|
187
|
-
} else if (u.kind === "impossible") {
|
|
188
|
-
console.log(c.yellow(`\n ${L.doctor.coversImpossible(u.entry, u.gate, u.linter)}`));
|
|
189
|
-
console.log(c.dim(` ${L.doctor.coversUnprovenHow(`${SELF} add ${u.entry}`)}`));
|
|
190
|
-
} else {
|
|
191
|
-
console.log(c.dim(`\n ${L.doctor.coversCantCheck(u.entry, u.gate)}`));
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
// Не вердикт, а совет: отсутствие браузерного сервера — незанятая возможность, а не дефект.
|
|
195
|
-
// Поэтому строка тусклая и без значка, и её нет у проекта без интерфейса.
|
|
196
|
-
let mcpText = "";
|
|
197
|
-
for (const f of [".mcp.json", ".cursor/mcp.json", ".vscode/mcp.json", ".claude/mcp.json"]) {
|
|
198
|
-
try { mcpText += await readFile(join(CWD, f), "utf8"); } catch { /* нет файла — нечего читать */ }
|
|
199
|
-
}
|
|
200
|
-
const browser = browserServerAdvice(facts, mcpText);
|
|
201
|
-
if (browser) {
|
|
202
|
-
console.log(c.dim(`\n ${L.doctor.noBrowserServer}`));
|
|
203
|
-
console.log(c.dim(` ${L.doctor.noBrowserServerHow(browser.servers.join(" · "))}`));
|
|
204
|
-
}
|
|
205
|
-
if (skip.length) {
|
|
206
|
-
console.log(c.dim(`\n ${L.doctor.notApplicable(skip.length)}`));
|
|
207
|
-
for (const [rec, why] of skip) console.log(c.dim(` · ${rec.slug.padEnd(22)} ${why}`));
|
|
208
|
-
}
|
|
209
|
-
console.log(
|
|
210
|
-
`\n ${c.bold(L.doctor.total)} ${L.doctor.totalHeld(held.length)}, ${L.doctor.totalTodo(c.yellow(todo.length))}, ` +
|
|
211
|
-
(byOther.length ? `${L.doctor.totalCovered(byOther.length)}, ` : "") +
|
|
212
|
-
c.dim(L.doctor.totalSkip(skip.length)) + "\n"
|
|
213
|
-
);
|
|
214
|
-
// Числа отдаются наружу, а не пересчитываются второй раз: два счёта одного и того же
|
|
215
|
-
// расходятся ровно так же, как два списка команд.
|
|
216
|
-
return { held: held.length, todo: todo.length, todoRecs: todo };
|
|
217
|
-
}
|
|
16
|
+
import { autoProbeAllowed, levelLimits } from "../lib/cadence.mjs";
|
|
218
17
|
|
|
219
18
|
// Короткий отчёт «что из этого реально брали» — не для человека, а для агента в следующей
|
|
220
19
|
// сессии и для самого владельца: список объявленных гейтов молчит о том, сколько из них
|
|
@@ -259,6 +58,12 @@ async function autoProbe(brief) {
|
|
|
259
58
|
|
|
260
59
|
async function cmdDoctor() {
|
|
261
60
|
const brief = process.argv.includes("--brief");
|
|
61
|
+
// Коротко по умолчанию, поимённо по `--verbose`. Отзыв с живого проекта 2026-09-11: вывод на
|
|
62
|
+
// сто строк, из них семьдесят — зелёные галочки, и красное теряется между ними. Сворачивается
|
|
63
|
+
// только то, что ничего не требует: пройденное, неприменимое, пояснения. Упавшее, совет и
|
|
64
|
+
// «что поставить» печатаются всегда.
|
|
65
|
+
// AQK_VERBOSE=1 — то же для конвейера, где лог читают потом и целиком.
|
|
66
|
+
const verbose = process.argv.includes("--verbose") || process.env.AQK_VERBOSE === "1";
|
|
262
67
|
const buf = brief ? beginBrief() : null;
|
|
263
68
|
// Версия в шапке — единственное, что привязывает баг-репорт к коммиту, если ставили не из
|
|
264
69
|
// релиза: без неё "у меня не работает" ничем не отличается от любой другой версии за год.
|
|
@@ -301,6 +106,11 @@ async function cmdDoctor() {
|
|
|
301
106
|
}
|
|
302
107
|
}
|
|
303
108
|
|
|
109
|
+
// СВОД, КОТОРОГО НЕ ВИДИТ CLAUDE CODE. Он читает CLAUDE.md, а не AGENTS.md (документация,
|
|
110
|
+
// сверено 2026-09-11); подробности и исходы — claudeSeesRules.
|
|
111
|
+
const shim = await claudeShimFor(CWD);
|
|
112
|
+
if (shim) console.log(`\n ${c.yellow("!")} ${L.doctor.claudeShim[shim]}`);
|
|
113
|
+
|
|
304
114
|
// Команды в точке входа заполнены или остались пустыми заготовками? Файл берётся тот же,
|
|
305
115
|
// что проверен выше, — иначе проект на `CLAUDE.md` этой проверки не получал вовсе.
|
|
306
116
|
const entryFile = (Array.isArray(man?.entry) ? man.entry : []).find((e) => typeof e === "string" && e.trim())?.trim() || "AGENTS.md";
|
|
@@ -319,7 +129,7 @@ async function cmdDoctor() {
|
|
|
319
129
|
const arb = countArbiters(text, ["человек", "human", "nobody"]);
|
|
320
130
|
if (arb.total && arb.human) {
|
|
321
131
|
console.log(`\n ${c.yellow("!")} ${L.doctor.rulesByHuman(arb.total, arb.machine, arb.human)}`);
|
|
322
|
-
console.log(c.dim(` ${L.doctor.rulesByHumanWhy}`));
|
|
132
|
+
if (verbose) console.log(c.dim(` ${L.doctor.rulesByHumanWhy}`));
|
|
323
133
|
}
|
|
324
134
|
|
|
325
135
|
const emptyCommands = (text.match(/^- [^:]+: ``$/gm) || []).length;
|
|
@@ -390,6 +200,16 @@ async function cmdDoctor() {
|
|
|
390
200
|
} else {
|
|
391
201
|
console.log(c.green(` ${L.doctor.allDone}\n`));
|
|
392
202
|
}
|
|
203
|
+
// Состояние пробы — из файла отметки, миллисекунды. Нет его — блок про пробу просто молчит.
|
|
204
|
+
let probe = null;
|
|
205
|
+
try { probe = await probeStatus(); } catch { /* пробы нет — и ладно */ }
|
|
206
|
+
// Чего уровень НЕ доказывает — сразу под ним, пока глаз на нём (см. levelLimits).
|
|
207
|
+
if (reached >= 1) {
|
|
208
|
+
const lim = levelLimits(probe);
|
|
209
|
+
console.log(c.dim(` ${L.doctor.limitsTitle}`));
|
|
210
|
+
console.log(` ${L.doctor.limitsProbe[lim.kind](lim, `${SELF} probe`)}`);
|
|
211
|
+
console.log(` ${L.doctor.limitsCi}\n`);
|
|
212
|
+
}
|
|
393
213
|
|
|
394
214
|
const facts = await detectFacts(man);
|
|
395
215
|
if (process.argv.includes("--baseline")) {
|
|
@@ -402,10 +222,7 @@ async function cmdDoctor() {
|
|
|
402
222
|
await reportBaseline(man, facts);
|
|
403
223
|
process.exit(0);
|
|
404
224
|
}
|
|
405
|
-
|
|
406
|
-
let probe = null;
|
|
407
|
-
try { probe = await probeStatus(); } catch { /* пробы нет — и ладно */ }
|
|
408
|
-
const cat = (await reportCatalog(man, facts, probe)) || { held: 0, todo: 0, todoRecs: [] };
|
|
225
|
+
const cat = (await reportCatalog(man, facts, probe, verbose)) || { held: 0, todo: 0, todoRecs: [] };
|
|
409
226
|
|
|
410
227
|
// «Объявлен» ≠ «работает». Без --run говорим это вслух, а не молчим.
|
|
411
228
|
const wantRun = process.argv.includes("--run");
|
|
@@ -420,7 +237,7 @@ async function cmdDoctor() {
|
|
|
420
237
|
const ji = process.argv.indexOf("--jobs");
|
|
421
238
|
const jobs = ji > -1 ? Number(process.argv[ji + 1]) : 1;
|
|
422
239
|
if (!Number.isInteger(jobs) || jobs < 1) die(L.doctor.jobsBad(process.argv[ji + 1] ?? ""));
|
|
423
|
-
const run = await runGates(man, { since: sinceRef(), only: listArg(process.argv, "--only"), skip: listArg(process.argv, "--skip"), jobs });
|
|
240
|
+
const run = await runGates(man, { since: sinceRef(), only: listArg(process.argv, "--only"), skip: listArg(process.argv, "--skip"), jobs, verbose });
|
|
424
241
|
gateFailed = run.failed;
|
|
425
242
|
failedNames = run.results.filter((r) => !r.ok).map((r) => r.name);
|
|
426
243
|
skippedNames = run.skipped || [];
|