agent-quality-kit 0.12.0 → 0.13.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 +26 -4
- package/README.ru.md +26 -4
- package/kit/gates/complexity-limit/gate.yml +4 -0
- package/kit/gates/dead-code/gate.yml +4 -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 +4 -2
- package/package.json +1 -1
- package/tool/commands/badge.mjs +1 -1
- package/tool/commands/context.mjs +56 -3
- package/tool/commands/doctor.mjs +136 -81
- package/tool/commands/probe.mjs +46 -66
- package/tool/commands/project.mjs +6 -1
- 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 +13 -1
- package/tool/i18n/ru-docs.mjs +24 -1
- package/tool/i18n/ru-gates.mjs +6 -1
- package/tool/i18n/ru.mjs +13 -1
- package/tool/lib/adopt.mjs +15 -1
- package/tool/lib/advice.mjs +115 -0
- package/tool/lib/brief.mjs +3 -1
- package/tool/lib/cadence.mjs +11 -1
- package/tool/lib/core.mjs +39 -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 +2 -31
- package/tool/lib/run.mjs +78 -7
- package/tool/selfcheck/smoke/first-run.test.mjs +105 -0
- package/tool/selfcheck/smoke/verdict.test.mjs +51 -2
- package/tool/selfcheck/smoke.sh +6 -2
- package/tool/selfcheck/units-cadence.mjs +15 -1
- package/tool/selfcheck/units-context.mjs +64 -1
- package/tool/selfcheck/units-level.mjs +76 -2
- package/tool/selfcheck/units-probe.mjs +2 -1
- package/tool/selfcheck/units-repo.mjs +25 -1
- package/tool/selfcheck/units-verdict.mjs +76 -0
package/tool/commands/doctor.mjs
CHANGED
|
@@ -4,17 +4,19 @@ 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
6
|
import { scopeOutput, splitAdvice, changedFiles } from "../lib/scope.mjs";
|
|
7
|
-
import { CWD, PKG_ROOT, TARGET_DIR, MANIFEST, SELF, c, exists, die } from "../lib/core.mjs";
|
|
8
|
-
import { cmdProbe, probeStatus
|
|
7
|
+
import { CWD, PKG_ROOT, TARGET_DIR, MANIFEST, SELF, c, exists, die, RUNTIME_FILES } from "../lib/core.mjs";
|
|
8
|
+
import { cmdProbe, probeStatus } from "./probe.mjs";
|
|
9
9
|
import { readManifest, assessLevel, unknownKeys, KNOWN_KEYS, advisorySet, layoutChecks, coversOf, coversUnproven, unparsedLines } from "../lib/manifest.mjs";
|
|
10
10
|
import { proveGates } from "../lib/prove.mjs";
|
|
11
|
-
import { detectFacts, readCatalog,
|
|
12
|
-
import {
|
|
11
|
+
import { detectFacts, readCatalog, browserServerAdvice } from "../lib/repo.mjs";
|
|
12
|
+
import { startWith, catalogBuckets, blindAdvice } from "../lib/advice.mjs";
|
|
13
|
+
import { proposeGates, readAdoptFiles } from "../lib/adopt.mjs";
|
|
13
14
|
import { assessBaseline, DEP_FILES, BASELINE_TOTAL } from "../lib/baseline.mjs";
|
|
14
15
|
import { L } from "../i18n/index.mjs";
|
|
15
16
|
import { countArbiters } from "./context.mjs";
|
|
16
17
|
import { beginBrief, finishBrief } from "../lib/brief.mjs";
|
|
17
|
-
import { declaredGates, sinceRef, runGates, progress } from "../lib/run.mjs";
|
|
18
|
+
import { declaredGates, sinceRef, runGates, progress, listArg } from "../lib/run.mjs";
|
|
19
|
+
import { autoProbeAllowed } from "../lib/cadence.mjs";
|
|
18
20
|
|
|
19
21
|
// Обязательный минимум проекта — прогоном, а не по памяти. До сих пор это было единственное
|
|
20
22
|
// место, где комплект просил верить на слово, что человек прочитал методичку и сверился.
|
|
@@ -59,14 +61,7 @@ async function reportCatalog(man, facts, probe = null) {
|
|
|
59
61
|
// Пока их считали вместе, вывод каждый прогон называл долгом то, что уже держит biome или
|
|
60
62
|
// ruff. Просьба первого чужого пользователя; она же — наша собственная норма про вывод.
|
|
61
63
|
const { covered, unknownGates } = coversOf(man);
|
|
62
|
-
const held
|
|
63
|
-
for (const rec of catalog) {
|
|
64
|
-
const v = triggerVerdict(rec, facts);
|
|
65
|
-
if (!v.applies) skip.push([rec, v.why]);
|
|
66
|
-
else if (facts.gateKeys.includes(rec.slug)) held.push(rec);
|
|
67
|
-
else if (covered.has(rec.slug)) byOther.push([rec, covered.get(rec.slug)]);
|
|
68
|
-
else todo.push(rec);
|
|
69
|
-
}
|
|
64
|
+
const { held, todo, skip, byOther } = catalogBuckets(catalog, facts, covered);
|
|
70
65
|
|
|
71
66
|
console.log(c.bold(`\n ${L.doctor.gatesHeading}\n`));
|
|
72
67
|
const marks = ["has_ci", "has_db", "has_docker", "has_tests", "has_deps"]
|
|
@@ -78,58 +73,13 @@ async function reportCatalog(man, facts, probe = null) {
|
|
|
78
73
|
);
|
|
79
74
|
|
|
80
75
|
for (const rec of held) console.log(` ${c.green("✔")} ${rec.slug.padEnd(22)} ${c.dim(rec.intent || "")}`);
|
|
81
|
-
for (const rec of todo) {
|
|
82
|
-
console.log(` ${c.yellow("✘")} ${rec.slug.padEnd(22)} ${rec.intent || ""}`);
|
|
83
|
-
console.log(c.dim(` ${L.doctor.install(`${SELF} add ${rec.slug}`)}`));
|
|
84
|
-
}
|
|
85
|
-
if (byOther.length) {
|
|
86
|
-
console.log(c.dim(`\n ${L.doctor.coveredBy(byOther.length)}`));
|
|
87
|
-
for (const [rec, gate] of byOther) console.log(c.dim(` ~ ${rec.slug.padEnd(22)} ${L.doctor.coveredByGate(gate)}`));
|
|
88
|
-
}
|
|
89
|
-
// Гейт, которого нет в gates:, не закрывает ничего — и молчать об этом нельзя: человек
|
|
90
|
-
// считает запись закрытой, а её не держит никто. Называется поимённо, жёлтым.
|
|
91
|
-
if (unknownGates.length) {
|
|
92
|
-
console.log(c.yellow(`\n ${L.doctor.coversUnknown(unknownGates.join(", "))}`));
|
|
93
|
-
}
|
|
94
|
-
// Заявка «эту запись держит наш линтер» сверяется с кодами правил из рецепта записи.
|
|
95
|
-
// Замерено на живом ruff.toml: девятнадцать групп правил, а print() не ловится — и заявка
|
|
96
|
-
// сняла бы запись с долга, не закрыв её ничем.
|
|
97
|
-
let linterCfg = "";
|
|
98
|
-
for (const f of ["ruff.toml", ".ruff.toml", "pyproject.toml", ".eslintrc.json", "eslint.config.js", "eslint.config.mjs", "biome.json"]) {
|
|
99
|
-
try { linterCfg += await readFile(join(CWD, f), "utf8"); } catch { /* нет файла — нечего читать */ }
|
|
100
|
-
}
|
|
101
|
-
const unproven = coversUnproven(man, catalog, linterCfg);
|
|
102
|
-
for (const u of unproven) {
|
|
103
|
-
console.log(c.yellow(`\n ${L.doctor.coversUnproven(u.entry, u.gate, u.codes.join(", "))}`));
|
|
104
|
-
console.log(c.dim(` ${L.doctor.coversUnprovenHow(`${SELF} add ${u.entry}`)}`));
|
|
105
|
-
}
|
|
106
|
-
// Не вердикт, а совет: отсутствие браузерного сервера — незанятая возможность, а не дефект.
|
|
107
|
-
// Поэтому строка тусклая и без значка, и её нет у проекта без интерфейса.
|
|
108
|
-
let mcpText = "";
|
|
109
|
-
for (const f of [".mcp.json", ".cursor/mcp.json", ".vscode/mcp.json", ".claude/mcp.json"]) {
|
|
110
|
-
try { mcpText += await readFile(join(CWD, f), "utf8"); } catch { /* нет файла — нечего читать */ }
|
|
111
|
-
}
|
|
112
|
-
const browser = browserServerAdvice(facts, mcpText);
|
|
113
|
-
if (browser) {
|
|
114
|
-
console.log(c.dim(`\n ${L.doctor.noBrowserServer}`));
|
|
115
|
-
console.log(c.dim(` ${L.doctor.noBrowserServerHow(browser.servers.join(" · "))}`));
|
|
116
|
-
}
|
|
117
|
-
if (skip.length) {
|
|
118
|
-
console.log(c.dim(`\n ${L.doctor.notApplicable(skip.length)}`));
|
|
119
|
-
for (const [rec, why] of skip) console.log(c.dim(` · ${rec.slug.padEnd(22)} ${why}`));
|
|
120
|
-
}
|
|
121
76
|
// ЧТО У ВАС УЖЕ ЕСТЬ — до итога и до списка крестов. Комплект, поставленный в проект с
|
|
122
77
|
// eslint, mocha и конвейером, показывал двадцать крестов и «держит машина 0»: мы считали
|
|
123
78
|
// только СВОИ записи, а чужие проверки не читали вовсе. С точки зрения владельца это
|
|
124
79
|
// неправда, и первое, что он видел, было обвинением. Предлагаем, а не вписываем: гейт в
|
|
125
80
|
// чужом манифесте без спроса — наше решение в чужом файле.
|
|
126
81
|
if (!declaredGates(man).length) {
|
|
127
|
-
const
|
|
128
|
-
for (const n of ADOPT_FILES) {
|
|
129
|
-
try { files[n] = await readFile(join(CWD, n), "utf8"); } catch { /* нет — и ладно */ }
|
|
130
|
-
}
|
|
131
|
-
for (const n of ADOPT_SCRIPTS) if (await exists(join(CWD, n))) files[n] = "";
|
|
132
|
-
const found = proposeGates(files);
|
|
82
|
+
const found = proposeGates(await readAdoptFiles(CWD));
|
|
133
83
|
if (found.length) {
|
|
134
84
|
console.log(`\n ${c.bold(L.doctor.haveAlready(found.length))}`);
|
|
135
85
|
for (const g of found) {
|
|
@@ -167,8 +117,8 @@ async function reportCatalog(man, facts, probe = null) {
|
|
|
167
117
|
// С ЧЕГО НАЧАТЬ. Двадцать одинаковых крестов — это ноль требований: закрывают первое
|
|
168
118
|
// попавшееся или не закрывают ничего. Порядок не по нашему вкусу: сперва то, что родилось из
|
|
169
119
|
// настоящего отказа И закрывается одной готовой командой.
|
|
170
|
-
|
|
171
|
-
|
|
120
|
+
const first = todo.length > 3 ? startWith(todo, facts, 3) : [];
|
|
121
|
+
if (first.length) {
|
|
172
122
|
console.log(`\n ${c.bold(L.doctor.startWith)}`);
|
|
173
123
|
for (const rec of first) {
|
|
174
124
|
const adv = blindAdvice(rec, facts, {});
|
|
@@ -182,6 +132,80 @@ async function reportCatalog(man, facts, probe = null) {
|
|
|
182
132
|
console.log(c.dim(`\n ${L.doctor.startHook}`));
|
|
183
133
|
}
|
|
184
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
|
+
}
|
|
185
209
|
console.log(
|
|
186
210
|
`\n ${c.bold(L.doctor.total)} ${L.doctor.totalHeld(held.length)}, ${L.doctor.totalTodo(c.yellow(todo.length))}, ` +
|
|
187
211
|
(byOther.length ? `${L.doctor.totalCovered(byOther.length)}, ` : "") +
|
|
@@ -196,7 +220,7 @@ async function reportCatalog(man, facts, probe = null) {
|
|
|
196
220
|
// сессии и для самого владельца: список объявленных гейтов молчит о том, сколько из них
|
|
197
221
|
// действительно стоят и работают именно СЕЙЧАС. Перезаписывается каждым прогоном, не копится:
|
|
198
222
|
// история — дело git-лога коммитов с этим отчётом, если владелец решит его коммитить.
|
|
199
|
-
async function writeRunReport({ version, reached, results }) {
|
|
223
|
+
async function writeRunReport({ version, reached, results, skipped = [] }) {
|
|
200
224
|
const stamp = new Date().toISOString().replace("T", " ").slice(0, 16);
|
|
201
225
|
const ok = results.filter((r) => r.ok).length;
|
|
202
226
|
const lines = [
|
|
@@ -205,6 +229,9 @@ async function writeRunReport({ version, reached, results }) {
|
|
|
205
229
|
`${L.report.level}: AQK-${reached < 0 ? L.doctor.levelNone : reached}`,
|
|
206
230
|
"",
|
|
207
231
|
...results.map((r) => `${r.ok ? "✔" : "✘"} ${r.name} — ${r.secs}s${r.ok ? "" : ` (${r.note || L.doctor.exitCode(r.code)})`}`),
|
|
232
|
+
// Пропущенные по --skip/--only — строкой «~»: блок для агента читает их как «не запускались»,
|
|
233
|
+
// а не как зелёные. Молчание о них прочиталось бы как «проверено».
|
|
234
|
+
...skipped.map((n) => `~ ${n} — ${L.report.skippedBySelect}`),
|
|
208
235
|
"",
|
|
209
236
|
L.report.summary(ok, results.length),
|
|
210
237
|
].filter((l) => l !== null);
|
|
@@ -214,6 +241,22 @@ async function writeRunReport({ version, reached, results }) {
|
|
|
214
241
|
await writeFile(dst, lines.join("\n") + "\n", "utf8");
|
|
215
242
|
}
|
|
216
243
|
|
|
244
|
+
// ПРОБА ЗАПУСКАЕТСЯ САМА, раз в сто коммитов, — кроме конвейера (там это минуты сюрпризом в
|
|
245
|
+
// быстрой проверке, отзыв с живого проекта 2026-09-11). Не влияет на код возврата никогда: это
|
|
246
|
+
// осмотр, а не порог. Отдельной функцией: внутри прогона эта лесенка дала вложенность 6, и наш же
|
|
247
|
+
// complexity-limit её поймал.
|
|
248
|
+
async function autoProbe(brief) {
|
|
249
|
+
let st;
|
|
250
|
+
try { st = await probeStatus(); } catch { return; /* пробы нет — прогон про гейты, а не про неё */ }
|
|
251
|
+
if (st.badEvery !== undefined) { console.log(c.yellow(`\n ${L.probe.badEvery(st.badEvery)}`)); return; }
|
|
252
|
+
if (st.state !== "never" && st.state !== "stale") return;
|
|
253
|
+
if (!autoProbeAllowed({ brief })) { console.log(c.dim(`\n ${L.probe.autoNotInCi(`${SELF} probe`)}`)); return; }
|
|
254
|
+
// Сообщение обязано быть верным в обоих случаях: первая версия печатала «прошло сто коммитов»
|
|
255
|
+
// и там, где пробы не было ВОВСЕ — число бралось из порога, а не из факта.
|
|
256
|
+
console.log(c.dim(`\n ${st.state === "never" ? L.probe.autoFirst : L.probe.auto(st.behind)}`));
|
|
257
|
+
try { await cmdProbe([], { auto: true }); } catch { /* проба не состоялась — прогон это не роняет */ }
|
|
258
|
+
}
|
|
259
|
+
|
|
217
260
|
async function cmdDoctor() {
|
|
218
261
|
const brief = process.argv.includes("--brief");
|
|
219
262
|
const buf = brief ? beginBrief() : null;
|
|
@@ -236,10 +279,26 @@ async function cmdDoctor() {
|
|
|
236
279
|
const checks = layoutChecks(man, inKit);
|
|
237
280
|
|
|
238
281
|
let missing = 0;
|
|
239
|
-
for (const [path, what] of checks) {
|
|
282
|
+
for (const [path, what, required] of checks) {
|
|
240
283
|
const ok = await exists(join(CWD, path));
|
|
241
|
-
if (!ok) missing++;
|
|
242
|
-
|
|
284
|
+
if (!ok && required) missing++;
|
|
285
|
+
const mark = ok ? c.green("✔") : required ? c.red("✘") : c.dim("○");
|
|
286
|
+
console.log(` ${mark} ${path.padEnd(22)} ${c.dim(what)}${!ok && !required ? c.dim(` · ${L.doctor.layoutAdvice}`) : ""}`);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// СЛУЖЕБНЫЙ ФАЙЛ, КОТОРЫЙ ВИДИТ GIT. Отзыв с живого проекта 2026-09-11: `.aqk/last-run.md`
|
|
290
|
+
// однажды закоммитили, и каждый прогон оставлял изменённый файл. `init` теперь кладёт их в
|
|
291
|
+
// .gitignore сам; здесь — для тех, кто поставил раньше. Спрашиваем git, а не диск.
|
|
292
|
+
const git = (...a) => spawnSync("git", a, { cwd: CWD, encoding: "utf8" });
|
|
293
|
+
if (git("rev-parse", "--git-dir").status === 0) {
|
|
294
|
+
const tracked = new Set(String(git("ls-files", "--", TARGET_DIR).stdout || "").split("\n"));
|
|
295
|
+
for (const f of RUNTIME_FILES.map((n) => `${TARGET_DIR}/${n}`)) {
|
|
296
|
+
if (tracked.has(f)) {
|
|
297
|
+
console.log(`\n ${c.yellow("!")} ${L.doctor.runtimeTracked(f, `git rm --cached ${f} && echo ${f} >> .gitignore`)}`);
|
|
298
|
+
} else if (await exists(join(CWD, f)) && git("check-ignore", "-q", f).status !== 0) {
|
|
299
|
+
console.log(c.dim(`\n ${L.doctor.runtimeNotIgnored(f, `echo ${f} >> .gitignore`)}`));
|
|
300
|
+
}
|
|
301
|
+
}
|
|
243
302
|
}
|
|
244
303
|
|
|
245
304
|
// Команды в точке входа заполнены или остались пустыми заготовками? Файл берётся тот же,
|
|
@@ -353,11 +412,19 @@ async function cmdDoctor() {
|
|
|
353
412
|
const gates = declaredGates(man);
|
|
354
413
|
let gateFailed = 0;
|
|
355
414
|
let failedNames = [];
|
|
415
|
+
let skippedNames = [];
|
|
356
416
|
if (wantRun) {
|
|
357
|
-
|
|
417
|
+
// --jobs N: сколько гейтов одновременно. Без флага — по одному, как было: чужие гейты бывают
|
|
418
|
+
// зависимыми (общий dist/), и плавающее красное хуже медленного. Не число — отказ, а не тихий
|
|
419
|
+
// последовательный прогон под видом параллельного.
|
|
420
|
+
const ji = process.argv.indexOf("--jobs");
|
|
421
|
+
const jobs = ji > -1 ? Number(process.argv[ji + 1]) : 1;
|
|
422
|
+
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 });
|
|
358
424
|
gateFailed = run.failed;
|
|
359
425
|
failedNames = run.results.filter((r) => !r.ok).map((r) => r.name);
|
|
360
|
-
|
|
426
|
+
skippedNames = run.skipped || [];
|
|
427
|
+
await writeRunReport({ version, reached, results: run.results, skipped: run.skipped });
|
|
361
428
|
|
|
362
429
|
// ПРОБА ЗАПУСКАЕТСЯ САМА. Владелец сформулировал так: «команду, о которой надо вспомнить,
|
|
363
430
|
// агент не вспомнит, а человек о ней не узнает». Это тот же класс, что файл, который можно
|
|
@@ -369,20 +436,7 @@ async function cmdDoctor() {
|
|
|
369
436
|
// надо. В кратком режиме не запускается: там хук на воротах коммита, и лишние секунды там
|
|
370
437
|
// стоят дороже. Не влияет на код возврата НИКОГДА — это осмотр, а не порог.
|
|
371
438
|
// Выключается AQK_PROBE=0 — у всего, что случается само, обязан быть выключатель.
|
|
372
|
-
if (!brief && process.env.AQK_PROBE !== "0")
|
|
373
|
-
try {
|
|
374
|
-
const st = await probeStatus();
|
|
375
|
-
if (st.badEvery !== undefined) {
|
|
376
|
-
console.log(c.yellow(`\n ${L.probe.badEvery(st.badEvery)}`));
|
|
377
|
-
} else if (st.state === "never" || st.state === "stale") {
|
|
378
|
-
// Сообщение обязано быть верным в обоих случаях. Первая версия печатала «прошло сто
|
|
379
|
-
// коммитов» и там, где пробы не было ВОВСЕ: число бралось из порога, а не из факта.
|
|
380
|
-
// Мелочь, но того же класса, что и всё остальное здесь: вывод, который не врёт.
|
|
381
|
-
console.log(c.dim(`\n ${st.state === "never" ? L.probe.autoFirst : L.probe.auto(st.behind)}`));
|
|
382
|
-
await cmdProbe([], { auto: true });
|
|
383
|
-
}
|
|
384
|
-
} catch { /* проба не состоялась — прогон это не роняет: он про гейты, а не про неё */ }
|
|
385
|
-
}
|
|
439
|
+
if (!brief && process.env.AQK_PROBE !== "0") await autoProbe(brief);
|
|
386
440
|
} else if (gates.length) {
|
|
387
441
|
console.log(
|
|
388
442
|
c.yellow(` ${L.doctor.declaredNotRun(gates.length)}`) +
|
|
@@ -417,6 +471,7 @@ async function cmdDoctor() {
|
|
|
417
471
|
if (wantRun) {
|
|
418
472
|
if (ok) {
|
|
419
473
|
console.log(c.green(` ${L.doctor.runVerdictOk}\n`));
|
|
474
|
+
if (skippedNames.length) console.log(c.yellow(` ${L.doctor.selectSkipped(skippedNames.join(", "))}\n`));
|
|
420
475
|
} else {
|
|
421
476
|
const why = [];
|
|
422
477
|
if (missing) why.push(L.doctor.whyMissing);
|
package/tool/commands/probe.mjs
CHANGED
|
@@ -26,8 +26,9 @@ 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
33
|
import { probeState, probeEvery, PROBE_EVERY, blindLines, parseBlind, parseRan } from "../lib/cadence.mjs";
|
|
33
34
|
import { L } from "../i18n/index.mjs";
|
|
@@ -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 || "—")}`)); }
|
|
@@ -465,4 +445,4 @@ async function probeStatus() {
|
|
|
465
445
|
return { ...probeState(mark, commitCount(), every), classes: parseBlind(mark?.text), ran: parseRan(mark?.text) };
|
|
466
446
|
}
|
|
467
447
|
|
|
468
|
-
export { cmdProbe, probeStatus, probeableGates, gatesState, extAlternatives, planProbeGates,
|
|
448
|
+
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}`);
|
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 });
|